UNPKG

@comake/skl-js-engine

Version:

Standard Knowledge Language Javascript Engine

259 lines 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ParentStdioTransport = exports.StdioClientTransport = void 0; const logger_1 = require("../../../logger"); const JsonRpcClient_1 = require("../../jsonRpc/JsonRpcClient"); const types_1 = require("../../jsonRpc/types"); const BaseTransport_1 = require("../base/BaseTransport"); const ProcessManager_1 = require("../process/ProcessManager"); const Transport_1 = require("../Transport"); const MessageUtils_1 = require("../utils/MessageUtils"); const PollingUtils_1 = require("../utils/PollingUtils"); /** * Client transport implementation for stdio communication */ class StdioClientTransport { constructor(processManager) { this.processManager = processManager; this.logger = logger_1.Logger.getInstance(); } setName(name) { this.name = name; this.logger.setMetadata({ name, transport: 'StdioClientTransport' }); } async send(message) { this.logger.log('Sending message', message); this.processManager.write(`${message}\n`); } onMessage(handler) { this.messageHandler = handler; // Note: Message handling is done in the main ParentStdioTransport class // This is here for interface compliance } async close() { this.messageHandler = undefined; } } exports.StdioClientTransport = StdioClientTransport; /** * StdioTransport implementation for parent process communication with child * This is the parent-side transport that manages a child process */ class ParentStdioTransport extends BaseTransport_1.BaseTransport { constructor(executorScriptPath, server) { super(); this.processManager = new ProcessManager_1.ProcessManager(); this.messageBuffer = new MessageUtils_1.MessageBuffer(); this.executorScriptPath = executorScriptPath; this.server = server; this.client = new JsonRpcClient_1.JsonRpcClient({ // 1 hour defaultTimeout: 60 * 60 * 1000 }); this.clientTransport = new StdioClientTransport(this.processManager); this.setupServerMethods(); this.setupEventHandlers(); } /** * Initialize the transport connection */ async initialize(config, executionOptions) { if (this.internalStatus !== Transport_1.TransportStatus.disconnected) { throw new Error(`Cannot initialize transport in ${this.internalStatus} state`); } try { this.setStatus(Transport_1.TransportStatus.connecting); // Set up client transport this.client.setTransport(this.clientTransport); // Spawn the process // Use spawnTimeout if provided, otherwise fall back to timeout or default const spawnTimeout = config?.spawnTimeout ?? config?.timeout ?? 3000; await this.processManager.spawn({ scriptPath: this.executorScriptPath, executionOptions, startupTimeout: spawnTimeout }); // Set up process communication this.setupProcessCommunication(); // Wait for process to be ready // Use readyTimeout if provided, otherwise fall back to timeout or default const readyTimeout = config?.readyTimeout ?? config?.timeout ?? 10000; await this.waitForReady(readyTimeout); this.setStatus(Transport_1.TransportStatus.connected); } catch (error) { this.setStatus(Transport_1.TransportStatus.error); throw error; } } /** * Send a message through the transport */ async send(message) { if (!this.isReady()) { throw new Error('Transport is not ready'); } // Expect message to have method and params for direct RPC calls const rpcMessage = message; if (rpcMessage.method) { return this.client.request(rpcMessage.method, rpcMessage.params); } throw new Error('Message must have a method property for RPC calls'); } /** * Make a direct RPC request to the child process */ async request(method, params, options) { if (!this.isReady()) { throw new Error('Transport is not ready'); } return this.client.request(method, params, options); } /** * Send a notification to the child process (no response expected) */ async notify(method, params) { if (!this.isReady()) { throw new Error('Transport is not ready'); } return this.client.notify(method, params); } /** * Close the transport connection */ async close() { if (this.internalStatus === Transport_1.TransportStatus.disconnected) { return; } this.setStatus(Transport_1.TransportStatus.disconnected); // Close JSON-RPC client await this.client.close(); // Terminate child process await this.processManager.terminate(); } /** * Check if the transport is ready for communication */ isReady() { return this.internalStatus === Transport_1.TransportStatus.connected && this.processManager.isRunning(); } /** * Set up server methods for handling incoming requests from Deno process */ setupServerMethods() { // Handle status requests this.server.registerMethod(types_1.STANDARD_METHODS.getStatus, async () => ({ status: 'ready', uptime: process.uptime() * 1000, memoryUsage: { used: process.memoryUsage().heapUsed, total: process.memoryUsage().heapTotal } })); // Handle ping requests this.server.registerMethod(types_1.STANDARD_METHODS.ping, async () => 'pong'); // Handle log notifications this.server.registerMethod(types_1.STANDARD_METHODS.log, async (params) => { if (this.messageHandler) { await this.messageHandler(params); } }); } /** * Set up event handlers */ setupEventHandlers() { this.server.on('error', error => { this.emit('error', error); }); this.client.on('error', error => { this.emit('error', error); }); this.processManager.on('error', error => { this.emit('error', error); }); this.processManager.on('exit', (code, signal) => { if (this.internalStatus === Transport_1.TransportStatus.connected) { const error = new Error(`Process exited unexpectedly (code: ${code}, signal: ${signal})`); this.setStatus(Transport_1.TransportStatus.error); this.emit('error', error); } }); } /** * Set up process communication handlers */ setupProcessCommunication() { // Handle stdout data (responses from Deno process) this.processManager.on('stdout', (data) => { const messages = this.messageBuffer.addData(data.toString()); for (const messageData of messages) { this.logger.log('Received message', messageData); this.handleIncomingMessage(messageData).catch(error => { this.emit('error', error); }); } }); // Handle stderr data (log messages and errors from Deno process) this.processManager.on('stderr', (data) => { const stderrOutput = data.toString().trim(); // Only treat messages starting with "ERROR:" as actual errors if (stderrOutput.startsWith('ERROR:')) { this.emit('error', new Error(`Deno process error: ${stderrOutput}`)); } else { // Log normal stderr output as debug information this.logger.log('Child process log', stderrOutput); } }); } /** * Handle incoming message from Deno process with bidirectional routing */ async handleIncomingMessage(messageData) { try { const message = JSON.parse(messageData); // Check if this is a response to our request (has 'result' or 'error' and 'id') if ((0, MessageUtils_1.isResponse)(message)) { // This is a response to a request we made - route to client this.logger.log('Routing response to parent client', { id: message.id }); await this.client.handleIncomingMessage(messageData); } else { // This is a request or notification for us - route to server this.logger.log('Routing request to parent server', { method: message.method, id: message.id }); const response = await this.server.processMessage(messageData); this.logger.log('Sending response to parent process', response); // Only send response if there is one (requests get responses, notifications don't) if (response) { const responseData = `${JSON.stringify(response)}\n`; this.processManager.write(responseData); } } } catch (error) { // Silently ignore invalid JSON messages (likely log output from child process) if (error instanceof SyntaxError && error.message.includes('JSON')) { this.logger.log('Ignoring invalid JSON message', { message: messageData }); return; } // Re-throw other types of errors this.emit('error', new Error(`Failed to handle incoming message: ${error.message}`)); } } /** * Wait for the process to be ready */ async waitForReady(timeout) { // Use a reasonable ping timeout (1/4 of total timeout, min 500ms, max 2000ms) const pingTimeout = Math.max(500, Math.min(2000, timeout / 4)); await (0, PollingUtils_1.pollUntilSuccess)(() => this.client.request(types_1.STANDARD_METHODS.ping, undefined, { timeout: pingTimeout }), { timeout, interval: 100, // Let process start before first ping initialDelay: 100 }, 'Transport initialization timed out'); } } exports.ParentStdioTransport = ParentStdioTransport; //# sourceMappingURL=ParentStdioTransport.js.map