UNPKG

@comake/skl-js-engine

Version:

Standard Knowledge Language Javascript Engine

391 lines 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonRpcServer = void 0; const types_1 = require("./types"); /** * JSON-RPC 2.0 Server implementation */ class JsonRpcServer { constructor(config = {}) { this.methods = {}; this.pendingRequests = new Map(); this.listeners = {}; this.config = { maxConcurrentRequests: config.maxConcurrentRequests ?? 100, requestTimeout: config.requestTimeout ?? 30000, strictValidation: config.strictValidation ?? true, errorHandler: config.errorHandler ?? this.defaultErrorHandler.bind(this) }; } /** * Register a method handler * @param method - Method name * @param handler - Method handler function */ registerMethod(method, handler) { if (typeof method !== 'string' || method.length === 0) { throw new Error('Method name must be a non-empty string'); } if (typeof handler !== 'function') { throw new Error('Handler must be a function'); } this.methods[method] = handler; } /** * Unregister a method handler * @param method - Method name */ unregisterMethod(method) { Reflect.deleteProperty(this.methods, method); } /** * Get all registered methods * @returns Array of method names */ getRegisteredMethods() { return Object.keys(this.methods); } /** * Process an incoming JSON-RPC message * @param messageData - Raw message data (string or object) * @returns Promise resolving to response (if request) or undefined (if notification) */ async processMessage(messageData) { let message; try { // Parse message if it's a string if (typeof messageData === 'string') { try { message = JSON.parse(messageData); } catch { return this.createErrorResponse(null, { code: types_1.JsonRpcErrorCode.parseError, message: 'Parse error', data: 'Invalid JSON' }); } } else { message = messageData; } // Validate message format const validation = this.validateMessage(message); if (!validation.valid) { return this.createErrorResponse(this.extractId(message), validation.error); } // Handle request or notification if (this.isRequest(message)) { this.emit('request', message); return await this.handleRequest(message); } if (this.isNotification(message)) { this.emit('notification', message); await this.handleNotification(message); return; } // This shouldn't happen after validation, but just in case return this.createErrorResponse(this.extractId(message), { code: types_1.JsonRpcErrorCode.invalidRequest, message: 'Invalid request', data: 'Message is neither request nor notification' }); } catch (error) { const jsonRpcError = this.config.errorHandler(error, this.isRequest(message) ? message : undefined); return this.createErrorResponse(this.extractId(message), jsonRpcError); } } /** * Handle a JSON-RPC request * @param request - JSON-RPC request object * @returns Promise resolving to response */ async handleRequest(request) { // Check concurrent request limit if (this.pendingRequests.size >= this.config.maxConcurrentRequests) { return this.createErrorResponse(request.id, { code: types_1.JsonRpcErrorCode.internalError, message: 'Server overloaded', data: `Maximum concurrent requests (${this.config.maxConcurrentRequests}) exceeded` }); } // Check if method exists const handler = this.methods[request.method]; if (!handler) { return this.createErrorResponse(request.id, { code: types_1.JsonRpcErrorCode.methodNotFound, message: 'Method not found', data: `Method '${request.method}' is not registered` }); } // Execute method with timeout const requestPromise = this.executeMethodWithTimeout(handler, request); try { const result = await requestPromise; return this.createSuccessResponse(request.id, result); } catch (error) { const jsonRpcError = this.config.errorHandler(error, request); return this.createErrorResponse(request.id, jsonRpcError); } finally { this.pendingRequests.delete(request.id); } } /** * Handle a JSON-RPC notification * @param notification - JSON-RPC notification object */ async handleNotification(notification) { const handler = this.methods[notification.method]; if (!handler) { // For notifications, we silently ignore unknown methods return; } try { // Handle both sync and async functions by wrapping in Promise.resolve const result = handler(notification.params); await Promise.resolve(result); } catch (error) { // For notifications, we emit error but don't send response this.emit('error', error); } } /** * Execute method with timeout * @param handler - Method handler * @param request - JSON-RPC request * @returns Promise resolving to result */ async executeMethodWithTimeout(handler, request) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Method '${request.method}' timed out after ${this.config.requestTimeout}ms`)); }, this.config.requestTimeout); try { const pendingRequest = { id: request.id, method: request.method, timestamp: Date.now(), timeout: this.config.requestTimeout, resolve(value) { clearTimeout(timeoutId); resolve(value); }, reject(error) { clearTimeout(timeoutId); reject(error); } }; // Handle both sync and async functions by wrapping in Promise.resolve const result = handler(request.params, request.id); Promise.resolve(result) .then((finalResult) => { clearTimeout(timeoutId); pendingRequest.resolve(finalResult); }) .catch((error) => { clearTimeout(timeoutId); pendingRequest.reject(error); }); } catch (error) { // Handle synchronous errors (like sync functions that throw) clearTimeout(timeoutId); reject(error); } }); } /** * Create a pending request promise with timeout * @param request - JSON-RPC request * @param timeout - Timeout in milliseconds * @returns Promise resolving to the result */ async createPendingRequest(request, timeout) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.pendingRequests.delete(request.id); this.emit('timeout', request); reject(new Error(`Request ${request.id} timed out after ${timeout}ms`)); }, timeout); const pendingRequest = { id: request.id, method: request.method, timestamp: Date.now(), timeout, resolve(value) { clearTimeout(timeoutId); resolve(value); }, reject(error) { clearTimeout(timeoutId); reject(error); } }; this.pendingRequests.set(request.id, pendingRequest); }); } /** * Validate JSON-RPC message format * @param message - Message to validate * @returns Validation result */ validateMessage(message) { if (!message || typeof message !== 'object') { return { valid: false, error: { code: types_1.JsonRpcErrorCode.invalidRequest, message: 'Invalid request', data: 'Message must be an object' } }; } if (message.jsonrpc !== types_1.JSONRPC_VERSION) { return { valid: false, error: { code: types_1.JsonRpcErrorCode.invalidRequest, message: 'Invalid request', data: `Invalid jsonrpc version. Expected '${types_1.JSONRPC_VERSION}'` } }; } if (typeof message.method !== 'string' || message.method.length === 0) { return { valid: false, error: { code: types_1.JsonRpcErrorCode.invalidRequest, message: 'Invalid request', data: 'Method must be a non-empty string' } }; } // Check if it's a request (has id) or notification (no id) const hasId = 'id' in message; if (hasId && typeof message.id !== 'string' && typeof message.id !== 'number' && message.id !== null) { return { valid: false, error: { code: types_1.JsonRpcErrorCode.invalidRequest, message: 'Invalid request', data: 'ID must be a string, number, or null' } }; } return { valid: true }; } /** * Check if message is a request (has id) */ isRequest(message) { return message && typeof message === 'object' && 'id' in message; } /** * Check if message is a notification (no id) */ isNotification(message) { return message && typeof message === 'object' && !('id' in message); } /** * Extract ID from message safely */ extractId(message) { if (message && typeof message === 'object' && 'id' in message) { return message.id; } return null; } /** * Create success response */ createSuccessResponse(id, result) { return { jsonrpc: types_1.JSONRPC_VERSION, result, id }; } /** * Create error response */ createErrorResponse(id, error) { return { jsonrpc: types_1.JSONRPC_VERSION, error, id }; } /** * Default error handler */ defaultErrorHandler(error, request) { // Handle parameter validation errors if (error.message.includes('invalid param')) { return { code: types_1.JsonRpcErrorCode.invalidParams, message: 'Invalid params', data: error.message }; } // Default to internal error return { code: types_1.JsonRpcErrorCode.internalError, message: error.message || 'Internal error', data: error.stack }; } /** * Register event listener */ on(event, listener) { this.listeners[event] = listener; } /** * Remove event listener */ off(event, listener) { if (this.listeners[event] === listener) { Reflect.deleteProperty(this.listeners, event); } } /** * Emit event */ emit(event, ...args) { const listener = this.listeners[event]; if (listener) { listener(...args); } } /** * Get server statistics */ getStats() { return { registeredMethods: Object.keys(this.methods).length, pendingRequests: this.pendingRequests.size, maxConcurrentRequests: this.config.maxConcurrentRequests }; } /** * Shutdown server (reject all pending requests) */ async shutdown() { // Reject all pending requests for (const [id, pendingRequest] of this.pendingRequests) { try { pendingRequest.reject(new Error('Server shutdown')); } catch { // Ignore errors during shutdown } } this.pendingRequests.clear(); } } exports.JsonRpcServer = JsonRpcServer; //# sourceMappingURL=JsonRpcServer.js.map