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