UNPKG

json-rpc-dual-engine

Version:

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

111 lines (110 loc) 4.26 kB
import { JsonRpcResponse } from './json-rpc-response.js'; import { nanoid } from 'nanoid'; export class JsonRpcClient { constructor({ transport, timeout = 10000, logger = console.error, } = {}) { this.timeout = timeout; this.transport = transport; this.logger = logger; } timeout; transport; logger; #pendingCalls = {}; #serverRequestProxy; #serverNotificationProxy; get remote() { return this.#serverRequestProxy ??= new Proxy({}, { get: (_target, method) => (...args) => { if (typeof method === 'symbol') { throw new Error('Failed to send json-rpc request. Cause: Invalid method name.', { cause: { method } }); } return this.sendRequest(method, args); } }); } get notifications() { return this.#serverNotificationProxy ??= new Proxy({}, { get: (_target, method) => (...args) => { if (typeof method === 'symbol') { throw new Error('Failed to send json-rpc request. Cause: Invalid method name.', { cause: { method } }); } this.sendNotification(method, args); } }); } buildRequest(method, params, { id = nanoid() } = {}) { const requestObj = { jsonrpc: '2.0', id, method, params }; try { return JSON.stringify(requestObj); } catch (cause) { throw new Error('Failed to send json-rpc request. Cause: Failed to serialize request params.', { cause }); } } async sendRequest(method, params) { if (!this.transport) { return Promise.reject(new Error('Failed to send json-rpc request. Cause: No transport function provided.')); } const id = nanoid(); const requestStr = this.buildRequest(method, params, { id }); const { promise, reject } = this.#pendingCalls[id] = Promise.withResolvers(); const timeoutId = setTimeout(() => reject(new Error('Request timed out. (the server did not respond in time)')), this.timeout); this.transport(requestStr); return promise.finally(() => { clearTimeout(timeoutId); delete this.#pendingCalls[id]; }); } sendNotification(method, params) { if (!this.transport) { return; } const requestObj = { jsonrpc: '2.0', method, params }; const requestStr = (() => { try { return JSON.stringify(requestObj); } catch (cause) { throw new Error('Failed to send json-rpc request. Cause: Failed to serialize request params.', { cause }); } })(); this.transport(requestStr); } accept(message) { try { this.#accept(message); } catch (cause) { throw new Error('Failed to handle json-rpc response. Cause: An error occurred while processing the response.', { cause }); } } #accept(message) { const response = JsonRpcResponse.parse(message); if (response.id === null || !(response.id in this.#pendingCalls)) { throw new Error('Failed to handle json-rpc response. Cause: Unexpected response id.', { cause: { id: response.id } }); } if (JsonRpcResponse.isError(response)) { this.#pendingCalls[response.id].reject(new Error('Server returned an error response.', { cause: response.error })); } if (JsonRpcResponse.isSuccess(response)) { this.#pendingCalls[response.id].resolve(response.result); } } toStream() { let localTransport = undefined; const writable = new WritableStream({ write: message => this.accept(message), }); const readable = new ReadableStream({ start: controller => { localTransport = this.transport = message => controller.enqueue(message); }, cancel: () => { if (this.transport === localTransport) { this.transport = undefined; } }, }); return { readable, writable }; } }