diff --git a/package.json b/package.json index 22f74fc..d04629c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@extollo/lib", - "version": "0.11.0", + "version": "0.12.0", "description": "The framework library that lifts up your code.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/support/bus/StateEvent.ts b/src/support/bus/StateEvent.ts new file mode 100644 index 0000000..842f691 --- /dev/null +++ b/src/support/bus/StateEvent.ts @@ -0,0 +1,122 @@ +import {Container, Inject, Injectable, Instantiable} from '../../di' +import {Awaitable, ErrorWithContext, JSONState, uuid4} from '../../util' +import {BaseSerializer} from './serial/BaseSerializer' +import {Event} from './types' + +/** + * An event base class that provides its own validator and serializer implementation. + * This is meant to streamline event creation for app-space by forcing the payload of + * the event to be a JSONState. + */ +@Injectable() +export abstract class StateEvent implements Event { + @Inject() + protected readonly injector!: Container + + /** The payload of the event. */ + private state?: T + + eventUuid = uuid4() + + public get eventName(): string { + return this.getName() + } + + /** Validate the incoming data. */ + public abstract validate(state: JSONState): state is T + + /** Get the event's name for the bus. */ + public getName(): string { + const ctor = this.constructor as typeof StateEvent + return `state:${ctor.name}` + } + + /** Get the Serializer implementation for this event. */ + public getSerializer(): StateEventSerializer { + const ctor = this.constructor as typeof StateEvent + return this.injector.makeNew(StateEventSerializer, ctor) + } + + /** Validate & set the internal state of this event instance. */ + public setState(state: JSONState): this { + if ( !this.validate(state) ) { + throw new ErrorWithContext('Invalid state', { + state, + }) + } + + this.state = state + return this + } + + /** Get the internal state of this event, validated. */ + public getState(): T { + if ( typeof this.state === 'undefined' ) { + throw new ErrorWithContext('State has not been set on event') + } + + return this.state + } +} + +/** + * Generic serializer implementation for StateEvents. + * Note that this is NOT registered with @ObjectSerializer() because we need to + * create an instance of this class for EACH implementation of StateEvent. + * Still working that one out. For now, you'll need to manually register the + * serializer. You can do this with the `getSerializer` method. + * + * @example + * ```ts + * type MyState = { + * id: number + * } + * + * class MyStateEvent extends StateEvent { + * public validate(state: JSONState): state is MyState { + * return is(state) + * } + * } + * ``` + */ +@Injectable() +export class StateEventSerializer extends BaseSerializer, JSONState> { + @Inject() + protected readonly injector!: Container + + /** The StateEvent implementation. */ + public readonly eventClass: Instantiable>; + + constructor( + eventClass: Instantiable>, + ) { + super() + this.eventClass = eventClass + } + + matchActual(some: StateEvent): boolean { + return some instanceof this.eventClass + } + + protected encodeActual(actual: StateEvent): Awaitable { + return actual.getState() + } + + protected decodeSerial(serial: JSONState): Awaitable> { + const inst = this.injector.makeNew>(this.eventClass) + if ( !inst.validate(serial) ) { + throw new ErrorWithContext('Invalid serial state', { + serial, + eventClass: inst.getName(), + }) + } + + inst.setState(serial) + return inst + } + + protected getName(): string { + const inst = this.injector.makeNew>(this.eventClass) + return `${inst.getName()}Serializer` + } +} diff --git a/src/support/bus/WebSocketBus.ts b/src/support/bus/WebSocketBus.ts index 87f01ae..8771178 100644 --- a/src/support/bus/WebSocketBus.ts +++ b/src/support/bus/WebSocketBus.ts @@ -1,4 +1,4 @@ -import {AwareOfContainerLifecycle, Container, Inject, Injectable, StaticInstantiable} from '../../di' +import {AwareOfContainerLifecycle, Inject, Injectable, StaticInstantiable} from '../../di' import { EventBus, Event, diff --git a/src/support/bus/serial/Serialization.ts b/src/support/bus/serial/Serialization.ts index 93db927..8c8252d 100644 --- a/src/support/bus/serial/Serialization.ts +++ b/src/support/bus/serial/Serialization.ts @@ -64,10 +64,21 @@ export class Serialization { protected serializers: Collection>> = new Collection() /** Register a new serializer with the service. */ - public register(serializer: Instantiable>): this { + public register(key: Instantiable>): this { // Prepend instead of push so that later-registered serializers are prioritized when matching this.serializers.prepend({ - key: serializer, + key, + }) + + return this + } + + /** Register an already-realized serializer instance with this service. */ + public registerInstance(key: Instantiable>, instance: Serializer): this { + // Prepend instead of push so that later-registered serializers are prioritized when matching + this.serializers.prepend({ + key, + instance, }) return this