UNPKG

@interopio/gateway-server

Version:

## `@glue42/gateway-ent` compatibility

164 lines (146 loc) 5.78 kB
import * as ws from 'ws'; import getLogger from '../../../logger.js'; import {socketKey} from '../../../utils.ts'; import {IOGateway} from '@interopio/gateway'; import GatewayEncoders = IOGateway.Encoding; const logger = getLogger('mesh.ws.broker'); type Command = // client to broker { type: 'hello', 'node-id': string } | { type: 'bye', 'node-id': string } | { type: 'data', from: string, to: 'all' | string} // broker to client | { type: 'node-added', 'node-id': string, 'new-node': string } | { type: 'node-removed', 'node-id': string, 'removed-node': string } ; function broadcastNodeAdded(nodes: NodeSockets, newSocket: ws.WebSocket, newNodeId: string) { Object.entries(nodes.nodes).forEach(([nodeId, socket]) => { if (nodeId !== newNodeId) { newSocket.send(codec.encode({type: 'node-added', 'node-id': newNodeId, "new-node": nodeId})); socket.send(codec.encode({type: 'node-added', 'node-id': nodeId, "new-node": newNodeId})); } }); } function broadcastNodeRemoved(nodes: NodeSockets, removedNodeId: string) { Object.entries(nodes.nodes).forEach(([nodeId, socket]) => { if (nodeId !== removedNodeId) { socket.send(codec.encode({type: 'node-removed', 'node-id': nodeId, "removed-node": removedNodeId})); } }); } function onOpen(connectedNodes: NodeSockets, key: string) { logger.info(`[${key}] connection accepted`); } function onClose(connectedNodes: NodeSockets, key: string, code: number, reason: string): void { logger.info(`[${key}] connected closed [${code}](${reason})`); const nodeIds = connectedNodes.sockets[key]; if (nodeIds) { delete connectedNodes.sockets[key]; for (const nodeId of nodeIds) { delete connectedNodes.nodes[nodeId]; } for (const nodeId of nodeIds) { broadcastNodeRemoved(connectedNodes, nodeId); } } } function processMessage(connectedNodes: NodeSockets, socket: ws.WebSocket, key: string, msg: Command) { switch (msg.type) { case 'hello': { const nodeId = msg['node-id']; connectedNodes.nodes[nodeId] = socket; connectedNodes.sockets[key] = connectedNodes.sockets[key] ?? []; connectedNodes.sockets[key].push(nodeId); logger.info(`[${key}] node ${nodeId} added.`); broadcastNodeAdded(connectedNodes, socket, nodeId); break; } case 'bye': { const nodeId = msg["node-id"]; delete connectedNodes[nodeId]; logger.info(`[${key}] node ${nodeId} removed.`); broadcastNodeRemoved(connectedNodes, nodeId); break; } case 'data': { const sourceNodeId = msg.from; const targetNodeId = msg.to; if ('all' === targetNodeId) { Object.entries(connectedNodes.nodes).forEach(([nodeId, socket]) => { if (nodeId !== sourceNodeId) { socket.send(codec.encode(msg)); } }); } else { const socket = connectedNodes.nodes[targetNodeId]; if (socket) { socket.send(codec.encode(msg)); } else { logger.warn(`unable to send to node ${targetNodeId} message ${JSON.stringify(msg)}`); } } break; } default: { logger.warn(`[${key}] ignoring unknown message ${JSON.stringify(msg)}`); break; } } } 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', '*'], ]) }); function onMessage(connectedNodes: NodeSockets, socket: ws.WebSocket, key: string, msg: string): void { try { const decoded = codec.decode(msg); if (logger.enabledFor('debug')) { logger.debug(`[${key}] processing msg ${JSON.stringify(decoded)}`); } processMessage(connectedNodes, socket, key, decoded); } catch (ex) { logger.error(`[${key}] unable to process message`, ex); } } class WebsocketBroker { constructor(private readonly server: ws.WebSocketServer) { } async close() { this.server.close(); } } type NodeSockets = {nodes: {[nodeId: string]: ws.WebSocket}, sockets: {[socket: string]: string[]}}; async function create(server: {wss: ws.WebSocketServer}): Promise<{close: () => Promise<void>}> { const connectedNodes: NodeSockets = {nodes: {}, sockets: {}}; logger.info(`mesh server is listening`); server.wss .on('error', () => { logger.error(`error starting mesh server`); }) .on('connection', (socket, request) => { const key = socketKey(request.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); } onMessage(connectedNodes, socket, key, data as unknown as string); }); socket.on('close', (code, reason) => { onClose(connectedNodes, key, code, reason as unknown as string); }); }); return new WebsocketBroker(server.wss); } export default create;