@arifwidianto/rpc-agent
Version:
RPC Agent for both client and server, extends more methods easily
463 lines • 21.4 kB
JavaScript
;
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