@iprokit/service
Version:
Powering distributed systems with simplicity and speed.
652 lines • 19.9 kB
JavaScript
"use strict";
/**
* @iProKit/Service
* Copyright (c) 2019-2025 Rutvik Katuri / iProTechs
* SPDX-License-Identifier: Apache-2.0
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Outgoing = exports.Incoming = void 0;
// Import Libs.
const stream_1 = require("stream");
// Import Local.
const frame_1 = __importDefault(require("./frame"));
const rfi_1 = __importDefault(require("./rfi"));
const signal_1 = __importDefault(require("./signal"));
// Symbol Definitions.
const readingPaused = Symbol('ReadingPaused');
const rifSent = Symbol('RFISent');
/**
* `Protocol` provides a high-level abstraction for the Service Communication Protocol (SCP).
* It manages the encoding and decoding of SCP frames over a duplex stream by encapsulating a user-provided socket.
*/
class Protocol extends stream_1.Duplex {
/**
* Underlying socket.
*/
socket;
/**
* `true` when read buffer is full and calls to `push` return `false`.
* Additionally new frames will not be read off the underlying socket until the consumer calls `read`.
*/
[readingPaused];
/**
* Creates an instance of `Protocol`.
*
* @param socket underlying socket.
*/
constructor(socket) {
super({ objectMode: true });
// Initialize options.
this.socket = socket;
// Initialize variables.
this[readingPaused] = false;
// Bind listeners.
this.onReadable = this.onReadable.bind(this);
this.onEnd = this.onEnd.bind(this);
// Add listeners.
this.socket.addListener('readable', this.onReadable);
this.socket.addListener('end', this.onEnd);
this.socket.addListener('error', (error) => this.emit('error', error));
}
//////////////////////////////
//////// Duplex
//////////////////////////////
/**
* Implements the readable stream method `_read`.
*
* WARNING: Should not be called by the consumer.
*/
_read() {
this[readingPaused] = false;
// Force trigger `onReadable` to read frame from the underlying socket.
setImmediate(this.onReadable);
}
/**
* Implements the writable stream method `_write`.
*
* WARNING: Should not be called by the consumer.
*/
_write(frame, encoding, callback) {
// Ohoooo, Frame is too large.
if (frame.length > frame_1.default.FRAME_BYTES) {
callback(new Error('FRAME_TOO_LARGE'));
return;
}
// Write HEAD Segments.
const head = Buffer.allocUnsafe(frame_1.default.HEAD_BYTES);
head.writeUInt16BE(frame.length, 0);
head.writeInt8(frame.type, frame_1.default.LENGTH_BYTES);
// Write TAIL Segments.
const tail = frame.payload ?? Buffer.alloc(0);
// Create a new frame buffer.
const frameBuffer = Buffer.concat([head, tail]);
/**
* Used to cache the error and return when `callback` is ready.
*/
let errorCache;
// Write the frame buffer into the underlying socket.
const write = this.socket.write(frameBuffer, (error) => (errorCache = error));
if (write) {
// Good to go, send the callback.
callback(errorCache);
}
else {
// Boy! This stream has too many back issues. It needs to see a doctor. HA.HA.HA...
this.socket.once('drain', () => callback(errorCache));
}
}
/**
* Implements the writable stream method `_final`.
* Used when `end()` is called to end the writable stream.
*
* WARNING: Should not be called by the consumer.
*/
_final(callback) {
this.socket.end(callback);
}
/**
* Implements the readable/writable stream method `_destroy`.
* Used when `destroy()` is called to destroy the stream.
*
* WARNING: Should not be called by the consumer.
*/
_destroy(error, callback) {
this.socket.destroy();
callback(error);
}
//////////////////////////////
//////// Read Operations
//////////////////////////////
/**
* Fired when `readable` event is triggered on the underlying socket.
* This is called every time there is a new frame to be read from the underlying socket.
*/
onReadable() {
while (!this[readingPaused]) {
const head = this.socket.read(frame_1.default.HEAD_BYTES);
// Looks like the HEAD is unavailable. oops!!!
if (!head)
return;
// Read HEAD Segments.
const length = head.readUInt16BE(0);
const type = head.readInt8(frame_1.default.LENGTH_BYTES);
// Read TAIL Segments.
let payload;
const payloadLength = length - frame_1.default.HEAD_BYTES;
if (payloadLength > 0) {
if (this.socket.readableLength < payloadLength) {
// Put the HEAD back into the buffer to read in the next pass since TAIL is not available on the wire.
this.socket.unshift(head);
return;
}
payload = this.socket.read(payloadLength);
}
// Create a new instance of the frame.
const frame = new frame_1.default(type, payload);
// Signal frame to the `data` event.
const push = this.push(frame);
// Encountered backpressure, reading will be paused.
if (!push) {
this[readingPaused] = true;
return;
}
}
}
/**
* Fired when end is received on the underlying socket.
* Ends the readable stream.
*/
onEnd() {
// Signal `end` event.
this.push(null);
}
//////////////////////////////
//////// Heartbeat
//////////////////////////////
/**
* Sends a heartbeat signal to keep the connection alive.
*
* @param callback callback called after the signal is sent.
*/
heartbeat(callback) {
let errorCache;
// Writes empty string into the underlying socket, acts as a heartbeat.
const write = this.socket.write('', (error) => (errorCache = error));
if (write) {
callback(errorCache);
}
else {
// Encountered backpressure. When the pressure is released send the callback.
this.socket.once('drain', () => callback(errorCache));
}
return this;
}
}
exports.default = Protocol;
//////////////////////////////
//////// Incoming
//////////////////////////////
/**
* `Readable` stream that decodes SCP frames into data.
*
* @emits `rfi` when RFI is received.
*/
class Incoming extends stream_1.Readable {
/**
* Underlying SCP stream.
*/
scp;
/**
* RFI received on the stream.
*/
#rfi;
/**
* String encoding to apply when decoding `DATA` frames.
* If not set, raw Buffers will be returned.
*/
#encoding;
/**
* `true` when read buffer is full and calls to `push` return `false`.
* Additionally new frame will not be read off underlying SCP stream until the consumer calls `read`.
*/
[readingPaused];
/**
* Creates an instance of `Incoming`.
*
* @param scp underlying SCP stream.
*/
constructor(scp) {
super({ objectMode: true });
// Initialize options.
this.scp = scp;
// Initialize variables.
this[readingPaused] = false;
// Bind listeners.
this.onReadable = this.onReadable.bind(this);
this.onEnd = this.onEnd.bind(this);
// Add listeners.
this.scp.addListener('readable', this.onReadable);
this.scp.addListener('end', this.onEnd);
}
//////////////////////////////
//////// Gets/Sets
//////////////////////////////
/**
* RFI received on the stream.
*/
get rfi() {
return this.#rfi;
}
get mode() {
return this.#rfi.mode;
}
get operation() {
return this.#rfi.operation;
}
get parameters() {
return this.#rfi.parameters;
}
/**
* Gets a parameter value.
*
* @param key parameter key.
*/
get(key) {
return this.#rfi.get(key);
}
/**
* Returns `true` if the parameter exists, `false` otherwise.
*
* @param key parameter key.
*/
has(key) {
return this.#rfi.has(key);
}
/**
* Returns an array of parameter keys.
*/
keys() {
return this.#rfi.keys();
}
/**
* Returns an array of parameter values.
*/
values() {
return this.#rfi.values();
}
/**
* Returns an array of key-value pairs of parameters.
*/
entries() {
return this.#rfi.entries();
}
/**
* Returns the number of parameters.
*/
get size() {
return this.#rfi.size;
}
/**
* Sets the string encoding to apply when decoding `DATA` frames.
* If not set, raw Buffers will be returned.
*
* Note: This does not affect `SIGNAL` frames, which will continue to be emitted as `Signal` objects.
*
* @param encoding string encoding to apply.
*/
setEncoding(encoding) {
this.#encoding = encoding;
return this;
}
//////////////////////////////
//////// Readable
//////////////////////////////
/**
* Implements the readable stream method `_read`.
*
* WARNING: Should not be called by the consumer.
*/
_read() {
this[readingPaused] = false;
// Force trigger `onReadable` to read frame from the underlying SCP stream.
setImmediate(this.onReadable);
}
/**
* Implements the readable stream method `_destroy`.
* Used when `destroy()` is called to destroy the stream.
*
* WARNING: Should not be called by the consumer.
*/
_destroy(error, callback) {
this.scp.removeListener('readable', this.onReadable);
this.scp.removeListener('end', this.onEnd);
callback(error);
}
//////////////////////////////
//////// Read Operations
//////////////////////////////
/**
* Fired when `readable` event is triggered on the underlying SCP stream.
* This is called every time there is a new frame to be read from the underlying SCP stream.
*/
onReadable() {
while (!this[readingPaused]) {
const frame = this.scp.read();
// Looks like the frame is not ready yet, it's probably finding itself.
if (!frame)
return;
// This is self explanatory. REALLY!!!
if (!this.#rfi) {
if (frame.type === frame_1.default.RFI) {
this.#rfi = rfi_1.default.objectify(frame.payload.toString());
this.emit('rfi');
return;
}
continue;
}
if (frame.type === frame_1.default.END) {
// Signal `end` event.
this.push(null);
return;
}
// Let's see what we've got?
let chunk;
if (frame.type === frame_1.default.DATA) {
if (this.#encoding) {
chunk = frame.payload?.toString(this.#encoding) ?? '';
}
else {
chunk = frame.payload ?? Buffer.alloc(0);
}
}
else if (frame.type === frame_1.default.SIGNAL) {
chunk = signal_1.default.objectify(frame.payload.toString());
}
// Signal `data` event.
const push = this.push(chunk);
// Slow down! Stream needs a break.
if (!push) {
this[readingPaused] = true;
return;
}
}
}
/**
* Fired when end is received on the underlying SCP stream.
* Ends the readable stream.
*/
onEnd() {
// Signal `end` event.
this.push(null);
}
}
exports.Incoming = Incoming;
//////////////////////////////
//////// Outgoing
//////////////////////////////
/**
* `Writable` stream that encodes stream data into SCP frames.
*/
class Outgoing extends stream_1.Writable {
/**
* Underlying SCP stream.
*/
scp;
/**
* RFI to send on the stream.
*/
#rfi;
/**
* `true` if the RFI has been sent, `false` otherwise.
*/
[rifSent];
/**
* Creates an instance of `Outgoing`.
*
* @param scp underlying SCP stream.
*/
constructor(scp) {
super({ objectMode: true });
// Initialize options.
this.scp = scp;
// Initialize variables.
this[rifSent] = false;
}
//////////////////////////////
//////// Gets/Sets
//////////////////////////////
/**
* RFI to send on the stream.
*/
get rfi() {
return this.#rfi;
}
get mode() {
return this.#rfi.mode;
}
get operation() {
return this.#rfi.operation;
}
get parameters() {
return this.#rfi.parameters;
}
/**
* Sets RFI to send on the stream.
*
* @param mode mode of the remote function.
* @param operation operation of the remote function.
* @param parameters optional parameters of the remote function.
*/
setRFI(mode, operation, parameters) {
this.#rfi = new rfi_1.default(mode, operation, parameters);
return this;
}
/**
* Gets a parameter value.
*
* @param key parameter key.
*/
get(key) {
return this.#rfi.get(key);
}
/**
* Returns `true` if the parameter exists, `false` otherwise.
*
* @param key parameter key.
*/
has(key) {
return this.#rfi.has(key);
}
/**
* Sets a parameter.
*
* @param key parameter key.
* @param value parameter value.
*/
set(key, value) {
this.#rfi.set(key, value);
return this;
}
/**
* Removes a parameter.
*
* @param key parameter key.
*/
delete(key) {
this.#rfi.delete(key);
return this;
}
/**
* Returns an array of parameter keys.
*/
keys() {
return this.#rfi.keys();
}
/**
* Returns an array of parameter values.
*/
values() {
return this.#rfi.values();
}
/**
* Returns an array of key-value pairs of parameters.
*/
entries() {
return this.#rfi.entries();
}
/**
* Returns the number of parameters.
*/
get size() {
return this.#rfi.size;
}
//////////////////////////////
//////// Writable
//////////////////////////////
/**
* Implements the writable stream method `_write`.
* Writes RFI frame on the first pass and payload frames subsequently.
*
* WARNING: Should not be called by the consumer.
*/
_write(chunk, encoding, callback) {
if (!this[rifSent]) {
if (!this.#rfi) {
callback(new Error('RFI_NOT_SET'));
return;
}
this.writeRFI(this.#rfi, (error) => {
if (error) {
callback(error);
return;
}
this[rifSent] = true;
this.writePayload(chunk, encoding, callback);
});
}
else {
this.writePayload(chunk, encoding, callback);
}
}
/**
* Implements the writable stream method `_final`.
* If RFI is sent, writes end frame then signal end.
* Otherwise signal end directly.
*
* WARNING: Should not be called by the consumer.
*/
_final(callback) {
if (this[rifSent]) {
this.writeEnd(callback);
}
else {
callback();
}
}
//////////////////////////////
//////// Write Operations
//////////////////////////////
/**
* Writes RFI into the underlying SCP stream.
*
* @param rfi RFI to write.
* @param callback callback called when the write operation is complete.
*/
writeRFI(rfi, callback) {
const rfiFrame = new frame_1.default(frame_1.default.RFI, Buffer.from(rfi.stringify()));
this.writeFrame(rfiFrame, callback);
}
/**
* Writes payload(data/signal) into the underlying SCP stream.
*
* @param payload payload to write.
* @param encoding string encoding to apply when encoding strings into Buffers. If not set, `utf8` will be used by default.
* @param callback callback called when the write operation is complete.
*/
writePayload(payload, encoding, callback) {
if (typeof payload === 'string') {
const payloadBuffer = Buffer.from(payload, encoding ?? 'utf8');
this.writeData(payloadBuffer, 0, frame_1.default.PAYLOAD_BYTES, callback);
}
else if (payload instanceof Buffer) {
this.writeData(payload, 0, frame_1.default.PAYLOAD_BYTES, callback);
}
else if (payload instanceof signal_1.default) {
this.writeSignal(payload, callback);
}
}
/**
* Writes data into the underlying SCP stream.
*
* @param data data to write.
* @param startHead start head.
* @param endHead end head.
* @param callback callback called when the write operation is complete.
*/
writeData(data, startHead, endHead, callback) {
const dataFrame = new frame_1.default(frame_1.default.DATA, data.subarray(startHead, endHead));
this.writeFrame(dataFrame, (error) => {
if (error) {
callback(error);
return;
}
if (endHead < data.length) {
// Update head for the next write.
startHead = endHead;
endHead += frame_1.default.PAYLOAD_BYTES;
this.writeData(data, startHead, endHead, callback);
}
else {
callback();
}
});
}
/**
* Writes signal into the underlying SCP stream.
*
* @param signal signal to write.
* @param callback callback called when the write operation is complete.
*/
writeSignal(signal, callback) {
const signalFrame = new frame_1.default(frame_1.default.SIGNAL, Buffer.from(signal.stringify()));
this.writeFrame(signalFrame, callback);
}
/**
* Writes end into the underlying SCP stream.
*
* @param callback callback called when the write operation is complete.
*/
writeEnd(callback) {
const endFrame = new frame_1.default(frame_1.default.END);
this.writeFrame(endFrame, callback);
}
//////////////////////////////
//////// Write Frame
//////////////////////////////
/**
* Writes frame into the underlying SCP stream.
*
* @param frame frame to write.
* @param callback callback called when the write operation is complete.
*/
writeFrame(frame, callback) {
/**
* Cache the error and return when `callback` is ready.
*/
let errorCache;
// Write the frame into the underlying SCP stream.
const write = this.scp.write(frame, (error) => (errorCache = error));
if (write) {
callback(errorCache);
}
else {
// Encountered some backpressure, waiting for chiropractor to release the pressure.
this.scp.once('drain', () => callback(errorCache));
}
}
}
exports.Outgoing = Outgoing;
//# sourceMappingURL=protocol.js.map