@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
259 lines • 10.5 kB
JavaScript
"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