@loopback/socketio
Version:
LoopBack's WebSocket server based on socket.io
179 lines (164 loc) • 5.53 kB
text/typescript
// 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);
};
}
}