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