UNPKG

@arifwidianto/rpc-agent

Version:

RPC Agent for both client and server, extends more methods easily

463 lines 21.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RPCService = void 0; const node_net_1 = require("node:net"); const node_dgram_1 = require("node:dgram"); const interfaces_1 = require("../interfaces"); const logger_1 = require("../utils/logger"); const extension_loader_1 = require("../utils/extension-loader"); const error_service_1 = require("./error.service"); class RPCService { extensionPath; tcpPort; udpPort; host; tcpServer = null; udpServer = null; extensions = new Map(); initialized = false; constructor(extensionPath, tcpPort = 9101, udpPort = 9102, host = "127.0.0.1") { this.extensionPath = extensionPath; this.tcpPort = tcpPort; this.udpPort = udpPort; this.host = host; } async initialize() { if (this.initialized) { return { tcp: !!this.tcpServer, udp: !!this.udpServer }; } try { await this.loadExtensions(); const [tcp, udp] = await Promise.all([this.initializeTCPServer(), this.initializeUDPServer()]); this.initialized = true; return { tcp, udp }; } catch (error) { logger_1.logger.error("Failed to initialize RPC service", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); await this.shutdown(); throw error; } } async loadExtensions() { try { const extensionsMap = await (0, extension_loader_1.loadExtensions)(this.extensionPath); this.extensions = extensionsMap; const extensionsManager = Array.from(this.extensions.values()).find((ext) => ext.name === "extensions"); if (extensionsManager && "setExtensions" in extensionsManager) { ; extensionsManager.setExtensions(this.extensions); } for (const [name, extension] of this.extensions) { const methodCount = Object.keys(extension.methods).length; logger_1.logger.info(`Loaded extension: ${name} with ${methodCount} methods`); } logger_1.logger.info(`Successfully loaded ${this.extensions.size} extensions`); } catch (error) { logger_1.logger.error("Failed to load extensions", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); logger_1.logger.warning("Service will start without extensions"); } } createErrorResponse(id, code, message, details) { const error = new error_service_1.RpcError({ message, code, details, requestId: id, }); return { jsonrpc: "2.0", id, error: error.toJSON(), }; } async handleRequest(request) { const { id, method, params = {} } = request; const traceId = logger_1.logger.startTrace("Handling RPC request", { method, id }); try { const [extensionName, methodName] = method.split("."); if (!extensionName || !methodName) { throw error_service_1.ErrorService.invalidRequest("Invalid method format. Expected 'extension.method'"); } const extension = this.extensions.get(extensionName); if (!extension) { throw error_service_1.ErrorService.extensionNotFound(`Extension '${extensionName}' not found`); } const methodHandler = extension.methods[methodName]?.bind(extension); if (!methodHandler) { throw error_service_1.ErrorService.methodNotFound(`Method '${methodName}' not found in extension '${extensionName}'`); } const timeoutMs = 5000; const result = await Promise.race([ methodHandler(params), new Promise((_, reject) => setTimeout(() => { reject(error_service_1.ErrorService.requestTimeout("Request timeout", { requestId: id, method, timeoutMs, })); }, timeoutMs)), ]); logger_1.logger.endTrace(traceId, "RPC request completed successfully", { method, id, result, }); return { jsonrpc: "2.0", id, result, }; } catch (error) { logger_1.logger.error("Error handling RPC request", { traceId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, method, params, }); if (error instanceof error_service_1.RpcError) { return { jsonrpc: "2.0", id, error: error.toJSON(), }; } const internalError = error_service_1.ErrorService.internalError(error instanceof Error ? error.message : "Unknown error", { requestId: id, method, }); return { jsonrpc: "2.0", id, error: internalError.toJSON(), }; } } async initializeTCPServer() { return new Promise((resolve) => { try { this.tcpServer = (0, node_net_1.createServer)({ keepAlive: true, noDelay: true, allowHalfOpen: false, pauseOnConnect: false, }); this.tcpServer.on("error", (error) => { if (error.code === "EADDRINUSE") { logger_1.logger.error(`TCP port ${this.tcpPort} is already in use`, { host: this.host, port: this.tcpPort, }); } else { logger_1.logger.error("TCP server error", { host: this.host, port: this.tcpPort, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } resolve(false); }); this.tcpServer.on("connection", (socket) => { const clientId = `${socket.remoteAddress}:${socket.remotePort}`; logger_1.logger.info("New TCP connection", { clientId }); socket.setKeepAlive(true, 1000); socket.setNoDelay(true); socket.setTimeout(10000); socket.on("timeout", () => { logger_1.logger.warning("TCP socket timeout", { clientId }); this.cleanupSocket(socket); }); let buffer = Buffer.alloc(0); const maxBufferSize = 1024 * 1024; socket.on("data", async (data) => { const traceId = logger_1.logger.startTrace("Received TCP data", { clientId, size: data.length, }); let requestId = null; try { buffer = Buffer.concat([buffer, data]); if (buffer.length > maxBufferSize) { logger_1.logger.error("TCP buffer size limit exceeded", { clientId, traceId, size: buffer.length, }); const errorResponse = this.createErrorResponse(null, interfaces_1.ErrorCode.SERVER_ERROR, "Buffer size limit exceeded", { size: buffer.length, maxSize: maxBufferSize }); this.sendTCPResponse(socket, errorResponse, clientId, traceId); buffer = Buffer.alloc(0); return; } let newlineIndex; while ((newlineIndex = buffer.indexOf("\n")) !== -1) { const messageStr = buffer.slice(0, newlineIndex).toString(); buffer = buffer.slice(newlineIndex + 1); try { const request = JSON.parse(messageStr); requestId = request.id ?? null; const response = await this.handleRequest(request); this.sendTCPResponse(socket, response, clientId, traceId); } catch (error) { logger_1.logger.error("Failed to handle TCP message", { clientId, traceId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, data: messageStr.substring(0, 1000), }); const errorResponse = this.createErrorResponse(requestId, interfaces_1.ErrorCode.PARSE_ERROR, error instanceof Error ? error.message : "Parse error", { data: messageStr.substring(0, 100) }); this.sendTCPResponse(socket, errorResponse, clientId, traceId); } } } catch (error) { logger_1.logger.error("TCP data processing error", { clientId, traceId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); const errorResponse = this.createErrorResponse(requestId, error instanceof error_service_1.RpcError ? error.code : interfaces_1.ErrorCode.INTERNAL_ERROR, error instanceof Error ? error.message : "Internal error", error instanceof error_service_1.RpcError ? error.details : undefined); this.sendTCPResponse(socket, errorResponse, clientId, traceId); } finally { logger_1.logger.endTrace(traceId, "TCP data processing completed", { clientId, size: data.length, }); } }); socket.on("error", (error) => { if (error.code !== "ECONNRESET") { logger_1.logger.error("TCP socket error", { clientId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } this.cleanupSocket(socket); }); socket.on("close", () => { logger_1.logger.info("TCP connection closed"); this.cleanupSocket(socket); }); }); this.tcpServer.listen(this.tcpPort, this.host, () => { logger_1.logger.info(`TCP server listening on ${this.host}:${this.tcpPort}`); resolve(true); }); } catch (error) { logger_1.logger.error("Failed to initialize TCP server", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); resolve(false); } }); } cleanupSocket(socket) { if (!socket.destroyed) { socket.end(); socket.destroy(); } } sendTCPResponse(socket, response, clientId, traceId) { if (!socket.destroyed) { const responseStr = JSON.stringify(response) + "\n"; socket.write(responseStr, (error) => { if (error) { logger_1.logger.error("Failed to send TCP response", { clientId, traceId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } else { logger_1.logger.debug("TCP response sent successfully", { clientId, traceId, size: responseStr.length, }); } }); } } async initializeUDPServer() { return new Promise((resolve) => { try { this.udpServer = (0, node_dgram_1.createSocket)("udp4"); this.udpServer.on("listening", () => { const address = this.udpServer?.address(); logger_1.logger.info("UDP server started", { host: this.host, port: this.udpPort, server: address, }); resolve(true); }); this.udpServer.on("error", (error) => { if (error.code === "EADDRINUSE") { logger_1.logger.error(`UDP port ${this.udpPort} is already in use`, { host: this.host, port: this.udpPort, }); } else { logger_1.logger.error("UDP server error", { host: this.host, port: this.udpPort, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } resolve(false); }); this.udpServer.on("message", async (data, rinfo) => { const clientId = `${rinfo.address}:${rinfo.port}`; const traceId = logger_1.logger.startTrace("Received UDP message", { clientId, size: data.length, }); let requestId = null; try { if (!this.udpServer) { throw error_service_1.ErrorService.internalError("UDP server not initialized"); } try { const messageStr = data.toString().trim(); const request = JSON.parse(messageStr); requestId = request.id ?? null; const response = await this.handleRequest(request); this.sendUDPResponse(response, rinfo, traceId); } catch (error) { logger_1.logger.error("UDP message processing error", { clientId, traceId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); const errorResponse = this.createErrorResponse(requestId, error instanceof error_service_1.RpcError ? error.code : interfaces_1.ErrorCode.INTERNAL_ERROR, error instanceof Error ? error.message : "Internal error", error instanceof error_service_1.RpcError ? error.details : undefined); this.sendUDPResponse(errorResponse, rinfo, traceId); } } catch (error) { logger_1.logger.error("UDP request handling error", { clientId, traceId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } finally { logger_1.logger.endTrace(traceId, "UDP message processing completed", { clientId, size: data.length, }); } }); this.udpServer.bind(this.udpPort, this.host); } catch (error) { logger_1.logger.error("Failed to initialize UDP server", { host: this.host, port: this.udpPort, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); resolve(false); } }); } sendUDPResponse(response, rinfo, traceId) { if (this.udpServer) { const responseStr = JSON.stringify(response) + "\n"; this.udpServer.send(responseStr, rinfo.port, rinfo.address, (error) => { if (error) { logger_1.logger.error("Failed to send UDP response", { clientId: `${rinfo.address}:${rinfo.port}`, traceId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); } else { logger_1.logger.debug("UDP response sent successfully", { clientId: `${rinfo.address}:${rinfo.port}`, traceId, size: responseStr.length, }); } }); } } async shutdown(timeoutMs = 5000) { logger_1.logger.info("Shutting down RPC service"); try { await new Promise((resolve, reject) => { const tcpShutdown = { done: false, waiting: false, }; const udpShutdown = { done: false, waiting: false, }; let _n = 0; let _to = setTimeout(() => { if (!tcpShutdown.done && !tcpShutdown.waiting) { this.tcpServer?.close(() => { logger_1.logger.info("TCP server closed"); tcpShutdown.done = true; tcpShutdown.waiting = false; }); } if (!udpShutdown.done && !udpShutdown.waiting) { this.udpServer?.close(() => { logger_1.logger.info("UDP server closed"); udpShutdown.done = true; udpShutdown.waiting = false; }); } if (tcpShutdown.done && !tcpShutdown.waiting && udpShutdown.done && !udpShutdown.waiting) { clearTimeout(_to); _to = undefined; resolve(true); } _n++; if (_n >= 5) { clearTimeout(_to); _to = undefined; reject(new Error("Shutdown timeout")); } }, timeoutMs); }); this.initialized = false; this.tcpServer = null; this.udpServer = null; process.exit(0); } catch (error) { logger_1.logger.error("Shutdown failed", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); process.exit(1); } } } exports.RPCService = RPCService; //# sourceMappingURL=rpc.service.js.map