@devgrid/netron
Version:
Event bus, streams and remote object invocation.
187 lines • 7.2 kB
JavaScript
import path from 'node:path';
import cuid from '@bugsnag/cuid';
import { WebSocket, WebSocketServer } from 'ws';
import { AsyncEventEmitter } from '@devgrid/async-emitter';
import { LocalPeer } from './local-peer';
import { RemotePeer } from './remote-peer';
import { TaskManager } from './task-manager';
import { CONNECT_TIMEOUT, getPeerEventName, NETRON_EVENT_PEER_CONNECT, NETRON_EVENT_PEER_DISCONNECT } from './common';
export class Netron extends AsyncEventEmitter {
constructor(options) {
super();
this.options = options;
this.ownEvents = new Map();
this.peers = new Map();
this.isStarted = false;
this.services = new Map();
this.id = options?.id ?? cuid();
this.taskManager = new TaskManager({
timeout: options?.taskTimeout,
overwriteStrategy: options?.taskOverwriteStrategy,
});
this.peer = new LocalPeer(this);
}
async start() {
if (this.isStarted) {
throw new Error('Netron already started');
}
await this.taskManager.loadTasksFromDir(path.join(__dirname, 'core-tasks'));
if (!this.options?.listenHost || !this.options?.listenPort) {
this.isStarted = true;
return Promise.resolve();
}
return new Promise((resolve, reject) => {
this.wss = new WebSocketServer({
host: this.options?.listenHost,
port: this.options?.listenPort,
});
this.wss.on('listening', () => {
this.isStarted = true;
resolve();
});
this.wss.on('error', (err) => {
reject(err);
});
this.wss.on('connection', (ws, req) => {
const peerId = new URL(req.url, 'ws://localhost:8080').searchParams.get('id');
if (!peerId) {
ws.close();
return;
}
const peer = new RemotePeer(ws, this, peerId);
this.peers.set(peer.id, peer);
ws.send(JSON.stringify({ type: 'id', id: this.id }));
this.emitSpecial(NETRON_EVENT_PEER_CONNECT, getPeerEventName(peer.id), { peerId });
ws.on('close', () => {
this.peers.delete(peerId);
this.emitSpecial(NETRON_EVENT_PEER_DISCONNECT, getPeerEventName(peerId), { peerId });
});
peer.init(false, this.options?.abilities);
});
});
}
async stop() {
if (this.wss) {
this.wss.close();
this.wss = undefined;
}
this.isStarted = false;
}
async connect(address, abilities) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Connection timeout'));
}, this.options?.connectTimeout ?? CONNECT_TIMEOUT);
const ws = new WebSocket(`${address}?id=${this.id}`);
const peer = new RemotePeer(ws, this);
let isResolved = false;
ws.on('open', () => {
clearTimeout(timeout);
const uidHandler = (message, isBinary) => {
if (isBinary) {
return;
}
try {
const data = JSON.parse(message.toString());
if (data.type === 'id') {
peer.id = data.id;
this.peers.set(peer.id, peer);
if (!isResolved) {
isResolved = true;
peer.init(true, abilities).then(() => {
this.emitSpecial(NETRON_EVENT_PEER_CONNECT, getPeerEventName(peer.id), { peerId: peer.id });
resolve(peer);
});
}
}
else {
ws.close();
}
}
catch (error) {
console.error('Message error:', error);
ws.close();
if (!isResolved) {
isResolved = true;
reject(new Error('Message error'));
}
}
finally {
ws.removeListener('message', uidHandler);
}
};
ws.on('message', uidHandler);
});
ws.on('error', (err) => {
console.error(`Connection error to ${address}:`, err);
if (!isResolved) {
isResolved = true;
reject(err);
}
});
ws.on('close', () => {
if (!isResolved) {
console.warn('Connection closed prematurely.');
reject(new Error('Connection closed prematurely'));
}
this.peers.delete(peer.id);
this.emitSpecial(NETRON_EVENT_PEER_DISCONNECT, getPeerEventName(peer.id), { peerId: peer.id });
});
});
}
disconnect(peerId) {
const peer = this.peers.get(peerId);
if (peer) {
peer.disconnect();
this.peers.delete(peerId);
this.emitSpecial(NETRON_EVENT_PEER_DISCONNECT, getPeerEventName(peerId), { peerId });
}
}
getServiceNames() {
return [...this.services.keys()];
}
addTask(fn) {
return this.taskManager.addTask(fn);
}
async runTask(peer, name, ...args) {
return await this.taskManager.runTask(name, peer, ...args);
}
deleteSpecialEvents(id) {
this.ownEvents.delete(id);
}
async emitSpecial(event, id, data) {
const events = this.ownEvents.get(id) || [];
events.push({ name: event, data });
this.ownEvents.set(id, events);
if (events.length > 1) {
return;
}
while (events.length > 0) {
const eventData = events.shift();
if (eventData === void 0) {
break;
}
try {
const timeoutPromise = new Promise((_, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`Emit timeout for event: ${eventData.name}`));
}, 5000);
this.emitParallel(eventData.name, eventData.data)
.finally(() => clearTimeout(timeoutId))
.catch(reject);
});
await timeoutPromise;
}
catch (err) {
console.error(`Event emit error: ${err.message}`);
}
}
this.ownEvents.delete(id);
}
static async create(options) {
const netron = new Netron(options);
await netron.start();
return netron;
}
}
//# sourceMappingURL=netron.js.map