@nym.sh/jrpc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @nym.sh/jrpc
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
@@ -0,0 +1,9 @@
1
+ import type { JrpcChannel } from "./transport";
2
+ type JsonRpcHandler = (...params: any[]) => any;
3
+ type JsonRpcMethod<Schema> = Extract<keyof Schema, string>;
4
+ export declare class JsonRpcClient<Schema extends Record<keyof Schema, JsonRpcHandler>> {
5
+ private dispatcher;
6
+ constructor(channel: JrpcChannel);
7
+ call<M extends JsonRpcMethod<Schema>>(method: M, ...params: Parameters<Schema[M]>): Promise<Awaited<ReturnType<Schema[M]>>>;
8
+ }
9
+ export {};
@@ -0,0 +1,20 @@
1
+ export declare const JsonRpcErrorCode: {
2
+ readonly ParseError: -32700;
3
+ readonly InvalidRequest: -32600;
4
+ readonly MethodNotFound: -32601;
5
+ readonly InvalidParams: -32602;
6
+ readonly InternalError: -32603;
7
+ readonly ServerError: -32000;
8
+ };
9
+ export type JsonRpcErrorCode = (typeof JsonRpcErrorCode)[keyof typeof JsonRpcErrorCode];
10
+ export interface JsonRpcErrorObject {
11
+ code: number;
12
+ message: string;
13
+ data?: unknown;
14
+ }
15
+ export declare class JsonRpcRemoteError extends Error {
16
+ code: number;
17
+ data?: unknown;
18
+ constructor(error: JsonRpcErrorObject);
19
+ }
20
+ export declare function toJsonRpcError(error: unknown): JsonRpcErrorObject;
@@ -0,0 +1,19 @@
1
+ import type { JsonRpcErrorCode as JsonRpcErrorCodeValue } from "./error";
2
+ export { JsonRpcClient } from "./client";
3
+ export { JsonRpcRemoteError } from "./error";
4
+ export { JsonRpcServer } from "./server";
5
+ export declare const JsonRpcErrorCode: {
6
+ readonly ParseError: -32700;
7
+ readonly InvalidRequest: -32600;
8
+ readonly MethodNotFound: -32601;
9
+ readonly InvalidParams: -32602;
10
+ readonly InternalError: -32603;
11
+ readonly ServerError: -32000;
12
+ };
13
+ export type JsonRpcErrorCode = JsonRpcErrorCodeValue;
14
+ export type { JsonRpcErrorObject } from "./error";
15
+ export type { JrpcChannel, JrpcMessage, JsonRpcErrorResponse, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, JsonRpcSuccessResponse, JsonRpcTransport, } from "./transport";
16
+ export interface TestSchema {
17
+ sendMessage(content: string): void;
18
+ test(a: number, b: string): void;
19
+ }
package/dist/index.js ADDED
@@ -0,0 +1,283 @@
1
+ // error.ts
2
+ var JsonRpcErrorCode = {
3
+ ParseError: -32700,
4
+ InvalidRequest: -32600,
5
+ MethodNotFound: -32601,
6
+ InvalidParams: -32602,
7
+ InternalError: -32603,
8
+ ServerError: -32000
9
+ };
10
+
11
+ class JsonRpcRemoteError extends Error {
12
+ code;
13
+ data;
14
+ constructor(error) {
15
+ super(error.message);
16
+ this.name = "JsonRpcRemoteError";
17
+ this.code = error.code;
18
+ this.data = error.data;
19
+ }
20
+ }
21
+ function toJsonRpcError(error) {
22
+ if (error instanceof Error) {
23
+ return {
24
+ code: JsonRpcErrorCode.ServerError,
25
+ message: error.message
26
+ };
27
+ }
28
+ return {
29
+ code: JsonRpcErrorCode.ServerError,
30
+ message: String(error)
31
+ };
32
+ }
33
+
34
+ // transport.ts
35
+ var dispatchers = new WeakMap;
36
+
37
+ class JsonRpcDispatcher {
38
+ channel;
39
+ activeRequests = new Set;
40
+ fatalHandlerError;
41
+ hasFatalHandlerError = false;
42
+ unresolvedIds = new Set;
43
+ pending = new Map;
44
+ requestHandler;
45
+ running;
46
+ isClosed = false;
47
+ closeReason;
48
+ constructor(channel) {
49
+ this.channel = channel;
50
+ }
51
+ setRequestHandler(handler) {
52
+ if (this.requestHandler) {
53
+ throw new Error("JSON-RPC channel already has a request handler.");
54
+ }
55
+ this.requestHandler = handler;
56
+ }
57
+ send(msg) {
58
+ return this.channel.send(msg);
59
+ }
60
+ start() {
61
+ if (!this.running) {
62
+ this.running = this.listen();
63
+ this.running.catch(() => {
64
+ return;
65
+ });
66
+ }
67
+ return this.running;
68
+ }
69
+ async call(method, params) {
70
+ if (this.isClosed) {
71
+ throw this.closeReason;
72
+ }
73
+ const id = this.createId();
74
+ const response = new Promise((resolve, reject) => {
75
+ this.pending.set(id, {
76
+ resolve,
77
+ reject
78
+ });
79
+ });
80
+ this.start();
81
+ try {
82
+ await this.channel.send({
83
+ jsonrpc: "2.0",
84
+ id,
85
+ method,
86
+ params
87
+ });
88
+ } catch (error) {
89
+ this.pending.delete(id);
90
+ this.unresolvedIds.delete(id);
91
+ throw error;
92
+ }
93
+ return response;
94
+ }
95
+ createId() {
96
+ let id;
97
+ do {
98
+ id = randomJsonRpcId();
99
+ } while (this.unresolvedIds.has(id));
100
+ this.unresolvedIds.add(id);
101
+ return id;
102
+ }
103
+ async listen() {
104
+ let listenerError;
105
+ let hasListenerError = false;
106
+ try {
107
+ for await (const msg of this.channel) {
108
+ if (isJsonRpcRequest(msg)) {
109
+ this.handleRequestInBackground(msg);
110
+ continue;
111
+ }
112
+ if (isJsonRpcResponse(msg)) {
113
+ this.handleResponse(msg);
114
+ }
115
+ }
116
+ } catch (error) {
117
+ hasListenerError = true;
118
+ listenerError = error;
119
+ this.close(error);
120
+ } finally {
121
+ this.close(new Error("JSON-RPC channel closed."));
122
+ await this.waitForActiveRequests();
123
+ }
124
+ if (hasListenerError) {
125
+ throw listenerError;
126
+ }
127
+ if (this.hasFatalHandlerError) {
128
+ throw this.fatalHandlerError;
129
+ }
130
+ }
131
+ handleRequestInBackground(msg) {
132
+ const request = this.handleRequest(msg);
133
+ this.activeRequests.add(request);
134
+ request.catch((error) => {
135
+ this.hasFatalHandlerError = true;
136
+ this.fatalHandlerError = error;
137
+ this.close(error);
138
+ this.channel.return(undefined).catch(() => {
139
+ return;
140
+ });
141
+ }).finally(() => {
142
+ this.activeRequests.delete(request);
143
+ });
144
+ }
145
+ async waitForActiveRequests() {
146
+ while (this.activeRequests.size > 0) {
147
+ await Promise.allSettled(this.activeRequests);
148
+ }
149
+ }
150
+ handleResponse(msg) {
151
+ const pending = this.pending.get(msg.id);
152
+ if (!pending) {
153
+ return;
154
+ }
155
+ this.pending.delete(msg.id);
156
+ this.unresolvedIds.delete(msg.id);
157
+ if (isJsonRpcErrorResponse(msg)) {
158
+ pending.reject(new JsonRpcRemoteError(msg.error));
159
+ return;
160
+ }
161
+ pending.resolve(msg.result);
162
+ }
163
+ async handleRequest(msg) {
164
+ if (this.requestHandler) {
165
+ await this.requestHandler(msg);
166
+ return;
167
+ }
168
+ await this.channel.send({
169
+ jsonrpc: "2.0",
170
+ id: msg.id,
171
+ error: {
172
+ code: JsonRpcErrorCode.MethodNotFound,
173
+ message: `Method not found: ${msg.method}`
174
+ }
175
+ });
176
+ }
177
+ close(reason) {
178
+ if (this.isClosed) {
179
+ return;
180
+ }
181
+ this.isClosed = true;
182
+ this.closeReason = reason;
183
+ for (const pending of this.pending.values()) {
184
+ pending.reject(reason);
185
+ }
186
+ this.pending.clear();
187
+ this.unresolvedIds.clear();
188
+ }
189
+ }
190
+ function getDispatcher(channel) {
191
+ let dispatcher = dispatchers.get(channel);
192
+ if (!dispatcher) {
193
+ dispatcher = new JsonRpcDispatcher(channel);
194
+ dispatchers.set(channel, dispatcher);
195
+ }
196
+ return dispatcher;
197
+ }
198
+ function isJsonRpcRequest(msg) {
199
+ return Object.hasOwn(msg, "method");
200
+ }
201
+ function isJsonRpcResponse(msg) {
202
+ return Object.hasOwn(msg, "result") || Object.hasOwn(msg, "error");
203
+ }
204
+ function isJsonRpcErrorResponse(msg) {
205
+ return Object.hasOwn(msg, "error");
206
+ }
207
+ function randomJsonRpcId() {
208
+ const [id = 0] = crypto.getRandomValues(new Uint32Array(1));
209
+ return id;
210
+ }
211
+
212
+ // client.ts
213
+ class JsonRpcClient {
214
+ dispatcher;
215
+ constructor(channel) {
216
+ this.dispatcher = getDispatcher(channel);
217
+ }
218
+ async call(method, ...params) {
219
+ return this.dispatcher.call(method, params);
220
+ }
221
+ }
222
+ // server.ts
223
+ class JsonRpcServer {
224
+ handlers;
225
+ dispatcher;
226
+ constructor(handlers, channel) {
227
+ this.handlers = handlers;
228
+ this.dispatcher = getDispatcher(channel);
229
+ this.dispatcher.setRequestHandler((msg) => this.handle(msg));
230
+ }
231
+ start() {
232
+ return this.dispatcher.start();
233
+ }
234
+ async handle(msg) {
235
+ if (!Object.hasOwn(this.handlers, msg.method)) {
236
+ await this.dispatcher.send({
237
+ jsonrpc: "2.0",
238
+ id: msg.id,
239
+ error: {
240
+ code: JsonRpcErrorCode.MethodNotFound,
241
+ message: `Method not found: ${msg.method}`
242
+ }
243
+ });
244
+ return;
245
+ }
246
+ const handler = this.handlers[msg.method];
247
+ const params = msg.params ?? [];
248
+ if (!Array.isArray(params)) {
249
+ await this.dispatcher.send({
250
+ jsonrpc: "2.0",
251
+ id: msg.id,
252
+ error: {
253
+ code: JsonRpcErrorCode.InvalidParams,
254
+ message: "Params must be an array."
255
+ }
256
+ });
257
+ return;
258
+ }
259
+ try {
260
+ const result = await handler(...params);
261
+ await this.dispatcher.send({
262
+ jsonrpc: "2.0",
263
+ id: msg.id,
264
+ result: result ?? null
265
+ });
266
+ } catch (error) {
267
+ await this.dispatcher.send({
268
+ jsonrpc: "2.0",
269
+ id: msg.id,
270
+ error: toJsonRpcError(error)
271
+ });
272
+ }
273
+ }
274
+ }
275
+
276
+ // index.ts
277
+ var JsonRpcErrorCode2 = JsonRpcErrorCode;
278
+ export {
279
+ JsonRpcServer,
280
+ JsonRpcRemoteError,
281
+ JsonRpcErrorCode2 as JsonRpcErrorCode,
282
+ JsonRpcClient
283
+ };
@@ -0,0 +1,10 @@
1
+ import type { JrpcChannel } from "./transport";
2
+ type JsonRpcHandler = (...params: any[]) => any;
3
+ export declare class JsonRpcServer<Schema extends Record<keyof Schema, JsonRpcHandler>> {
4
+ private handlers;
5
+ private dispatcher;
6
+ constructor(handlers: Schema, channel: JrpcChannel);
7
+ start(): Promise<void>;
8
+ private handle;
9
+ }
10
+ export {};
@@ -0,0 +1,31 @@
1
+ import type { JsonRpcErrorObject } from "./error";
2
+ export interface JsonRpcRequest {
3
+ jsonrpc: "2.0";
4
+ id: number;
5
+ method: string;
6
+ params?: unknown[];
7
+ }
8
+ export interface JsonRpcSuccessResponse {
9
+ jsonrpc: "2.0";
10
+ id: number;
11
+ result: unknown;
12
+ }
13
+ export interface JsonRpcErrorResponse {
14
+ jsonrpc: "2.0";
15
+ id: number;
16
+ error: JsonRpcErrorObject;
17
+ }
18
+ export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
19
+ export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse;
20
+ export type JrpcMessage = JsonRpcMessage;
21
+ export interface JrpcChannel extends AsyncGenerator<JrpcMessage, void, unknown> {
22
+ send(msg: JsonRpcMessage): Promise<void>;
23
+ }
24
+ export type RequestHandler = (msg: JsonRpcRequest) => Promise<void>;
25
+ export interface JsonRpcTransport {
26
+ call(method: string, params: unknown[]): Promise<unknown>;
27
+ send(msg: JsonRpcMessage): Promise<void>;
28
+ setRequestHandler(handler: RequestHandler): void;
29
+ start(): Promise<void>;
30
+ }
31
+ export declare function getDispatcher(channel: JrpcChannel): JsonRpcTransport;
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@nym.sh/jrpc",
3
+ "version": "0.1.0",
4
+ "description": "Typed JSON-RPC over async channels.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/kennethnym/jrpc"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "type": "module",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "rm -rf dist && bun build index.ts --outdir=dist --target=node --format=esm && tsc -p tsconfig.build.json",
27
+ "fmt": "oxfmt",
28
+ "fmt:check": "oxfmt --check",
29
+ "lint": "oxlint",
30
+ "lint:fix": "oxlint --fix",
31
+ "test": "bun test",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "oxfmt": "^0.55.0",
37
+ "oxlint": "^1.70.0",
38
+ "typescript": "^6.0.3"
39
+ }
40
+ }