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