@iprokit/service
Version:
Powering distributed systems with simplicity and speed.
486 lines âĸ 18.1 kB
JavaScript
"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