UNPKG

@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
"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