Skip to content

Commit

Permalink
feat(event): consolidate events through a bus
Browse files Browse the repository at this point in the history
  • Loading branch information
ssube committed May 25, 2021
1 parent 466b8cb commit 9f9e6b8
Show file tree
Hide file tree
Showing 19 changed files with 222 additions and 151 deletions.
10 changes: 8 additions & 2 deletions src/module/LocalModule.ts
Expand Up @@ -3,6 +3,7 @@ import { Logger, Module, ModuleOptions, Provides } from 'noicejs';

import {
INJECT_COUNTER,
INJECT_EVENT,
INJECT_LOCALE,
INJECT_LOGGER,
INJECT_PARSER,
Expand All @@ -11,6 +12,10 @@ import {
INJECT_STATE,
INJECT_TEMPLATE,
} from '.';
import { Counter } from '../service/counter';
import { LocalCounter } from '../service/counter/LocalCounter';
import { EventBus } from '../service/event';
import { NodeEventBus } from '../service/event/NodeEventBus';
import { LocaleService } from '../service/locale';
import { NextLocaleService } from '../service/locale/NextLocale';
import { YamlParser } from '../service/parser/YamlParser';
Expand All @@ -23,11 +28,10 @@ import { LocalStateService } from '../service/state/TurnState';
import { TemplateService } from '../service/template';
import { ChainTemplateService } from '../service/template/ChainTemplateService';
import { Singleton } from '../util/container';
import { Counter } from '../service/counter';
import { LocalCounter } from '../service/counter/LocalCounter';

export class LocalModule extends Module {
protected counter: Singleton<Counter>;
protected event: Singleton<EventBus>;
protected locale: Singleton<LocaleService>;
protected random: Singleton<RandomGenerator>;
protected script: Singleton<ScriptService>;
Expand All @@ -38,6 +42,7 @@ export class LocalModule extends Module {
super();

this.counter = new Singleton(() => mustExist(this.container).create(LocalCounter));
this.event = new Singleton(() => mustExist(this.container).create(NodeEventBus));
this.locale = new Singleton(() => mustExist(this.container).create(NextLocaleService));
this.random = new Singleton(() => mustExist(this.container).create(SeedRandomGenerator));
this.script = new Singleton(() => mustExist(this.container).create(LocalScriptService));
Expand All @@ -48,6 +53,7 @@ export class LocalModule extends Module {
public async configure(options: ModuleOptions): Promise<void> {
await super.configure(options);

this.bind(INJECT_EVENT).toFactory(() => this.event.get());
this.bind(INJECT_PARSER).toConstructor(YamlParser);
}

Expand Down
1 change: 1 addition & 0 deletions src/module/index.ts
@@ -1,5 +1,6 @@
// service symbols
export const INJECT_COUNTER = Symbol('inject-counter');
export const INJECT_EVENT = Symbol('inject-event-bus');
export const INJECT_LOADER = Symbol('inject-loader');
export const INJECT_LOCALE = Symbol('inject-locale');
export const INJECT_LOGGER = Symbol('inject-logger');
Expand Down
5 changes: 2 additions & 3 deletions src/service/actor/BehaviorActor.ts
@@ -1,5 +1,4 @@
import { NotImplementedError } from '@apextoaster/js-utils';
import { EventEmitter } from 'events';
import { BaseOptions } from 'noicejs';

import { ActorService } from '.';
Expand All @@ -17,9 +16,9 @@ const WAIT_CMD: Command = {
* Behavioral input generates commands based on the actor's current
* state (room, inventory, etc).
*/
export class BehaviorActorService extends EventEmitter implements ActorService {
export class BehaviorActorService implements ActorService {
constructor(options: BaseOptions) {
super();
/* noop */
}

public async start() {
Expand Down
53 changes: 29 additions & 24 deletions src/service/actor/PlayerActor.ts
@@ -1,22 +1,16 @@
import { constructorName, mustExist, NotImplementedError } from '@apextoaster/js-utils';
import { EventEmitter } from 'events';
import { BaseOptions, Inject, Logger } from 'noicejs';

import { ActorService, InputEvent } from '.';
import { ActorService } from '.';
import { Command } from '../../model/Command';
import { INJECT_LOCALE, INJECT_LOGGER, INJECT_TOKENIZER } from '../../module';
import { VERB_WAIT } from '../../util/constants';
import { INJECT_EVENT, INJECT_LOCALE, INJECT_LOGGER, INJECT_TOKENIZER } from '../../module';
import { KNOWN_VERBS } from '../../util/constants';
import { EventBus, InputEvent, OutputEvent } from '../event';
import { LocaleService } from '../locale';
import { TokenizerService } from '../tokenizer';

const WAIT_CMD: Command = {
index: 0,
input: `${VERB_WAIT} turn`,
verb: VERB_WAIT,
target: 'turn',
};

export interface PlayerActorOptions extends BaseOptions {
[INJECT_EVENT]?: EventBus;
[INJECT_LOCALE]?: LocaleService;
[INJECT_LOGGER]?: Logger;
[INJECT_TOKENIZER]?: TokenizerService;
Expand All @@ -26,31 +20,32 @@ export interface PlayerActorOptions extends BaseOptions {
* Behavioral input generates commands based on the actor's current
* state (room, inventory, etc).
*/
@Inject(INJECT_LOCALE, INJECT_LOGGER, INJECT_TOKENIZER)
export class PlayerActorService extends EventEmitter implements ActorService {
protected history: Array<Command>;
@Inject(INJECT_EVENT, INJECT_LOCALE, INJECT_LOGGER, INJECT_TOKENIZER)
export class PlayerActorService implements ActorService {
protected event: EventBus;
protected locale: LocaleService;
protected logger: Logger;
protected tokenizer: TokenizerService;

constructor(options: PlayerActorOptions) {
super();
protected history: Array<Command>;

constructor(options: PlayerActorOptions) {
this.history = [];

this.event = mustExist(options[INJECT_EVENT]);
this.locale = mustExist(options[INJECT_LOCALE]);
this.logger = mustExist(options[INJECT_LOGGER]).child({
kind: constructorName(PlayerActorService),
kind: constructorName(this),
});
this.tokenizer = mustExist(options[INJECT_TOKENIZER]);
}

public async start() {
this.on('input', (event: InputEvent) => this.doInput(event));
this.on('room', async () => {
this.emit('command', {
command: await this.last(),
});
});
this.event.on('render-output', (event) => this.doInput(event));
this.event.on('state-output', (event) => this.doOutput(event));

await this.locale.start();
await this.tokenizer.translate(KNOWN_VERBS);
}

public async stop() {
Expand All @@ -73,10 +68,20 @@ export class PlayerActorService extends EventEmitter implements ActorService {
this.history.push(...commands);

for (const command of commands) {
this.emit('command', {
this.event.emit('actor-command', {
command,
});
}
}
}

public async doOutput(event: OutputEvent): Promise<void> {
this.logger.debug({ event }, 'translating output');

const lines = event.lines.map((it) => this.locale.translate(it));
this.event.emit('actor-output', {
lines,
step: event.step,
});
}
}
36 changes: 1 addition & 35 deletions src/service/actor/index.ts
@@ -1,45 +1,11 @@
import { EventEmitter } from 'events';

import { Service } from '..';
import { Command } from '../../model/Command';
import { Actor } from '../../model/entity/Actor';
import { Room } from '../../model/entity/Room';
import { ErrorHandler, EventHandler } from '../../util/types';
import { StepResult } from '../state';

export interface InputEvent {
lines: Array<string>;
}

export interface RoomEvent {
room: Room;
}

export interface OutputEvent {
lines: Array<string>;
step: StepResult;
}

export interface CommandEvent {
actor: Actor;
command: Command;
}

export interface ActorService extends EventEmitter, Service {
export interface ActorService extends Service {
last(): Promise<Command>;

/**
* @todo remove, do in start
*/
translate(verbs: ReadonlyArray<string>): Promise<void>;

emit(name: 'input', event: InputEvent): boolean;
emit(name: 'room', event: RoomEvent): boolean;
emit(name: 'output', event: OutputEvent): boolean;

on(name: 'command', handler: EventHandler<CommandEvent>): this;
on(name: 'error', handler: ErrorHandler): this;
on(name: 'output', handler: EventHandler<OutputEvent>): this;
on(name: 'quit', handler: EventHandler<void>): this;
on(name: 'room', handler: EventHandler<RoomEvent>): this;
}
32 changes: 32 additions & 0 deletions src/service/event/NodeEventBus.ts
@@ -0,0 +1,32 @@
import { constructorName, mustExist } from '@apextoaster/js-utils';
import { EventEmitter } from 'events';
import { BaseOptions, Inject, Logger } from 'noicejs';

import { EventBus } from '.';
import { INJECT_LOGGER } from '../../module';

interface EventBusOptions extends BaseOptions {
[INJECT_LOGGER]?: Logger;
}

@Inject(INJECT_LOGGER)
export class NodeEventBus extends EventEmitter implements EventBus {
protected logger: Logger;

constructor(options: EventBusOptions) {
super();

this.logger = mustExist(options[INJECT_LOGGER]).child({
kind: constructorName(this),
});
}

public emit(name: string, ...args: Array<unknown>): boolean {
this.logger.debug({
eventArgs: args,
eventName: name,
}, 'bus proxying event');

return super.emit(name, ...args);
}
}
68 changes: 68 additions & 0 deletions src/service/event/index.ts
@@ -0,0 +1,68 @@
import { EventEmitter } from 'events';

import { Command } from '../../model/Command';
import { Actor } from '../../model/entity/Actor';
import { Room } from '../../model/entity/Room';
import { ErrorHandler, EventHandler } from '../../util/event';
import { StepResult } from '../state';

export interface InputEvent {
lines: Array<string>;
}

export interface RoomEvent {
room: Room;
}

export interface OutputEvent {
lines: Array<string>;
step: StepResult;
}

export interface CommandEvent {
// actor: Actor;
command: Command;
}

export interface EventBus extends EventEmitter {
emit(name: 'error', err: Error): boolean;
emit(name: 'quit'): boolean;
emit(name: 'step', step: StepResult): boolean;

/**
* Parsed commands coming from actor service.
*/
emit(name: 'actor-command', event: CommandEvent): boolean;

/**
* Translated output coming from actor service.
*/
emit(name: 'actor-output', event: OutputEvent): boolean;

/**
* Unparsed input coming from render service.
*/
emit(name: 'render-output', event: InputEvent): boolean;

/**
* Updated room events coming from state service.
*/
emit(name: 'state-room', event: RoomEvent): boolean;

/**
* Untranslated output coming from state service.
*/
emit(name: 'state-output', event: OutputEvent): boolean;

on(name: 'actor-command', handler: EventHandler<CommandEvent>): this;
on(name: 'actor-output', handler: EventHandler<OutputEvent>): this;
on(name: 'render-output', handler: EventHandler<InputEvent>): this;
on(name: 'state-room', event: EventHandler<RoomEvent>): this;
on(name: 'state-output', event: EventHandler<OutputEvent>): this;

// unqualified
on(name: 'error', handler: ErrorHandler): this;
on(name: 'quit', event: EventHandler<void>): this;
on(name: 'step', event: EventHandler<StepResult>): this;
}

12 changes: 6 additions & 6 deletions src/service/render/BaseRender.ts
Expand Up @@ -2,33 +2,33 @@ import { constructorName, mustExist } from '@apextoaster/js-utils';
import { BaseOptions, Inject, Logger } from 'noicejs';

import { RenderService } from '.';
import { INJECT_ACTOR_PLAYER, INJECT_LOCALE, INJECT_LOGGER } from '../../module';
import { ActorService } from '../actor';
import { INJECT_EVENT, INJECT_LOCALE, INJECT_LOGGER } from '../../module';
import { EventBus } from '../event';
import { LocaleService } from '../locale';
import { StepResult } from '../state';

export interface BaseRenderOptions extends BaseOptions {
[INJECT_ACTOR_PLAYER]?: ActorService;
[INJECT_EVENT]?: EventBus;
[INJECT_LOCALE]?: LocaleService;
[INJECT_LOGGER]?: Logger;
}

@Inject(INJECT_ACTOR_PLAYER, INJECT_LOCALE, INJECT_LOGGER)
@Inject(INJECT_EVENT, INJECT_LOCALE, INJECT_LOGGER)
export abstract class BaseRender implements RenderService {
// services
protected event: EventBus;
protected logger: Logger;
protected locale: LocaleService;
protected player: ActorService;

// state
protected step: StepResult;

constructor(options: BaseRenderOptions) {
this.event = mustExist(options[INJECT_EVENT]);
this.locale = mustExist(options[INJECT_LOCALE]);
this.logger = mustExist(options[INJECT_LOGGER]).child({
kind: constructorName(this),
});
this.player = mustExist(options[INJECT_ACTOR_PLAYER]);

this.step = {
turn: 0,
Expand Down
14 changes: 6 additions & 8 deletions src/service/render/InkRender.ts
Expand Up @@ -6,7 +6,7 @@ import * as React from 'react';
import { RenderService } from '.';
import { Frame } from '../../component/ink/Frame';
import { onceWithRemove } from '../../util/event';
import { OutputEvent, RoomEvent } from '../actor';
import { OutputEvent, RoomEvent } from '../event';
import { BaseRender, BaseRenderOptions } from './BaseRender';

export interface InkState {
Expand Down Expand Up @@ -41,7 +41,7 @@ export class InkRender extends BaseRender implements RenderService {
}

public async read(): Promise<string> {
const { pending } = onceWithRemove<OutputEvent>(this.player, 'output');
const { pending } = onceWithRemove<OutputEvent>(this.event, 'output');
const event = await pending;

return event.lines[0];
Expand All @@ -57,11 +57,9 @@ export class InkRender extends BaseRender implements RenderService {
this.renderRoot();
this.prompt(`turn ${this.step.turn}`);

this.player.on('output', (output) => this.onOutput(output));
this.player.on('quit', () => this.onQuit());
this.player.on('room', (room) => this.onRoom(room));

await this.player.start(); // TODO: services absolutely should never start/stop one another
this.event.on('actor-output', (output) => this.onOutput(output));
this.event.on('state-room', (room) => this.onRoom(room));
this.event.on('quit', () => this.onQuit());
}

public async stop(): Promise<void> {
Expand All @@ -84,7 +82,7 @@ export class InkRender extends BaseRender implements RenderService {
this.output.push(`${this.promptStr} > ${this.inputStr}`);

// forward event to state
this.player.emit('input', {
this.event.emit('render-output', {
lines: [line],
});
}
Expand Down

0 comments on commit 9f9e6b8

Please sign in to comment.