json-rpc-dual-engine
Version:
JSON-RPC-2.0 client and server protocol-agnostic engine.
111 lines (110 loc) • 4.26 kB
JavaScript
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 };
}
}