UNPKG

@iprokit/service

Version:

Powering distributed systems with simplicity and speed.

486 lines â€ĸ 18.1 kB
"use strict"; /** * @iProKit/Service * Copyright (c) 2019-2025 Rutvik Katuri / iProTechs * SPDX-License-Identifier: Apache-2.0 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServerOutgoing = exports.ServerIncoming = exports.Connection = exports.Executor = void 0; // Import Libs. const stream_1 = require("stream"); const net_1 = __importDefault(require("net")); const protocol_1 = __importStar(require("./protocol")); const coordinator_1 = require("./coordinator"); // Symbol Definitions. const connections = Symbol('Connections'); /** * `Server` binds to an IP address and port number, listening for incoming SCP client connections. * Manages registered executions to handle various SCP modes and dispatches I/Os to the appropriate execution handlers. * * @emits `incoming` when a new incoming stream is received. * @emits `clientError` when an error occurs on the client connection. */ class Server extends net_1.default.Server { /** * Unique identifier of the server. */ identifier; /** * Executions registered on the server. */ executions; /** * Client socket connections. */ [connections]; /** * Creates an instance of SCP `Server`. * * @param identifier unique identifier of the server. */ constructor(identifier) { super(); // Initialize options. this.identifier = identifier; // Initialize variables. this.executions = new Array(); this[connections] = new Array(); // Bind listeners. this.onConnection = this.onConnection.bind(this); this.onIncoming = this.onIncoming.bind(this); // Add listeners. this.addListener('connection', this.onConnection); this.addListener('incoming', this.onIncoming); // Apply `Executor` properties. đŸ‘ģ Executor.applyProperties(this); } ////////////////////////////// //////// Event Listeners ////////////////////////////// /** * @emits `incoming` when a new incoming stream is received. * @emits `clientError` when an error occurs on the client connection. */ onConnection(socket) { const connection = new Connection(socket); connection.on('incoming', (incoming, outgoing) => { this.emit('incoming', incoming, outgoing); }); connection.on('error', (error) => { this.emit('clientError', error, connection); }); connection.on('close', () => { // Find the connection and remove it. const connectionIndex = this[connections].findIndex((c) => c === connection); if (connectionIndex >= 0) this[connections].splice(connectionIndex, 1); }); this[connections].push(connection); } /** * - Subscribe is handled by `subscribe` function. * - Omni is handled by `dispatch` function. */ onIncoming(incoming, outgoing) { // Set: Outgoing. outgoing.parameters.SID = this.identifier; if (incoming.mode === 'SUBSCRIBE') { this.subscribe(incoming, outgoing); } else { // Set: Incoming. const operationRegExp = new RegExp(/^(?:(?<segment>[^.]+)\.)?(?<nexus>[^.]+)$/); const { groups } = operationRegExp.exec(incoming.operation); const { segment, nexus } = groups; incoming.segment = segment; incoming.nexus = nexus; incoming.matched = false; // Below line will blow your mind! đŸ¤¯ this.dispatch(0, this.executions, incoming, outgoing, () => { }); } } ////////////////////////////// //////// Dispatch ////////////////////////////// /** * Recursively loop through the executions to find and execute its handler. * * @param executionIndex index of the current execution being processed. * @param executions executions to be processed. * @param incoming incoming stream. * @param outgoing outgoing stream. * @param unwind function called once the processed executions unwind. */ dispatch(executionIndex, executions, incoming, outgoing, unwind) { // Need I say more. if (executionIndex >= executions.length) return unwind(); const execution = executions[executionIndex]; // Shits about to go down! 😎 if ('executions' in execution) { // Treat as `Segment`. const operationMatches = incoming.segment.match(execution.regExp); if (operationMatches) { // Segment found, Save match and process the segment. incoming.matched = true; // đŸŽĸ const unwindFunction = () => { incoming.matched = false; this.dispatch(executionIndex + 1, this.executions, incoming, outgoing, unwind); }; this.dispatch(0, execution.executions, incoming, outgoing, unwindFunction); return; } } else { // Treat as `Nexus`. const modeMatches = incoming.mode === execution.mode || 'OMNI' === execution.mode; const segmentMatches = (incoming.segment && incoming.matched) || (!incoming.segment && !incoming.matched); const operationMatches = incoming.nexus.match(execution.regExp); if (modeMatches && segmentMatches && operationMatches) { // Nexus found, execute the handler. 🎉 const proceedFunction = () => this.dispatch(executionIndex + 1, executions, incoming, outgoing, unwind); execution.handler(incoming, outgoing, proceedFunction); return; } } // Execution not found, lets keep going though the loop. this.dispatch(executionIndex + 1, executions, incoming, outgoing, unwind); } ////////////////////////////// //////// Subscribe ////////////////////////////// /** * Registers a subscription from the client socket connection. * Broadcasts are only sent to subscribed connections. */ async subscribe(incoming, outgoing) { try { // Read: Incoming stream. incoming.resume(); await stream_1.promises.finished(incoming); // Set: Connection properties. incoming.scp.identifier = incoming.parameters.CID; incoming.scp.canBroadcast = true; // Write: Outgoing stream. outgoing.end(''); await stream_1.promises.finished(outgoing); } catch (error) { // â—ī¸âš ī¸â—ī¸ incoming.destroy(); outgoing.destroy(); } } ////////////////////////////// //////// IServer ////////////////////////////// broadcast(operation, ...args) { const broadcasts = new Array(); const outgoingData = JSON.stringify(args); for (const connection of this[connections]) { if (connection.canBroadcast) { const broadcast = new Promise((resolve, reject) => { connection.createOutgoing('BROADCAST', operation, { SID: this.identifier }, async (outgoing) => { try { // Write: Outgoing stream. outgoing.end(outgoingData); await stream_1.promises.finished(outgoing); resolve(connection.identifier); } catch (error) { // â—ī¸âš ī¸â—ī¸ outgoing.destroy(); reject(error); } }); }); broadcasts.push(broadcast); } } return Promise.all(broadcasts); } attach(operation, executor) { const { executions } = executor; const regExp = new RegExp(`^${operation.replace(/\*/g, '.*')}$`); this.executions.push({ operation, regExp, executions }); return this; } ////////////////////////////// //////// Connection Management ////////////////////////////// close(callback) { super.close(callback); // 🛑 🙉 to new connections. for (const connection of this[connections]) { connection.end(); // 🔚 âšī¸ } return this; } } exports.default = Server; ////////////////////////////// //////// Executor ////////////////////////////// /** * Registers executions that handle SCP I/Os. * Once attached, SCP I/Os are dispatched to the appropriate registered executions. */ class Executor { /** * Executions registered. */ executions; /** * Creates an instance of `Executor`. */ constructor() { // Initialize variables. this.executions = new Array(); // Apply `Executor` properties. đŸ‘ģ Executor.applyProperties(this); } ////////////////////////////// //////// Apply ////////////////////////////// /** * Applies properties of `IExecutor` interface to the provided instance, * enabling registration of executions. * * @param instance instance to which the `IExecutor` properties are applied. */ static applyProperties(instance) { instance.reply = (operation, func) => this.registerNexus(instance, 'REPLY', operation, this.replyHandler(func)); instance.conductor = (operation, func) => this.registerNexus(instance, 'CONDUCTOR', operation, this.conductorHandler(func)); instance.omni = (operation, handler) => this.registerNexus(instance, 'OMNI', operation, handler); } ////////////////////////////// //////// Register ////////////////////////////// /** * Registers an individual SCP nexus for handling specific SCP mode and operation pattern. * * @param instance executor instance where the nexus will be registered. * @param mode SCP mode of the nexus. * @param operation operation pattern of the nexus. * @param handler handler function of the nexus. */ static registerNexus(instance, mode, operation, handler) { const regExp = new RegExp(`^${operation.replace(/\*/g, '.*')}$`); instance.executions.push({ mode, operation, regExp, handler }); return instance; } ////////////////////////////// //////// Handlers ////////////////////////////// /** * Creates a handler for executing reply function. * * @param func reply function to execute. */ static replyHandler(func) { return async (incoming, outgoing, proceed) => { let incomingData = new Array(); let outgoingData = ''; try { // Read: Incoming stream. for await (const chunk of incoming) { incomingData.push(chunk); } // Execute. 🤖 try { const returned = await func(...JSON.parse(Buffer.concat(incomingData).toString())); outgoingData = returned !== undefined || null ? JSON.stringify(returned) : JSON.stringify({}); outgoing.parameters.STATUS = 'OK'; } catch (error) { error instanceof Error && delete error.stack; // Delete stack from error because we don't need it. outgoingData = JSON.stringify(error, Object.getOwnPropertyNames(error)); outgoing.parameters.STATUS = 'ERROR'; } // Write: Outgoing stream. outgoing.end(outgoingData); await stream_1.promises.finished(outgoing); } catch (error) { // â—ī¸âš ī¸â—ī¸ incoming.destroy(); outgoing.destroy(); } }; } /** * Creates a handler for executing conductor function. * * @param func conductor function to execute. */ static conductorHandler(func) { return async (incoming, outgoing, proceed) => { let incomingData = new Array(); const conductor = new coordinator_1.Conductor(incoming, outgoing); // 🎩đŸšĻ🔲 try { // Read: Conductor. for await (const chunk of conductor) { incomingData.push(chunk); } // Execute. 🤖 func(conductor, ...JSON.parse(Buffer.concat(incomingData).toString())); // Write: Conductor. await conductor.flush(); // đŸšŊ💨 } catch (error) { // â—ī¸âš ī¸â—ī¸ conductor.destroy(); } }; } } exports.Executor = Executor; ////////////////////////////// //////// Connection ////////////////////////////// /** * Represents a client socket connection used by the SCP `Server`. * * @emits `incoming` when a new incoming stream is received. */ class Connection extends protocol_1.default { /** * Unique identifier of the client socket connection. */ identifier; /** * `true` if the connection can accept broadcasts, `false` otherwise. */ canBroadcast; /** * RFI + outgoing callback queue. */ #outgoingQueue; /** * Creates an instance of SCP `Connection`. * * @param socket underlying socket. */ constructor(socket) { super(socket); // Initialize variables. this.identifier = 'unknown'; this.canBroadcast = false; this.#outgoingQueue = new Array(); // Add listeners. this.addListener('end', () => this.end()); this.addListener('error', (error) => this.destroy()); // 🚴đŸŊ💨 this.cycleIO(); } ////////////////////////////// //////// Incoming/Outgoing ////////////////////////////// /** * Creates a new `Incoming` and `Outgoing` stream. * Invoked recursively on the `close` event of the current outgoing stream to continuously listen for incoming streams. * * @emits `incoming` when a new incoming stream is received. */ cycleIO() { const incoming = new ServerIncoming(this); incoming.once('rfi', () => { const outgoing = new ServerOutgoing(this); outgoing.setRFI(incoming.mode, incoming.operation); outgoing.once('close', () => this.cycleIO()); this.emit('incoming', incoming, outgoing); }); } /** * Creates a new `Outgoing` stream. * * @param mode mode of the remote function. * @param operation operation of the remote function. * @param parameters parameters of the remote function. * @param callback callback executed when the outgoing stream is ready. */ createOutgoing(mode, operation, parameters, callback) { // Push the RFI + outgoing callback into the queue. this.#outgoingQueue.push({ mode, operation, parameters, callback }); // This is the first in the queue, let's execute it! if (this.#outgoingQueue.length === 1) { this.executeOutgoing(); } return this; } /** * Executes one outgoing callback at a time in FIFO manner. * Invoked recursively on the `close` event of the current outgoing stream. */ executeOutgoing() { // The first(0th) RFI + outgoing callback from the queue. const { mode, operation, parameters, callback: firstCallback } = this.#outgoingQueue[0]; const outgoing = new ServerOutgoing(this); outgoing.setRFI(mode, operation, parameters); outgoing.once('close', () => { // Remove the first(0th) RFI + outgoing callback from the queue. this.#outgoingQueue.shift(); // 🎡 if (this.#outgoingQueue.length > 0) { this.executeOutgoing(); } }); // Let's execute the outgoing callback! firstCallback(outgoing); } } exports.Connection = Connection; ////////////////////////////// //////// Incoming/Outgoing ////////////////////////////// /** * Represents an SCP server incoming. */ class ServerIncoming extends protocol_1.Incoming { /** * Segment portion of the operation pattern. */ segment; /** * Nexus portion of the operation pattern. */ nexus; /** * `true` if the segment matched, `false` otherwise. */ matched; } exports.ServerIncoming = ServerIncoming; /** * Represents an SCP server outgoing. */ class ServerOutgoing extends protocol_1.Outgoing { } exports.ServerOutgoing = ServerOutgoing; //# sourceMappingURL=server.js.map