UNPKG

@comake/skl-js-engine

Version:

Standard Knowledge Language Javascript Engine

335 lines 11.1 kB
"use strict"; /* eslint-disable indent */ Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonRpcClient = void 0; const types_1 = require("./types"); /** * JSON-RPC 2.0 Client implementation */ class JsonRpcClient { constructor(config = {}) { this.pendingRequests = new Map(); this.listeners = {}; this.requestIdCounter = 0; this.config = { defaultTimeout: config.defaultTimeout ?? 30000, maxRetries: config.maxRetries ?? 3, retryDelay: config.retryDelay ?? 1000, sequentialIds: config.sequentialIds ?? true }; } /** * Set the transport for sending messages * @param transport - Transport implementation */ setTransport(transport) { this.transport = transport; this.transport.onMessage((message) => { this.handleIncomingMessage(message).catch((error) => { this.emit('error', error); }); }); } /** * Send a JSON-RPC request and wait for response * @param method - Method name * @param params - Method parameters * @param options - Request options * @returns Promise resolving to the response result */ async request(method, params, options = {}) { if (!this.transport) { throw new Error('Transport not set. Call setTransport() first.'); } const timeout = options.timeout ?? this.config.defaultTimeout; const maxRetries = options.retries ?? this.config.maxRetries; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const request = this.createRequest(method, params); const result = await this.sendRequestWithTimeout(request, timeout); return result; } catch (error) { lastError = error; // Don't retry on certain errors if (this.shouldNotRetry(error) || attempt === maxRetries) { break; } // Wait before retrying if (attempt < maxRetries) { await this.delay(this.config.retryDelay * (attempt + 1)); } } } throw lastError ?? new Error('Request failed after all retry attempts'); } /** * Send a JSON-RPC notification (no response expected) * @param method - Method name * @param params - Method parameters */ async notify(method, params) { if (!this.transport) { throw new Error('Transport not set. Call setTransport() first.'); } const notification = this.createNotification(method, params); await this.transport.send(JSON.stringify(notification)); } /** * Send a batch of requests * @param requests - Array of request specifications * @returns Promise resolving to array of results */ async batch(requests) { if (!this.transport) { throw new Error('Transport not set. Call setTransport() first.'); } const batchRequests = requests.map((req) => { const id = req.id ?? this.generateId(); return this.createRequest(req.method, req.params, id); }); const batchMessage = JSON.stringify(batchRequests); // Create pending promises for all requests const promises = batchRequests.map((request) => this.createPendingRequest(request, this.config.defaultTimeout)); // Send the batch await this.transport.send(batchMessage); // Wait for all responses return Promise.all(promises); } /** * Handle incoming message from transport * @param messageData - Raw message data */ async handleIncomingMessage(messageData) { try { const message = JSON.parse(messageData); // Handle batch response if (Array.isArray(message)) { for (const response of message) { this.handleResponse(response); } } else { this.handleResponse(message); } } catch (error) { this.emit('error', new Error(`Failed to parse incoming message: ${error.message}`)); } } /** * Handle a single response message * @param response - JSON-RPC response */ handleResponse(response) { this.emit('response', response); const pendingRequest = this.pendingRequests.get(response.id); if (!pendingRequest) { // Orphaned response - might be from a timed-out request return; } this.pendingRequests.delete(response.id); if (this.isSuccessResponse(response)) { pendingRequest.resolve(response.result); } else { const error = new Error(response.error.message); error.code = response.error.code; error.data = response.error.data; pendingRequest.reject(error); } } /** * Send a request with timeout handling * @param request - JSON-RPC request * @param timeout - Timeout in milliseconds * @returns Promise resolving to the result */ async sendRequestWithTimeout(request, timeout) { const promise = this.createPendingRequest(request, timeout); this.emit('request', request); await this.transport.send(JSON.stringify(request)); return promise; } /** * 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); }); } /** * Create a JSON-RPC request * @param method - Method name * @param params - Method parameters * @param id - Optional request ID * @returns JSON-RPC request object */ createRequest(method, params, id) { const request = { jsonrpc: types_1.JSONRPC_VERSION, method, id: id ?? this.generateId() }; if (params !== undefined) { request.params = params; } return request; } /** * Create a JSON-RPC notification * @param method - Method name * @param params - Method parameters * @returns JSON-RPC notification object */ createNotification(method, params) { const notification = { jsonrpc: types_1.JSONRPC_VERSION, method }; if (params !== undefined) { notification.params = params; } return notification; } /** * Generate a unique request ID * @returns Unique request ID */ generateId() { if (this.config.sequentialIds) { this.requestIdCounter += 1; return this.requestIdCounter; } return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } /** * Check if response is a success response * @param response - JSON-RPC response * @returns True if success response */ isSuccessResponse(response) { return 'result' in response; } /** * Check if an error should not be retried * @param error - Error to check * @returns True if error should not be retried */ shouldNotRetry(error) { // Don't retry on method not found, invalid params, or parse errors const errorCode = error.code; return (errorCode === types_1.JsonRpcErrorCode.methodNotFound || errorCode === types_1.JsonRpcErrorCode.invalidParams || errorCode === types_1.JsonRpcErrorCode.parseError || errorCode === types_1.JsonRpcErrorCode.invalidRequest || errorCode === types_1.JsonRpcErrorCode.internalError); } /** * Delay execution for specified milliseconds * @param ms - Delay in milliseconds * @returns Promise that resolves after delay */ async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Register event listener * @param event - Event name * @param listener - Event listener */ on(event, listener) { this.listeners[event] = listener; } /** * Remove event listener * @param event - Event name * @param listener - Event listener */ off(event, listener) { if (this.listeners[event] === listener) { Reflect.deleteProperty(this.listeners, event); } } /** * Emit event * @param event - Event name * @param args - Event arguments */ emit(event, ...args) { const listener = this.listeners[event]; if (listener) { listener(...args); } } /** * Get client statistics * @returns Client statistics */ getStats() { return { pendingRequests: this.pendingRequests.size, totalRequests: this.requestIdCounter, config: { ...this.config } }; } /** * Cancel all pending requests */ cancelAllRequests() { const error = new Error('Request cancelled'); for (const [id, pendingRequest] of this.pendingRequests) { pendingRequest.reject(error); } this.pendingRequests.clear(); } /** * Cancel a specific request * @param id - Request ID to cancel * @returns True if request was cancelled */ cancelRequest(id) { const pendingRequest = this.pendingRequests.get(id); if (pendingRequest) { pendingRequest.reject(new Error('Request cancelled')); this.pendingRequests.delete(id); return true; } return false; } /** * Close the client and cleanup resources */ async close() { this.cancelAllRequests(); if (this.transport) { await this.transport.close(); } } } exports.JsonRpcClient = JsonRpcClient; //# sourceMappingURL=JsonRpcClient.js.map