import {StateEvent, WebSocketBus} from '../../support/bus' import {constructable, Constructable, Instantiable, TypedDependencyKey} from '../../di' import {Awaitable, Collection, JSONState} from '../../util' import {Request} from '../lifecycle/Request' export type SocketEventHandler = { eventClass: Instantiable>, handler: Constructable<(state: TState) => Awaitable>, } /** * Helper class for building websocket APIs using first-class route syntax. * This is returned by the `Route.socket(...)` method, so you'll probably want * to use that. * * @see Route#socket * @example * ```ts * Route.socket('/ws/endpoint') * .connected(MyCtrl, (ctrl: MyCtrl) => ctrl.connect) * .event(MyEvent, MyCtrl, (ctrl: MyCtrl) => ctrl.myHandler) * ``` */ export class SocketRouteBuilder { public static get(): SocketRouteBuilder { return new SocketRouteBuilder() } /** Handlers that should be registered with any new socket connections. */ protected handlers: Collection> = new Collection() /** Callback to execute when a new connection is opened. */ protected connectionCallback?: Constructable<(ws: WebSocketBus) => Awaitable> /** * Register a callback to execute each time a new socket is opened. * This can be used to perform basic setup/validation/authentication tasks. * * @example * ```ts * Route.socket('/ws/endpoint') * .connected(MyCtrl, (ctrl: MyCtrl) => ctrl.connect) * ``` * * @param key * @param selector */ connected( key: TypedDependencyKey, selector: (x: TKey) => (ws: WebSocketBus) => Awaitable, ): this { this.connectionCallback = constructable(key) .tap(inst => Function.prototype.bind.call(selector(inst), inst as any) as ((ws: WebSocketBus) => Awaitable)) return this } /** * Register a `StateEvent` listener on the socket. * * @example * ```ts * Route.socket('/ws/endpoint') * .event(MyEvent, MyCtrl, (ctrl: MyCtrl) => ctrl.myHandler) * ``` * * @see StateEvent * @param eventClass * @param key * @param selector */ event( eventClass: Instantiable>, key: TypedDependencyKey, selector: (x: TKey) => (state: TState) => Awaitable, ): this { const handler = constructable(key) .tap(inst => Function.prototype.bind.call(selector(inst), inst as any) as ((state: TState) => Awaitable)) this.handlers.push({ eventClass, handler, }) return this } /** * Attaches event listeners & initial callback to the given socket. This is used * by as the handler for new connections. * * @see Route#socket * @param request * @param ws */ async build(request: Request, ws: WebSocketBus): Promise { await this.handlers.promiseMap(handler => { ws.subscribe(handler.eventClass, (event: StateEvent) => { return handler.handler.apply(request)(event.getState()) }) }) if ( this.connectionCallback ) { await this.connectionCallback.apply(request)(ws) } } }