@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
410 lines • 16 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RemotePeer = void 0;
const ws_1 = require("ws");
const events_1 = require("events");
const common_1 = require("@devgrid/common");
const utils_1 = require("./utils");
const service_stub_1 = require("./service-stub");
const abstract_peer_1 = require("./abstract-peer");
const stream_reference_1 = require("./stream-reference");
const readable_stream_1 = require("./readable-stream");
const predicates_1 = require("./predicates");
const constants_1 = require("./constants");
const packet_1 = require("./packet");
class RemotePeer extends abstract_peer_1.AbstractPeer {
constructor(socket, netron, id = '') {
super(netron, id);
this.socket = socket;
this.events = new events_1.EventEmitter();
this.responseHandlers = new common_1.TimedMap(this.netron.options?.requestTimeout ?? constants_1.REQUEST_TIMEOUT, (packetId) => {
const handlers = this.deleteResponseHandler(packetId);
if (handlers?.errorHandler) {
handlers.errorHandler(new Error('Request timeout exceeded'));
}
});
this.writableStreams = new Map();
this.readableStreams = new Map();
this.eventSubscribers = new Map();
this.remoteSubscriptions = new Map();
this.services = new Map();
this.definitions = new Map();
this.logger = netron.logger.child({ peerId: this.id, remotePeer: true });
}
async init(isConnector, options) {
this.logger.info('Initializing remote peer', { isConnector });
this.socket.on('message', (data, isBinary) => {
if (isBinary) {
try {
this.handlePacket((0, packet_1.decodePacket)(data));
}
catch (error) {
this.logger.error('Packet decode error:', error);
}
}
else {
this.logger.warn('Received non-binary message:', data);
}
});
if (isConnector) {
this.logger.info('Initializing as connector');
this.abilities = (await this.runTask('abilities', this.netron.peer.abilities));
if (this.abilities.services) {
this.logger.info('Registering remote services', { count: this.abilities.services.size });
for (const [name, definition] of this.abilities.services) {
this.definitions.set(definition.id, definition);
this.services.set(name, definition);
}
}
if (this.abilities.allowServiceEvents) {
this.logger.info('Subscribing to service lifecycle events');
await this.subscribe(constants_1.NETRON_EVENT_SERVICE_EXPOSE, (event) => {
this.logger.info('Service exposed event received', { event });
this.definitions.set(event.definition.id, event.definition);
this.services.set(event.qualifiedName, event.definition);
});
await this.subscribe(constants_1.NETRON_EVENT_SERVICE_UNEXPOSE, (event) => {
this.logger.info('Service unexposed event received', { event });
this.definitions.delete(event.defId);
this.services.delete(event.qualifiedName);
});
}
}
}
async exposeService(instance) {
const meta = Reflect.getMetadata(constants_1.SERVICE_ANNOTATION, instance.constructor);
if (!meta) {
throw new Error('Invalid service');
}
if (this.services.has(meta.name)) {
throw new Error(`Service already exposed: ${meta.name}`);
}
const def = await this.runTask('expose_service', meta);
const stub = new service_stub_1.ServiceStub(this.netron.peer, instance, meta);
this.netron.peer.stubs.set(def.id, stub);
this.netron.peer.serviceInstances.set(instance, stub);
return def;
}
async unexposeService(serviceName) {
const defId = await this.runTask('unexpose_service', serviceName);
for (const i of this.interfaces.values()) {
if (i.instance.$def?.parentId === defId) {
this.releaseInterface(i.instance);
}
}
const stub = this.netron.peer.stubs.get(defId);
if (stub) {
this.netron.peer.serviceInstances.delete(stub.instance);
this.netron.peer.stubs.delete(defId);
}
}
async subscribe(eventName, handler) {
const handlers = this.eventSubscribers.get(eventName);
if (!handlers) {
this.eventSubscribers.set(eventName, [handler]);
await this.runTask('subscribe', eventName);
}
else if (!handlers.includes(handler)) {
handlers.push(handler);
}
}
async unsubscribe(eventName, handler) {
const handlers = this.eventSubscribers.get(eventName);
if (handlers) {
const index = handlers.indexOf(handler);
if (index >= 0) {
handlers.splice(index, 1);
if (handlers.length === 0) {
this.eventSubscribers.delete(eventName);
await this.runTask('unsubscribe', eventName);
}
}
}
}
getServiceNames() {
return [...this.services.keys()];
}
get(defId, name) {
const def = this.definitions.get(defId);
if (!def) {
throw new Error(`Unknown definition: ${defId}`);
}
return new Promise((resolve, reject) => {
this.sendRequest(packet_1.TYPE_GET, [defId, name], (result) => {
resolve(this.processResult(def, result));
}, reject).catch(reject);
});
}
set(defId, name, value) {
const def = this.definitions.get(defId);
if (!def) {
throw new Error(`Unknown definition: ${defId}`);
}
return new Promise((resolve, reject) => {
this.sendRequest(packet_1.TYPE_SET, [defId, name, value], () => {
resolve();
}, reject).catch(reject);
});
}
call(defId, method, args) {
const def = this.definitions.get(defId);
if (!def) {
throw new Error(`Unknown definition: ${defId}`);
}
args = this.processArgs(def, args);
return new Promise((resolve, reject) => {
this.sendRequest(packet_1.TYPE_CALL, [defId, method, ...args], (result) => {
resolve(this.processResult(def, result));
}, reject).catch(reject);
});
}
disconnect() {
this.logger.info('Disconnecting remote peer');
this.events.emit('manual-disconnect');
if (this.socket.readyState === ws_1.WebSocket.OPEN || this.socket.readyState === ws_1.WebSocket.CONNECTING) {
this.socket.close();
}
else {
this.logger.warn(`Attempt to close WebSocket in unexpected state: ${this.socket.readyState}`);
}
this.cleanup();
}
once(event, listener) {
this.events.once(event, listener);
}
cleanup() {
this.responseHandlers.clear();
this.writableStreams.clear();
this.readableStreams.clear();
this.eventSubscribers.clear();
this.remoteSubscriptions.clear();
this.services.clear();
this.definitions.clear();
}
runTask(name, ...args) {
return new Promise((resolve, reject) => {
this.sendRequest(packet_1.TYPE_TASK, [name, ...args], (result) => {
resolve(result);
}, reject).catch(reject);
});
}
sendRequest(type, data, successHandler, errorHandler) {
const packet = (0, packet_1.createPacket)(packet_1.Packet.nextId(), 1, type, data);
this.responseHandlers.set(packet.id, {
successHandler,
errorHandler,
});
return this.sendPacket(packet);
}
sendResponse(packet, data) {
packet.setImpulse(0);
packet.data = data;
return this.sendPacket(packet);
}
sendErrorResponse(packet, error) {
packet.setImpulse(0);
packet.setError(1);
packet.data = error;
return this.sendPacket(packet);
}
sendPacket(packet) {
return new Promise((resolve, reject) => {
if (this.socket.readyState === ws_1.WebSocket.OPEN) {
this.socket.send((0, packet_1.encodePacket)(packet), { binary: true }, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
}
else {
reject(new Error('Socket closed'));
}
});
}
sendStreamChunk(streamId, chunk, index, isLast, isLive) {
return this.sendPacket((0, packet_1.createStreamPacket)(packet_1.Packet.nextId(), streamId, index, isLast, isLive, chunk));
}
handleResponse(packet) {
const id = packet.id;
const handlers = this.deleteResponseHandler(id);
if (handlers) {
const data = packet.data;
if (packet.getError() === 0) {
handlers.successHandler(data);
}
else {
handlers.errorHandler?.(data);
}
}
}
async handlePacket(packet) {
this.logger.debug('Handling packet', { type: packet.getType() });
const pType = packet.getType();
if (packet.getImpulse() === 0) {
this.handleResponse(packet);
return;
}
switch (pType) {
case packet_1.TYPE_SET: {
const [defId, name, value] = packet.data;
this.logger.debug('Processing SET packet', { defId, name });
try {
const stub = this.netron.peer.getStubByDefinitionId(defId);
await stub.set(name, value);
await this.sendResponse(packet, undefined);
}
catch (err) {
this.logger.error('Error setting value:', err);
try {
await this.sendErrorResponse(packet, err);
}
catch (err_) {
this.logger.error('Error sending error response:', err_);
}
}
break;
}
case packet_1.TYPE_GET: {
const [defId, name] = packet.data;
this.logger.debug('Processing GET packet', { defId, name });
try {
const stub = this.netron.peer.getStubByDefinitionId(defId);
await this.sendResponse(packet, await stub.get(name));
}
catch (err) {
this.logger.error('Error getting value:', err);
try {
await this.sendErrorResponse(packet, err);
}
catch (err_) {
this.logger.error('Error sending error response:', err_);
}
}
break;
}
case packet_1.TYPE_CALL: {
const [defId, method, ...args] = packet.data;
this.logger.debug('Processing CALL packet', { defId, method });
try {
const stub = this.netron.peer.getStubByDefinitionId(defId);
await this.sendResponse(packet, await stub.call(method, args));
}
catch (err) {
this.logger.error('Error calling method:', err);
try {
await this.sendErrorResponse(packet, err);
}
catch (err_) {
this.logger.error('Error sending error response:', err_);
}
}
break;
}
case packet_1.TYPE_TASK: {
const [name, ...args] = packet.data;
this.logger.debug('Processing TASK packet', { name });
try {
await this.sendResponse(packet, await this.netron.runTask(this, name, ...args));
}
catch (err) {
this.logger.error('Error running task:', err);
try {
await this.sendErrorResponse(packet, err);
}
catch (err_) {
this.logger.error('Error sending error response:', err_);
}
}
break;
}
case packet_1.TYPE_STREAM: {
if (!packet.streamId) {
this.logger.warn('Received STREAM packet without streamId');
return;
}
let stream = this.readableStreams.get(packet.streamId);
if (!stream) {
this.logger.debug('Creating new readable stream', { streamId: packet.streamId });
stream = readable_stream_1.NetronReadableStream.create(this, packet.streamId, packet.isLive());
this.events.emit('stream', stream);
}
stream.onPacket(packet);
break;
}
case packet_1.TYPE_STREAM_ERROR: {
const { streamId, message } = packet.data;
this.logger.error('Stream error received', { streamId, message });
const stream = this.readableStreams.get(streamId);
if (stream) {
stream.destroy(new Error(message));
}
break;
}
default: {
this.logger.warn('Unknown packet type:', pType);
}
}
}
async releaseInterfaceInternal(iInstance) {
await this.runTask('unref_service', iInstance.$def?.id);
this.unrefService(iInstance.$def?.id);
}
refService(def, parentDef) {
const existingDef = this.definitions.get(def.id);
if (existingDef) {
return existingDef;
}
def.parentId = parentDef.id;
this.definitions.set(def.id, def);
return def;
}
unrefService(defId) {
if (defId) {
const def = this.definitions.get(defId);
if (def) {
if (!this.services.has((0, utils_1.getQualifiedName)(def.meta.name, def.meta.version))) {
this.definitions.delete(defId);
}
}
}
}
processResult(parentDef, result) {
if ((0, predicates_1.isServiceDefinition)(result)) {
const def = this.refService(result, parentDef);
return this.queryInterfaceByDefId(def.id, def);
}
else if ((0, predicates_1.isNetronStreamReference)(result)) {
return stream_reference_1.StreamReference.to(result, this);
}
return result;
}
processArgs(ctxDef, args) {
return args;
}
deleteResponseHandler(packetId) {
const handlers = this.responseHandlers.get(packetId);
if (handlers) {
this.responseHandlers.delete(packetId);
}
return handlers;
}
getDefinitionById(defId) {
const def = this.definitions.get(defId);
if (!def) {
throw new Error(`Unknown definition: ${defId}.`);
}
return def;
}
getDefinitionByServiceName(name) {
const def = this.services.get(name);
if (def === void 0) {
throw new Error(`Unknown service: ${name}.`);
}
return def;
}
}
exports.RemotePeer = RemotePeer;
//# sourceMappingURL=remote-peer.js.map