@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
215 lines • 7.45 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChildStdioTransport = void 0;
/* eslint-disable indent */
const node_events_1 = require("node:events");
const JsonRpcClient_1 = require("../../jsonRpc/JsonRpcClient");
const JsonRpcServer_1 = require("../../jsonRpc/JsonRpcServer");
const MessageUtils_1 = require("../utils/MessageUtils");
/**
* Custom logger that outputs to stderr to avoid contaminating stdout JSON-RPC channel
*/
class ChildProcessLogger {
constructor() {
this.metadata = {};
}
setMetadata(metadata) {
this.metadata = { ...this.metadata, ...metadata };
}
log(message, data) {
const logEntry = {
message,
metadata: this.metadata,
...data && { data }
};
// Use stderr to avoid contaminating stdout JSON-RPC channel
process.stderr.write(`${message} ${JSON.stringify(logEntry.metadata)} ${data ? JSON.stringify(data) : ''}\n`);
}
error(message, error) {
const logEntry = {
level: 'error',
message,
metadata: this.metadata,
...error && { error: error.message, stack: error.stack }
};
process.stderr.write(`ERROR: ${JSON.stringify(logEntry)}\n`);
}
}
/**
* Bidirectional StdioTransport for child processes
* This class handles both server (receiving requests) and client (sending requests) functionality
* over a single stdio channel with proper message routing.
*
* Usage in child process:
* ```typescript
* const transport = new ChildStdioTransport();
*
* // Register methods that parent can call
* transport.registerMethod('ping', async () => 'pong');
*
* // Make requests to parent
* const result = await transport.request('getTime');
* ```
*/
class ChildStdioTransport extends node_events_1.EventEmitter {
constructor() {
super();
this.logger = new ChildProcessLogger();
this.messageBuffer = new MessageUtils_1.MessageBuffer();
this.initialized = false;
this.server = new JsonRpcServer_1.JsonRpcServer();
this.client = new JsonRpcClient_1.JsonRpcClient();
// Set up the client transport that routes through our stdio
const clientTransport = {
send: async (message) => {
this.sendMessage(message);
},
onMessage() {
// The message routing will handle client responses automatically
},
async close() {
// Nothing to close for stdio
}
};
this.client.setTransport(clientTransport);
this.setupEventHandlers();
}
/**
* Set a name for this transport (used in logging)
*/
setName(name) {
this.name = name;
this.logger.setMetadata({ name, transport: 'ChildStdioTransport' });
}
/**
* Initialize the bidirectional transport
* This should be called once after setting up all methods
*/
async initialize() {
if (this.initialized) {
throw new Error('Transport already initialized');
}
this.initialized = true;
this.setupStdioCommunication();
this.logger.log('Transport initialized');
}
/**
* Register a method that the parent can call
* @param method - Method name
* @param handler - Method handler function
*/
registerMethod(method, handler) {
this.server.registerMethod(method, handler);
}
/**
* Send a request to the parent process
* @param method - Method name
* @param params - Method parameters
* @param options - Request options
* @returns Promise resolving to the response
*/
async request(method, params, options) {
if (!this.initialized) {
throw new Error('Transport not initialized. Call initialize() first.');
}
return this.client.request(method, params, options);
}
/**
* Send a notification to the parent process (no response expected)
* @param method - Method name
* @param params - Method parameters
*/
async notify(method, params) {
if (!this.initialized) {
throw new Error('Transport not initialized. Call initialize() first.');
}
return this.client.notify(method, params);
}
/**
* Get transport statistics
*/
getStats() {
return {
serverMethods: 0,
pendingRequests: this.client.getStats().pendingRequests,
initialized: this.initialized
};
}
/**
* Close the transport and cleanup resources
*/
async close() {
await this.client.close();
this.initialized = false;
this.removeAllListeners();
}
/**
* Set up stdio communication handlers
*/
setupStdioCommunication() {
// Handle incoming data from parent
process.stdin.on('data', async (data) => {
const messages = this.messageBuffer.addData(data.toString());
for (const messageData of messages) {
await this.handleIncomingMessage(messageData);
}
});
// Handle process termination gracefully
process.on('SIGTERM', () => {
this.close().finally(() => process.exit(0));
});
process.on('SIGINT', () => {
this.close().finally(() => process.exit(0));
});
}
/**
* Handle incoming message and route to appropriate handler
*/
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 client: ${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 server: ${message.method}`);
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) {
this.sendMessage(JSON.stringify(response));
}
}
}
catch (error) {
process.stderr.write(`Error processing message: ${error.message}\n`);
this.emit('error', error);
}
}
/**
* Send a message to the parent process
*/
sendMessage(message) {
this.logger.log(`Sending: ${message}`);
process.stdout.write(`${message}\n`);
}
/**
* Set up event handlers
*/
setupEventHandlers() {
this.server.on('error', (error) => {
this.logger.error('Server error', error);
this.emit('error', error);
});
this.client.on('error', (error) => {
this.logger.error('Client error', error);
this.emit('error', error);
});
}
}
exports.ChildStdioTransport = ChildStdioTransport;
//# sourceMappingURL=ChildStdioTransport.js.map