UNPKG

json-rpc-dual-engine

Version:

JSON-RPC-2.0 client and server protocol-agnostic engine.

122 lines (121 loc) 4.59 kB
import { JsonRpcRequest } from './json-rpc-request.js'; import { JsonRpcError } from './json-rpc-error.js'; export class JsonRpcServer { handler; constructor(handler, { transport, logger = console.error, } = {}) { this.handler = handler; this.transport = transport; this.logger = logger; } transport; logger; async accept(message) { try { await this.#accept(message); } catch (e) { if (e instanceof JsonRpcError) { await this.#respond(e.response); } else { throw e; } } } async #accept(message) { const request = JsonRpcRequest.parse(message); const method = this.#findApiMethod(request); const params = request.params === undefined ? [] : Array.isArray(request.params) ? request.params : [request.params]; const result = await (async () => { try { return (await method.apply(this.handler, params)) ?? null; } catch (e) { throw this.#buildUserError(e, request.id); } })(); if ('id' in request && request.id !== undefined) { await this.#respondSuccess(result, request.id); } } #findApiMethod(request, subject = this.handler, methodName = '') { // TODO Create an interface on method registration so that the user can optionally define the method parameters' // types and we can validate them here. Invalid argument error has error code -32602 // Alternatively, should use JSON schema to validate the request params. methodName ||= request.method; if (!methodName.startsWith('_')) { if (typeof subject[methodName] === 'function') { return subject[methodName]; } const [_fullName, firstPart, rest] = methodName.match(/^([^.]+)\.(.+)/) ?? []; if (!!firstPart && typeof subject[firstPart] === 'object' && subject[firstPart] !== null) { return this.#findApiMethod(request, subject[firstPart], rest); } } throw new JsonRpcError({ jsonrpc: '2.0', error: { code: -32601, message: `Requested method does not exist in the server.`, data: { method: request.method }, }, id: request.id ?? null, }); } async #respondSuccess(result, id) { await this.#respond({ jsonrpc: '2.0', result, id }); } async #respond(response) { const responseStr = (() => { try { return JSON.stringify(response); } catch (e) { throw this.#buildUserError(e, response.id); } })(); this.transport?.(responseStr); } #buildUserError(error, id) { const timestamp = new Date().toISOString(); this.logger?.(`An error occured while processing a user request. ${JSON.stringify({ cause: { timestamp, id, error } })}`); if (error instanceof JsonRpcError) { return error; } if (typeof error === 'object' && error !== null && 'jsonrpc' in error && error.jsonrpc === '2.0') { return new JsonRpcError(error); } return new JsonRpcError({ jsonrpc: '2.0', error: { // TODO Create an error registration interface so that the user can define specific error codes for // each type of error that the server can throw. code: -32000, message: 'An error occured on the server while processing the request.', data: error instanceof Error ? { type: error.constructor.name, message: error.message, cause: error.cause, stack: error.stack, } : { error: String(error) }, }, id: id ?? null, }); } toStream() { let localTransport = undefined; return new TransformStream({ start: controller => localTransport = this.transport = message => controller.enqueue(message), transform: chunk => this.accept(chunk), flush: () => { if (this.transport === localTransport) { this.transport = undefined; } }, }); } }