UNPKG

@loopback/socketio

Version:

LoopBack's WebSocket server based on socket.io

179 lines (164 loc) 5.53 kB
// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved. // Node module: @loopback/socketio // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import { BindingScope, Constructor, Context, CoreBindings, DecoratorType, invokeMethod, MetadataAccessor, MetadataInspector, MetadataMap, } from '@loopback/core'; import {Socket} from 'socket.io'; import { SOCKET_IO_CONNECT_METADATA, SOCKET_IO_SUBSCRIBE_METADATA, } from './decorators'; import {SocketIoBindings} from './keys'; import {SocketIoDoneFunction} from './types'; type SocketIoEventMatcherInfo = { matcher: string | RegExp; methodNames: string[]; }; /** * Request context for a socket.io request */ export class SocketIoConnectionContext extends Context { constructor( public readonly socket: Socket, parent: Context, ) { super(parent); } } /** * A factory to instantiate socket.io controllers */ export class SocketIoControllerFactory { private controller: {[method: string]: Function}; public readonly connCtx: SocketIoConnectionContext; constructor( parentCtx: Context, private controllerClass: Constructor<object>, socket: Socket, ) { this.connCtx = new SocketIoConnectionContext(socket, parentCtx); this.connCtx.bind(SocketIoBindings.SOCKET).to(this.connCtx.socket); this.connCtx.bind(CoreBindings.CONTROLLER_CLASS).to(this.controllerClass); this.connCtx .bind(CoreBindings.CONTROLLER_CURRENT) .toClass(controllerClass) .inScope(BindingScope.SINGLETON); } async create() { // Instantiate the controller instance this.controller = await this.connCtx.get<{[method: string]: Function}>( CoreBindings.CONTROLLER_CURRENT, ); await this.setup(); return this.controller; } /** * Set up the controller for the given socket */ async setup() { await this.connect(); this.registerSubscribeMethods(); } async connect() { const connectMethods = this.getDecoratedMethodsForConnect(); for (const methodName in connectMethods) { await invokeMethod(this.controller, methodName, this.connCtx, [ this.connCtx.socket, ]); } } protected registerSubscribeMethods() { const methodsByEventHandler = this.getDecorateSubscribeMethodsByEventName(); const regexMethodsHandlers = new Map<RegExp, Function[]>(); const methodHandlers = new Map<String, (...args: unknown[]) => unknown>(); methodsByEventHandler.forEach(eventMatcherInfo => { const {matcher, methodNames} = eventMatcherInfo; methodNames.forEach(methodName => { let handler = methodHandlers.get(methodName); if (!handler) { handler = this.getCallback(methodName); methodHandlers.set(methodName, handler); } if (matcher instanceof RegExp) { const handlers = regexMethodsHandlers.get(matcher) ?? []; handlers.push(handler); regexMethodsHandlers.set(matcher, handlers); } else { this.connCtx.socket.on(matcher, handler); } }); }); // Register event handlers with regexp if (regexMethodsHandlers.size) { // Use a socket middleware to match event names with regexp // eslint-disable-next-line @typescript-eslint/no-misused-promises this.connCtx.socket.use(async (packet, next) => { const [eventName, ...args] = packet; for (const iterator of regexMethodsHandlers.entries()) { const [regex, handlers] = iterator; if (eventName.match(regex)) { for (const handler of handlers) { await handler(args); } } } next(); }); } } getDecoratedMethodsForConnect() { return this.getAllMethodMetadataForKey(SOCKET_IO_CONNECT_METADATA); } getDecorateSubscribeMethodsByEventName() { const eventMatchersInfo = new Map<string, SocketIoEventMatcherInfo>(); const subscribeMethods = this.getDecorateSubscribeMethods(); for (const methodName in subscribeMethods) { for (const matcher of subscribeMethods[methodName]) { const matcherString = matcher.toString(); const eventMatcherInfo: SocketIoEventMatcherInfo = eventMatchersInfo.get(matcherString) ?? { matcher: matcher, methodNames: [], }; eventMatcherInfo.methodNames.push(methodName); eventMatchersInfo.set(matcherString, eventMatcherInfo); } } return eventMatchersInfo; } protected getDecorateSubscribeMethods() { return this.getAllMethodMetadataForKey(SOCKET_IO_SUBSCRIBE_METADATA); } protected getAllMethodMetadataForKey<V, DT extends DecoratorType>( metadataAccessor: MetadataAccessor<V, DT>, ): MetadataMap<V> { return ( MetadataInspector.getAllMethodMetadata( metadataAccessor, this.controllerClass.prototype, ) ?? ({} as MetadataMap<V>) ); } public getCallback(methodName: string) { return async (...args: unknown[]) => { let done: SocketIoDoneFunction = async (_response: unknown) => {}; if (typeof args[args.length - 1] === 'function') { done = args.pop() as SocketIoDoneFunction; } const eventCtx = new Context(this.connCtx); eventCtx.bind(SocketIoBindings.MESSAGE).to(args); const sequence = await eventCtx.get(SocketIoBindings.SEQUENCE); await sequence.handle(methodName, args, done); }; } }