@interopio/gateway-server
Version:
## `@glue42/gateway-ent` compatibility
164 lines (146 loc) • 5.78 kB
text/typescript
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;