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