@iprokit/service
Version:
Powering distributed systems with simplicity and speed.
245 lines • 7.74 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 });
// Import Libs.
const events_1 = require("events");
const dgram_1 = require("dgram");
// Import Local.
const pod_1 = __importDefault(require("./pod"));
// Symbol Definitions.
const socket = Symbol('Socket');
/**
* `Server` binds to a multicast address and port number, listening for incoming SDP client connections.
* Tracks pods' availability and emits events when their states change.
*
* @emits `listening` when the server is bound after calling `listen()`.
* @emits `available` when a pod becomes available.
* @emits `unavailable` when a pod becomes unavailable.
* @emits `error` when an error occurs.
* @emits `close` when the server is fully closed.
*/
class Server extends events_1.EventEmitter {
/**
* Pods discovered.
*/
pods;
/**
* Underlying UDP Socket.
*/
[socket];
/**
* Representation of the server as a Pod.
*/
#pod;
/**
* Multicast group that have been joined.
*/
#membership;
/**
* Local port of the server.
*/
#localPort;
/**
* Local address of the server.
*/
#localAddress;
/**
* Creates an instance of SDP `Server`.
*
* @param identifier unique identifier of the server.
*/
constructor(identifier) {
super();
// Initialize variables.
this.pods = new Map();
this[socket] = (0, dgram_1.createSocket)({ type: 'udp4', reuseAddr: true });
this.#pod = new pod_1.default(identifier, Server.UNAVAILABLE_TOKEN);
this.#membership = null;
this.#localPort = null;
this.#localAddress = null;
// Bind listeners.
this.onMessage = this.onMessage.bind(this);
// Add listeners.
this[socket].addListener('message', this.onMessage);
this[socket].addListener('error', (error) => this.emit('error', error));
}
//////////////////////////////
//////// Gets/Sets
//////////////////////////////
/**
* Unique identifier of the server.
*/
get identifier() {
return this.#pod.identifier;
}
/**
* Attributes of the server.
*/
get attributes() {
return this.#pod.attributes;
}
/**
* Multicast group that have been joined.
*/
get membership() {
return this.#membership;
}
/**
* Local port of the server.
*/
get localPort() {
return this.#localPort;
}
/**
* Local address of the server.
*/
get localAddress() {
return this.#localAddress;
}
/**
* `true` when the server is listening for connections, `false` otherwise.
*/
get listening() {
return this.#pod.session !== Server.UNAVAILABLE_TOKEN;
}
/**
* Retrieves the bound address, family, and port of the server as reported by operating system.
*/
address() {
return this.listening ? this[socket].address() : null;
}
//////////////////////////////
//////// Event Listeners
//////////////////////////////
/**
* @emits `available` when a pod is available.
* @emits `unavailable` when a pod is unavailable.
*/
onMessage(buffer, remoteInfo) {
const { identifier, session, attributes } = pod_1.default.objectify(buffer.toString());
const { address: host } = remoteInfo;
if (identifier === this.identifier) {
this[socket].emit('echo', host);
return;
}
// Be ready to be confused. 😈
const foundPod = this.pods.get(identifier);
if (foundPod && session === foundPod.session)
return;
if (session !== Server.UNAVAILABLE_TOKEN) {
this.pods.set(identifier, { session, attributes, host });
this.send(() => this.emit('available', identifier, attributes, host));
}
else {
this.pods.set(identifier, { session: Server.UNAVAILABLE_TOKEN, attributes: null, host: null });
this.emit('unavailable', identifier);
}
}
//////////////////////////////
//////// Send/Echo
//////////////////////////////
/**
* Encodes and multicasts `this.#pod` on the network.
*
* @param callback called once the pod is multicast.
*/
send(callback) {
this[socket].send(this.#pod.stringify(), this.#localPort, this.#membership, (error) => callback && callback());
}
/**
* Encodes and multicasts `this.#pod` on the network, then waits for an echo.
*
* @param callback called once the echo is received.
*/
echo(callback) {
// Read
this[socket].once('echo', (address) => callback(address));
// Write
this.send();
}
//////////////////////////////
//////// Connection Management
//////////////////////////////
/**
* Starts listening for pods on the network and emits `listening` event.
*
* @param port local port.
* @param address address of the multicast group.
* @param callback optional callback added as a one-time listener for the `listening` event.
*/
listen(port, address, callback) {
callback && this.once('listening', callback);
this.#localPort = port;
this.#membership = address;
this[socket].bind(this.#localPort, () => {
this[socket].addMembership(this.#membership);
this.#pod.session = Server.createToken();
this.echo((address) => {
this.#localAddress = address;
this.emit('listening');
});
});
return this;
}
/**
* Closes the underlying UDP socket and stops listening for pods, emitting the `close` event.
*
* @param callback optional callback added as a one-time listener for the `close` event.
*/
close(callback) {
callback && this.once('close', callback);
this.#pod.session = Server.UNAVAILABLE_TOKEN;
this.echo((address) => {
this[socket].dropMembership(this.#membership);
this[socket].close(() => {
this.#membership = null;
this.#localPort = null;
this.#localAddress = null;
this.emit('close');
});
});
return this;
}
//////////////////////////////
//////// Ref/Unref
//////////////////////////////
/**
* References the socket, preventing it from closing automatically.
* Calling `ref` again has no effect if already referenced.
*/
ref() {
this[socket].ref();
return this;
}
/**
* Unreferences the socket, allowing it to close automatically when no other event loop activity is present.
* Calling `unref` again has no effect if already unreferenced.
*/
unref() {
this[socket].unref();
return this;
}
//////////////////////////////
//////// Session Helpers
//////////////////////////////
/**
* Returns a new session token for an available pod.
*/
static createToken() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
return Array.from({ length: 5 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
/**
* Session token representing an unavailable pod.
*/
static UNAVAILABLE_TOKEN = '00000';
}
exports.default = Server;
//# sourceMappingURL=server.js.map