import {Container, Inject, Injectable, Instantiable} from '../../di' import {Awaitable, ErrorWithContext, hasOwnProperty, 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 } } /** A serialized `StateEvent` instance. */ export type SerializedStateEvent = { state: T, eventUuid: string, } export function isSerializedStateEvent(what: unknown): what is SerializedStateEvent { return ( typeof what === 'object' && what !== null && hasOwnProperty(what, 'state') && hasOwnProperty(what, 'eventUuid') && typeof what.eventUuid === 'string' ) } /** * 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, SerializedStateEvent> { @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 { state: actual.getState(), eventUuid: actual.eventUuid, } } protected decodeSerial(serial: SerializedStateEvent): Awaitable> { const inst = this.injector.makeNew>(this.eventClass) if ( !isSerializedStateEvent(serial) || !inst.validate(serial.state) ) { throw new ErrorWithContext('Invalid serial state', { serial, eventClass: inst.getName(), }) } inst.setState(serial.state) inst.eventUuid = serial.eventUuid return inst } protected getName(): string { const inst = this.injector.makeNew>(this.eventClass) return `${inst.getName()}Serializer` } }