UNPKG

@comake/skl-js-engine

Version:

Standard Knowledge Language Javascript Engine

641 lines 24.7 kB
"use strict"; // /* eslint-disable indent */ // /* eslint-disable no-void */ // /* eslint-disable @typescript-eslint/no-use-before-define */ // /* eslint-disable no-inline-comments */ // /* eslint-disable line-comment-position */ // /* eslint-disable @typescript-eslint/explicit-function-return-type */ // import type { ChildProcess } from 'child_process'; // import { EventEmitter } from 'node:events'; // import type { Readable, Writable } from 'stream'; // import { Logger } from '../../logger'; // import { DEFAULT_EXECUTION_OPTIONS, EXECUTION_CONSTANTS } from '../constants'; // import { spawnDenoProcess } from '../denoUtils'; // import { ProcessSpawnError } from '../errors'; // import type { ClientTransport } from '../jsonRpc/JsonRpcClient'; // import { JsonRpcClient } from '../jsonRpc/JsonRpcClient'; // import { JsonRpcServer } from '../jsonRpc/JsonRpcServer'; // import type { // LogNotification, // StatusRequest, // StatusResponse // } from '../jsonRpc/types'; // import { // STANDARD_METHODS // } from '../jsonRpc/types'; // import { buildDenoPermissions, validatePermissionConfig } from '../PermissionBuilder'; // import type { ExecutionOptions, PermissionConfig } from '../types'; // import { BaseTransport } from './base/BaseTransport'; // import type { Transport, TransportConfig } from './Transport'; // import { TransportStatus } from './Transport'; // /** // * StdioTransport implementation using stdin/stdout for JSON-RPC communication // * This is the parent-side transport that manages a child process // */ // export class StdioTransport extends BaseTransport { // private childProcess?: ChildProcess; // private readonly server: JsonRpcServer; // private readonly client: JsonRpcClient; // private readonly clientTransport: StdioClientTransport; // private readonly executorScriptPath: string; // public constructor(executorScriptPath: string, server: JsonRpcServer) { // super(); // this.executorScriptPath = executorScriptPath; // this.server = server; // this.client = new JsonRpcClient(); // this.clientTransport = new StdioClientTransport(); // this.setupServerMethods(); // this.setupEventHandlers(); // } // /** // * Initialize the transport connection // */ // public async initialize(config?: TransportConfig, executionOptions?: ExecutionOptions): Promise<void> { // if (this.status !== TransportStatus.disconnected) { // throw new Error(`Cannot initialize transport in ${this.status} state`); // } // try { // this.setStatus(TransportStatus.connecting); // const normalizedOptions = this.normalizeOptions(executionOptions); // const permissionConfig = this.extractPermissionConfig(normalizedOptions); // // Validate permissions before proceeding // validatePermissionConfig(permissionConfig); // const permissions = buildDenoPermissions(permissionConfig); // const mergedPermissions: string[] = [ ...EXECUTION_CONSTANTS.denoFlags ]; // permissions.forEach(permission => { // if (!mergedPermissions.includes(permission)) { // mergedPermissions.push(permission); // } // }); // // Build command arguments (will be updated when we have permission support) // const commandArgs = [ ...mergedPermissions, this.executorScriptPath ]; // // Spawn the Deno process // this.childProcess = spawnDenoProcess(commandArgs); // // Set up client transport // this.clientTransport.setProcess(this.childProcess.stdout!, this.childProcess.stdin!); // this.client.setTransport(this.clientTransport); // // Set up process communication // this.setupProcessCommunication(); // // Wait for process to be ready // await this.waitForReady(config?.timeout ?? 5000); // this.setStatus(TransportStatus.connected); // } catch (error: unknown) { // this.setStatus(TransportStatus.error); // throw error; // } // } // /** // * Normalizes execution options with defaults // * @param options - Raw execution options // * @returns Normalized options with all required fields // */ // private normalizeOptions(options?: ExecutionOptions): Required<ExecutionOptions> { // return { // timeout: options?.timeout ?? DEFAULT_EXECUTION_OPTIONS.timeout, // functionName: options?.functionName ?? DEFAULT_EXECUTION_OPTIONS.functionName, // allowNetwork: options?.allowNetwork ?? DEFAULT_EXECUTION_OPTIONS.allowNetwork, // allowedDomains: options?.allowedDomains ?? [ ...DEFAULT_EXECUTION_OPTIONS.allowedDomains ], // allowEnv: options?.allowEnv ?? DEFAULT_EXECUTION_OPTIONS.allowEnv, // allowRead: options?.allowRead ?? DEFAULT_EXECUTION_OPTIONS.allowRead, // debugMode: options?.debugMode ?? DEFAULT_EXECUTION_OPTIONS.debugMode, // retries: options?.retries ?? DEFAULT_EXECUTION_OPTIONS.retries // }; // } // /** // * Extracts permission configuration from execution options // * @param options - Normalized execution options // * @returns Permission configuration object // */ // private extractPermissionConfig(options: Required<ExecutionOptions>): PermissionConfig { // return { // allowNetwork: options.allowNetwork, // allowedDomains: options.allowedDomains, // allowEnv: options.allowEnv, // allowRead: options.allowRead // }; // } // /** // * Send a message through the transport // */ // public async send<TRequest, TResponse>(message: TRequest): Promise<TResponse> { // if (!this.isReady()) { // throw new Error('Transport is not ready'); // } // // Expect message to have method and params for direct RPC calls // const rpcMessage = message as any; // 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 // */ // public async request<TParams = any, TResult = any>( // method: string, // params?: TParams, // options?: { timeout?: number; retries?: number } // ): Promise<TResult> { // if (!this.isReady()) { // throw new Error('Transport is not ready'); // } // return this.client.request<TParams, TResult>(method, params, options); // } // /** // * Send a notification to the child process (no response expected) // */ // public async notify<TParams = any>(method: string, params?: TParams): Promise<void> { // if (!this.isReady()) { // throw new Error('Transport is not ready'); // } // return this.client.notify(method, params); // } // /** // * Close the transport connection // */ // public async close(): Promise<void> { // if (this.status === TransportStatus.disconnected) { // return; // } // this.setStatus(TransportStatus.disconnected); // // Close JSON-RPC client // await this.client.close(); // // Close child process // if (this.childProcess && !this.childProcess.killed) { // this.childProcess.kill(EXECUTION_CONSTANTS.processSignals.term); // // Wait for process to exit gracefully // await new Promise<void>(resolve => { // const timeout = setTimeout(() => { // if (this.childProcess && !this.childProcess.killed) { // this.childProcess.kill('SIGKILL'); // } // resolve(); // }, 3000); // this.childProcess!.on('exit', () => { // clearTimeout(timeout); // resolve(); // }); // }); // } // this.childProcess = undefined; // } // /** // * Check if the transport is ready for communication // */ // public isReady(): boolean { // return this.status === TransportStatus.connected && this.childProcess !== undefined && !this.childProcess.killed; // } // /** // * Set up server methods for handling incoming requests from Deno process // */ // private setupServerMethods(): void { // // Handle status requests // this.server.registerMethod<StatusRequest, StatusResponse>(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(STANDARD_METHODS.ping, async() => 'pong'); // // Handle log notifications // this.server.registerMethod<LogNotification, void>(STANDARD_METHODS.log, async params => { // if (this.messageHandler) { // await this.messageHandler(params); // } // }); // } // /** // * Set up event handlers // */ // private setupEventHandlers(): void { // this.server.on('error', error => { // this.handleError(error, 'Server error'); // }); // this.client.on('error', error => { // this.handleError(error, 'Client error'); // }); // // Add fallback error handler to prevent process crashes // this.on('error', (error: Error) => { // // If no one else handles the error, at least log it instead of crashing // if (this.listenerCount('error') === 1) { // this.logger.error('Unhandled transport error', error); // } // }); // } // /** // * Set up process communication handlers // */ // private setupProcessCommunication(): void { // if (!this.childProcess) { // throw new Error('Child process not available'); // } // let buffer = ''; // // Handle stdout data (responses from Deno process) // this.childProcess.stdout?.on('data', (data: Buffer) => { // buffer += data.toString(); // // Process complete JSON messages // const lines = buffer.split('\n'); // buffer = lines.pop() ?? ''; // Keep incomplete line in buffer // for (const line of lines) { // if (line.trim()) { // this.logger.log('Received message', line.trim()); // this.handleIncomingMessage(line.trim()).catch(error => { // this.emit('error', error); // }); // } // } // }); // // Handle stderr data (log messages and errors from Deno process) // this.childProcess.stderr?.on('data', (data: Buffer) => { // 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 process exit // this.childProcess.on('exit', (code, signal) => { // if (this.status === TransportStatus.connected) { // const error = new Error(`Process exited unexpectedly (code: ${code}, signal: ${signal})`); // this.setStatus(TransportStatus.error); // this.emit('error', error); // } // }); // // Handle process errors // this.childProcess.on('error', error => { // this.setStatus(TransportStatus.error); // this.emit('error', new ProcessSpawnError(error)); // }); // } // /** // * Handle incoming message from Deno process with bidirectional routing // */ // private async handleIncomingMessage(messageData: string): Promise<void> { // try { // const message = this.safeParse(messageData); // if (!message) { // return; // } // // Check if this is a response to our request (has 'result' or 'error' and 'id') // if (this.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 && this.childProcess?.stdin) { // const responseData = `${JSON.stringify(response)}\n`; // this.childProcess.stdin.write(responseData); // } // } // } catch (error: unknown) { // this.emit('error', new Error(`Failed to handle incoming message: ${(error as Error).message}`)); // } // } // private safeParse(message: string): any { // try { // return JSON.parse(message); // } catch { // return undefined; // } // } // /** // * Check if a message is a JSON-RPC response // */ // private isResponse(message: any): boolean { // return message && // typeof message === 'object' && // ('result' in message || 'error' in message) && // 'id' in message && // !('method' in message); // } // /** // * Wait for the process to be ready // */ // private async waitForReady(timeout: number): Promise<void> { // return new Promise((resolve, reject) => { // const timeoutId = setTimeout(() => { // reject(new Error(`Transport initialization timed out after ${timeout}ms`)); // }, timeout); // // Try to ping the process to check if it's ready // const checkReady = async(): Promise<void> => { // try { // await this.client.request(STANDARD_METHODS.ping, undefined, { timeout: 1000 }); // clearTimeout(timeoutId); // resolve(); // } catch { // // Process not ready yet, try again // setTimeout(checkReady, 100); // } // }; // // Start checking after a short delay to let process start // setTimeout(checkReady, 100); // }); // } // } // /** // * Client transport implementation for stdio communication // */ // export class StdioClientTransport implements ClientTransport { // private stdout?: Readable; // private stdin?: Writable; // private messageHandler?: (message: string) => void; // private name?: string; // private readonly logger = Logger.getInstance(); // public setProcess(stdout: Readable, stdin: Writable): void { // this.stdout = stdout; // this.stdin = stdin; // } // public setName(name: string): void { // this.name = name; // this.logger.setMetadata({ name, transport: 'StdioClientTransport' }); // } // public async send(message: string): Promise<void> { // if (!this.stdin) { // throw new Error('Process stdin not available'); // } // this.logger.log('Sending message', message); // this.stdin.write(`${message}\n`); // } // public onMessage(handler: (message: string) => void): void { // this.messageHandler = handler; // // Note: Message handling is done in the main StdioTransport class // // This is here for interface compliance // } // public async close(): Promise<void> { // this.stdout = undefined; // this.stdin = undefined; // this.messageHandler = undefined; // } // } // /** // * 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 BidirectionalStdioTransport(); // * // * // Register methods that parent can call // * transport.registerMethod('ping', async () => 'pong'); // * // * // Make requests to parent // * const result = await transport.request('getTime'); // * ``` // */ // export class BidirectionalStdioTransport extends EventEmitter { // private readonly server: JsonRpcServer; // private readonly client: JsonRpcClient; // private readonly logger = Logger.getInstance(); // private buffer = ''; // private name?: string; // private initialized = false; // public constructor() { // super(); // this.server = new JsonRpcServer(); // this.client = new JsonRpcClient(); // // Set up the client transport that routes through our stdio // const clientTransport: ClientTransport = { // send: async(message: string): Promise<void> => { // this.sendMessage(message); // }, // onMessage(): void { // // The message routing will handle client responses automatically // }, // async close(): Promise<void> { // // Nothing to close for stdio // } // }; // this.client.setTransport(clientTransport); // this.setupEventHandlers(); // } // /** // * Set a name for this transport (used in logging) // */ // public setName(name: string): void { // this.name = name; // this.logger.setMetadata({ name, transport: 'BidirectionalStdioTransport' }); // } // /** // * Initialize the bidirectional transport // * This should be called once after setting up all methods // */ // public async initialize(): Promise<void> { // if (this.initialized) { // throw new Error('Transport already initialized'); // } // this.initialized = true; // this.setupStdioCommunication(); // // Log to stderr to avoid contaminating stdout // this.logger.log('Transport initialized'); // } // /** // * Register a method that the parent can call // * @param method - Method name // * @param handler - Method handler function // */ // public registerMethod<TParams = any, TResult = any>( // method: string, // handler: (params: TParams) => Promise<TResult> | TResult // ): void { // 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 // */ // public async request<TParams = any, TResult = any>( // method: string, // params?: TParams, // options?: { timeout?: number } // ): Promise<TResult> { // if (!this.initialized) { // throw new Error('Transport not initialized. Call initialize() first.'); // } // return this.client.request<TParams, TResult>(method, params, options); // } // /** // * Send a notification to the parent process (no response expected) // * @param method - Method name // * @param params - Method parameters // */ // public async notify<TParams = any>(method: string, params?: TParams): Promise<void> { // if (!this.initialized) { // throw new Error('Transport not initialized. Call initialize() first.'); // } // return this.client.notify(method, params); // } // /** // * Get transport statistics // */ // public getStats(): { // serverMethods: number; // pendingRequests: number; // initialized: boolean; // } { // return { // serverMethods: 0, // Server methods count not accessible due to private property // pendingRequests: this.client.getStats().pendingRequests, // initialized: this.initialized // }; // } // /** // * Close the transport and cleanup resources // */ // public async close(): Promise<void> { // await this.client.close(); // this.initialized = false; // this.removeAllListeners(); // } // /** // * Set up stdio communication handlers // */ // private setupStdioCommunication(): void { // // Handle incoming data from parent // process.stdin.on('data', async(data: Buffer) => { // this.buffer += data.toString(); // // Process complete JSON messages // const lines = this.buffer.split('\n'); // this.buffer = lines.pop() ?? ''; // Keep incomplete line in buffer // for (const line of lines) { // if (line.trim()) { // await this.handleIncomingMessage(line.trim()); // } // } // }); // // 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 // */ // private async handleIncomingMessage(messageData: string): Promise<void> { // try { // const message = this.safeParse(messageData); // if (!message) { // return; // } // // Check if this is a response to our request (has 'result' or 'error' and 'id') // if (this.isResponse(message)) { // // This is a response to a request we made - route to client // this.logger.log(`Routing response to client: ${message.id}`); // // Manually trigger client's handleIncomingMessage // 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: unknown) { // process.stderr.write(`Error processing message: ${(error as Error).message}\n`); // this.emit('error', error); // } // } // private safeParse(message: string): any { // try { // return JSON.parse(message); // } catch { // return undefined; // } // } // /** // * Check if a message is a JSON-RPC response // */ // private isResponse(message: any): boolean { // return message && // typeof message === 'object' && // ('result' in message || 'error' in message) && // 'id' in message && // !('method' in message); // } // /** // * Send a message to the parent process // */ // private sendMessage(message: string): void { // // Log to stderr to avoid contaminating stdout JSON-RPC channel // this.logger.log(`Sending: ${message}`); // process.stdout.write(`${message}\n`); // } // /** // * Set up event handlers // */ // private setupEventHandlers(): void { // this.server.on('error', (error: Error) => { // this.logger.error('Server error', error); // this.emit('error', error); // }); // this.client.on('error', (error: Error) => { // this.logger.error('Client error', error); // this.emit('error', error); // }); // } // } // export class StdioServerTransport implements Transport { // private readonly server: JsonRpcServer; // private name?: string; // private readonly logger = Logger.getInstance(); // public constructor(private readonly stdio: Readable, private readonly stdout: Writable, server: JsonRpcServer) { // this.server = server; // } // public setName(name: string): void { // this.name = name; // this.logger.setMetadata({ name }); // } // public get status(): TransportStatus { // return TransportStatus.connected; // } // public isReady(): boolean { // return true; // } // public async initialize(): Promise<void> { // this.logger.log('Initializing transport'); // this.stdio.on('data', async(data: Buffer) => { // this.logger.log('Received message', data.toString()); // const response = await this.server.processMessage(data.toString()); // await this.send(JSON.stringify(response)); // }); // } // public async send<TRequest, TResponse>(message: TRequest): Promise<TResponse> { // this.logger.log('Sending message', message); // return new Promise<any>(resolve => { // const json = JSON.stringify(message); // if (this.stdout.write(json)) { // resolve(undefined as unknown as TResponse); // } else { // this.stdout.once('drain', resolve); // } // }); // } // public onMessage<T>(): void { // return void 0; // } // public async close(): Promise<void> { // this.logger.log('Closing transport'); // this.stdio.destroy(); // } // } //# sourceMappingURL=StdioTransport.js.map