@devgrid/netron
Version:
A powerful TypeScript library for building distributed systems with event bus, streaming capabilities, and remote object invocation. Features WebSocket-based bidirectional communication between Node.js and browser environments, service discovery, and type
284 lines • 13.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Netron = void 0;
const node_path_1 = __importDefault(require("node:path"));
const ioredis_1 = require("ioredis");
const node_crypto_1 = require("node:crypto");
const ws_1 = require("ws");
const async_emitter_1 = require("@devgrid/async-emitter");
const local_peer_1 = require("./local-peer");
const remote_peer_1 = require("./remote-peer");
const utils_1 = require("./utils");
const logger_1 = __importDefault(require("./logging/logger"));
const task_manager_1 = require("./task-manager");
const service_discovery_1 = require("./service-discovery");
const constants_1 = require("./constants");
class Netron extends async_emitter_1.AsyncEventEmitter {
constructor(options) {
super();
this.ownEvents = new Map();
this.peers = new Map();
this.isStarted = false;
this.services = new Map();
this.options = options ?? {};
this.id = options?.id ?? (0, node_crypto_1.randomUUID)();
if (options?.loggerOptions || options?.loggerDestination) {
logger_1.default.initLogger(options.loggerOptions || {}, options.loggerDestination);
}
this.logger = logger_1.default.getLogger(options?.loggerContext);
this.taskManager = new task_manager_1.TaskManager({
timeout: options?.taskTimeout,
overwriteStrategy: options?.taskOverwriteStrategy,
});
this.peer = new local_peer_1.LocalPeer(this);
}
async start() {
if (this.isStarted) {
this.logger.warn('Netron instance already started');
throw new Error('Netron already started');
}
this.logger.info('Starting Netron instance');
await this.taskManager.loadTasksFromDir(node_path_1.default.join(__dirname, 'core-tasks'));
if (!this.options?.listenHost || !this.options?.listenPort) {
this.logger.info('Netron started in client-only mode');
if (this.options.discoveryEnabled && this.options.discoveryRedisUrl) {
this.logger.info('Initializing service discovery in client mode');
await this.initServiceDiscovery(true);
}
this.isStarted = true;
return Promise.resolve();
}
return new Promise((resolve, reject) => {
this.wss = new ws_1.WebSocketServer({
host: this.options?.listenHost,
port: this.options?.listenPort,
});
this.wss.on('listening', async () => {
this.logger.info(`Netron server started at ${this.options.listenHost}:${this.options.listenPort}`);
this.isStarted = true;
if (this.options.discoveryEnabled && this.options.discoveryRedisUrl) {
await this.initServiceDiscovery(false);
}
resolve();
});
this.wss.on('error', (err) => {
this.logger.error('WebSocket server error', { error: err });
reject(err);
});
this.wss.on('connection', (ws, req) => {
const peerId = new URL(req.url, 'ws://localhost').searchParams.get('id');
if (!peerId) {
this.logger.warn('Connection attempt without peer ID, closing');
ws.close();
return;
}
this.logger.info('New peer connection', { peerId });
const peer = new remote_peer_1.RemotePeer(ws, this, peerId);
this.peers.set(peer.id, peer);
ws.send(JSON.stringify({ type: 'id', id: this.id }));
this.emitSpecial(constants_1.NETRON_EVENT_PEER_CONNECT, (0, utils_1.getPeerEventName)(peer.id), { peerId });
ws.on('close', () => {
this.logger.info('Peer disconnected', { peerId });
this.peers.delete(peerId);
this.emitSpecial(constants_1.NETRON_EVENT_PEER_DISCONNECT, (0, utils_1.getPeerEventName)(peerId), { peerId });
});
peer.init(false, this.options);
});
});
}
async initServiceDiscovery(clientMode) {
this.logger.info('Initializing service discovery');
this.discoveryRedis = new ioredis_1.Redis(this.options.discoveryRedisUrl);
this.discovery = new service_discovery_1.ServiceDiscovery(this.discoveryRedis, this, clientMode ? '' : `${this.options.listenHost}:${this.options.listenPort}`, clientMode ? [] : this.getExposedServices(), {
heartbeatInterval: this.options.discoveryHeartbeatInterval,
heartbeatTTL: this.options.discoveryHeartbeatTTL,
pubSubEnabled: this.options.discoveryPubSubEnabled ?? true,
clientMode,
});
this.discovery.startHeartbeat();
await this.discovery.subscribeToEvents((event) => {
this.logger.debug('Service discovery event received', { event });
this.emit('discovery:event', event);
});
this.logger.info(`Service discovery initialized successfully (${clientMode ? 'client mode' : 'server mode'})`);
}
async stop() {
this.logger.info('Stopping Netron instance');
if (this.wss) {
this.logger.info('Closing WebSocket server');
this.wss.close();
this.wss = undefined;
}
if (this.discovery) {
this.logger.info('Shutting down service discovery');
await this.discovery.shutdown();
await this.discoveryRedis.quit();
this.discovery = undefined;
this.discoveryRedis = undefined;
}
this.isStarted = false;
this.logger.info('Netron instance stopped');
}
async connect(address, reconnect = true) {
this.logger.info('Connecting to remote peer', { address, reconnect });
const baseDelay = 1000;
let reconnectAttempts = 0;
let manuallyDisconnected = false;
const connectPeer = () => new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.logger.error('Connection timeout', { address });
reject(new Error('Connection timeout'));
}, this.options?.connectTimeout ?? constants_1.CONNECT_TIMEOUT);
const ws = new ws_1.WebSocket(`${address}?id=${this.id}`);
const peer = new remote_peer_1.RemotePeer(ws, this, address);
let resolved = false;
ws.once('open', () => {
this.logger.debug('WebSocket connection established', { address });
clearTimeout(timeoutId);
ws.once('message', async (message, isBinary) => {
if (!isBinary) {
try {
const data = JSON.parse(message.toString());
if (data.type === 'id') {
peer.id = data.id;
this.peers.set(peer.id, peer);
await peer.init(true, this.options);
peer.once('manual-disconnect', () => {
this.logger.info('Manual disconnect requested', { peerId: peer.id });
manuallyDisconnected = true;
});
ws.once('close', () => {
this.logger.info('WebSocket connection closed', { peerId: peer.id });
this.peers.delete(peer.id);
this.emitSpecial(constants_1.NETRON_EVENT_PEER_DISCONNECT, (0, utils_1.getPeerEventName)(peer.id), { peerId: peer.id });
if (reconnect && !manuallyDisconnected) {
attemptReconnect();
}
});
resolved = true;
reconnectAttempts = 0;
this.emitSpecial(constants_1.NETRON_EVENT_PEER_CONNECT, (0, utils_1.getPeerEventName)(peer.id), { peerId: peer.id });
this.logger.info('Peer connection established', { peerId: peer.id });
resolve(peer);
}
else {
this.logger.warn('Invalid handshake message type', { type: data.type });
ws.close();
reject(new Error('Invalid handshake'));
}
}
catch (error) {
this.logger.error('Error parsing handshake message', { error });
ws.close();
reject(error);
}
}
else {
this.logger.warn('Received binary handshake message');
ws.close();
reject(new Error('Invalid handshake'));
}
});
});
ws.on('error', (err) => {
this.logger.error('WebSocket connection error', { error: err });
clearTimeout(timeoutId);
if (!resolved) {
reject(err);
}
});
ws.on('close', () => {
this.logger.warn('WebSocket connection closed prematurely', { address });
clearTimeout(timeoutId);
if (!resolved) {
reject(new Error('Connection closed prematurely'));
}
});
});
const attemptReconnect = () => {
if (this.options.maxReconnectAttempts && reconnectAttempts >= this.options.maxReconnectAttempts) {
this.logger.error(`Reconnect attempts exceeded (${this.options.maxReconnectAttempts}). Giving up.`);
return;
}
const delay = Math.min(baseDelay * 2 ** reconnectAttempts, 30000);
this.logger.info(`Reconnecting to ${address} in ${delay} ms (attempt ${reconnectAttempts + 1}/${this.options.maxReconnectAttempts ?? 'unlimited'})...`);
setTimeout(async () => {
reconnectAttempts++;
try {
await connectPeer();
this.logger.info(`Successfully reconnected to ${address}.`);
}
catch (err) {
this.logger.warn(`Reconnect failed (${reconnectAttempts}/${this.options.maxReconnectAttempts ?? 'unlimited'}):`);
attemptReconnect();
}
}, delay);
};
return connectPeer();
}
disconnect(peerId) {
const peer = this.peers.get(peerId);
if (peer) {
peer.disconnect();
this.peers.delete(peerId);
this.emitSpecial(constants_1.NETRON_EVENT_PEER_DISCONNECT, (0, utils_1.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) {
this.logger.error(`Event emit error: ${err.message}`);
}
}
this.ownEvents.delete(id);
}
static async create(options) {
const netron = new Netron(options);
await netron.start();
return netron;
}
getExposedServices() {
return Array.from(this.services.values()).map(stub => ({
name: stub.definition.meta.name,
version: stub.definition.meta.version,
}));
}
}
exports.Netron = Netron;
//# sourceMappingURL=netron.js.map