UNPKG

@interopio/gateway-server

Version:

## `@glue42/gateway-ent` compatibility

160 lines (140 loc) 5.2 kB
import * as ws from 'ws'; import {socketKey} from '../../../utils.js'; import getLogger from '../../../logger.js'; import {IOGateway} from '@interopio/gateway'; import GatewayEncoders = IOGateway.Encoding; const logger = getLogger('mesh.ws.relay'); type Command = { type: 'hello', 'from': string, to: string } | { type: 'bye', 'from': string, to: 'all' | string } | { type: 'data', from: string, to: 'all' | string, data: {body: {type: string}}} ; const codec = GatewayEncoders.transit<Command>({ keywordize: new Map<string, GatewayEncoders.KeywordizeCommand>([ ['/type', '*'], ['/message/body/type', '*'], ['/message/origin', '*'], ['/message/receiver/type', '*'], ['/message/source/type', '*'], ['/message/body/type', '*'], ]) }); export interface Relays { onMsg?: ((key: string, node: string, msg: string) => void); onErr?: ((key: string, err: Error) => void) send(key: string, node: string | undefined, msg: string, cb: (key: string, err?: Error) => void): void; } class InternalRelays implements Relays { // key -> socket private readonly clients = new Map<string, ws.WebSocket>(); // node -> key private readonly links = new Map<string, string>(); public onMsg?: (key: string, node: string, msg: string) => void public onErr?: (key: string, err: Error) => void add(key: string, soc: ws.WebSocket) { this.clients.set(key, soc); } remove(key: string) { this.clients.delete(key); for (const [node, k] of this.links) { if (k === key) { this.links.delete(node); } } } receive(key: string, msg: string) { const node = this.link(key, msg); if (node && this.onMsg) { this.onMsg(key, node, msg); } } private link(key: string, msg: string): string | undefined { try { const decoded = codec.decode(msg); const {type, from, to} = decoded; if (to === 'all') { switch (type) { case 'hello': { if (logger.enabledFor('debug')) { logger.debug(`${key} registers node ${from}`); } this.links.set(from, key); break; } case 'bye': { if (logger.enabledFor('debug')) { logger.debug(`${key} unregisters node ${from}`); } this.links.delete(from); break; } } return; } return from; } catch (e) { if (this.onErr) { // probably a decode error or a bug this.onErr(key, e instanceof Error ? e : new Error(`link failed :${e}`)); } else { logger.warn(`${key} unable to process ${msg}`, e); } } } send(key: string, node: string, msg: string, cb: (k: string, err?: Error) => void): void { const decoded = codec.decode(msg); if (logger.enabledFor('debug')) { logger.debug(`${key} sending msg to ${node} ${JSON.stringify(decoded)}`); } const clientKey = this.links.get(node); if (clientKey) { const client = this.clients.get(clientKey); if (client) { client.send(msg, {binary: false}, (err?: Error) => { cb(clientKey, err) }); return; } } throw new Error(`${key} no active link for ${decoded.to}`); } } const internal = new InternalRelays(); export const relays: Relays = internal; async function create(server: {wss:ws.WebSocketServer}): Promise<{ close?: () => Promise<void> }> { logger.info(`relays server is listening`); server.wss .on('error', () => { logger.error(`error starting relays server`); }) .on('connection', (socket, request) => { const key = socketKey(request.socket); logger.info(`${key} connected on relays`); internal.add(key, socket); //onOpen(connectedNodes, key); socket.on('error', (err: Error) => { logger.error(`[${key}] websocket error: ${err}`, err); }); socket.on('message', (data, _isBinary) => { if (Array.isArray(data)) { data = Buffer.concat(data); } try { internal.receive(key, data as unknown as string); } catch (e) { logger.warn(`[${key}] error processing received data '${data}'`, e); } }); socket.on('close', (code, reason) => { internal.remove(key); logger.info(`${key} disconnected from relays`); }); }); return { close: async () => { server.wss.close(); } }; } export default create;