@asyncswap/jsonrpc 0.3.1
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/CHANGELOG.md +23 -0
- package/README.md +52 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +57 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +66 -0
- package/dist/server.js.map +1 -0
- package/example.ts +30 -0
- package/package.json +41 -0
- package/src/client.ts +68 -0
- package/src/index.ts +2 -0
- package/src/server.ts +106 -0
- package/src/types/base.d.ts +5 -0
- package/src/types/request.d.ts +10 -0
- package/src/types/response.d.ts +24 -0
- package/tsconfig.json +40 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @asyncswap/jsonrpc
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1c3a46e: fix example
|
|
8
|
+
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- dc1718c: refactor of types dir to global types
|
|
14
|
+
- 00ba692: - Converts error codes as enums
|
|
15
|
+
- Uses generic types on rpc request and response methods, and results
|
|
16
|
+
- 07dc4e5: improve docs example and updates to build config
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- e15733c: update scripts and make changeset in monorepo
|
|
21
|
+
- e3ac747: patch
|
|
22
|
+
- abddcdc: add source map to typescript build build
|
|
23
|
+
- ded3e6b: update
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# jsonrpc
|
|
2
|
+
|
|
3
|
+
## `@asyncswap/jsonrpc`
|
|
4
|
+
|
|
5
|
+
A minimal jsonrpc server and client library.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @asyncswap/jsonrpc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### RPC Server
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { JsonRpcServer } from "@asyncswap/jsonrpc";
|
|
17
|
+
|
|
18
|
+
const server = new JsonRpcServer()
|
|
19
|
+
|
|
20
|
+
server.register("add", ([a,b]: [number, number]) => a + b)
|
|
21
|
+
server.register("ping", () => "pong")
|
|
22
|
+
|
|
23
|
+
Bun.serve({
|
|
24
|
+
port: 4444,
|
|
25
|
+
async fetch(req) {
|
|
26
|
+
if (req.method !== "POST") {
|
|
27
|
+
return new Response("JSON-RPC only", { status: 405 })
|
|
28
|
+
}
|
|
29
|
+
const payload = await req.json();
|
|
30
|
+
const response = await server.handle(payload);
|
|
31
|
+
if (!response) {
|
|
32
|
+
return new Response(null, { status: 204 })
|
|
33
|
+
}
|
|
34
|
+
return Response.json(response);
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
console.log("JSON-RPC running on http://localhost:4444")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### RPC Client
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { initializeRpcClient } from "@asyncswap/jsonrpc";
|
|
45
|
+
const url = "http://localhost:4444";
|
|
46
|
+
const client = initializeRpcClient(url);
|
|
47
|
+
const result = await client.call(
|
|
48
|
+
"ping",
|
|
49
|
+
[]
|
|
50
|
+
);
|
|
51
|
+
console.log(result)
|
|
52
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class JsonRpcClient {
|
|
2
|
+
private send;
|
|
3
|
+
private id;
|
|
4
|
+
constructor(send: (req: JsonRpcRequest<unknown>) => Promise<JsonRpcResponse<unknown, number>>);
|
|
5
|
+
call<Method = string, Result = unknown, E = unknown>(method: Method, params?: unknown[]): Promise<Result | E>;
|
|
6
|
+
notify<Method = string>(method: Method, params?: unknown[]): Promise<JsonRpcResponse<unknown, number>>;
|
|
7
|
+
}
|
|
8
|
+
export declare function initializeRpcClient(url: string, jwtToken?: string, timeout?: number): JsonRpcClient;
|
|
9
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAa;IAIxB,OAAO,CAAC,IAAI;IAHb,OAAO,CAAC,EAAE,CAAK;gBAGN,IAAI,EAAE,CACb,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,KACxB,OAAO,CAAC,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAGzC,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EACxD,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,OAAO,EAAE,GAChB,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IActB,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE;CAQ1D;AAED,wBAAgB,mBAAmB,CAClC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,GAAE,MAAa,GACpB,aAAa,CA2Bf"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class JsonRpcClient {
|
|
2
|
+
send;
|
|
3
|
+
id = 0;
|
|
4
|
+
constructor(send) {
|
|
5
|
+
this.send = send;
|
|
6
|
+
}
|
|
7
|
+
async call(method, params) {
|
|
8
|
+
const request = {
|
|
9
|
+
jsonrpc: "2.0",
|
|
10
|
+
method,
|
|
11
|
+
params,
|
|
12
|
+
id: ++this.id,
|
|
13
|
+
};
|
|
14
|
+
const response = await this.send(request);
|
|
15
|
+
if ("error" in response) {
|
|
16
|
+
return response.error;
|
|
17
|
+
}
|
|
18
|
+
return response.result;
|
|
19
|
+
}
|
|
20
|
+
notify(method, params) {
|
|
21
|
+
const request = {
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
method,
|
|
24
|
+
params,
|
|
25
|
+
};
|
|
26
|
+
return this.send(request);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function initializeRpcClient(url, jwtToken, timeout = 5000) {
|
|
30
|
+
const client = new JsonRpcClient(async (req) => {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
33
|
+
try {
|
|
34
|
+
const headers = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
};
|
|
37
|
+
if (jwtToken) {
|
|
38
|
+
headers["Authorization"] = `Bearer ${jwtToken}`;
|
|
39
|
+
}
|
|
40
|
+
const res = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers,
|
|
43
|
+
body: JSON.stringify(req),
|
|
44
|
+
});
|
|
45
|
+
return (await res.json());
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
50
|
+
throw new Error(`Req timeout after ${timeout}ms`);
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return client;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,aAAa;IAIhB;IAHD,EAAE,GAAG,CAAC,CAAC;IAEf,YACS,IAEsC;QAFtC,SAAI,GAAJ,IAAI,CAEkC;IAC3C,CAAC;IAEL,KAAK,CAAC,IAAI,CACT,MAAc,EACd,MAAkB;QAElB,MAAM,OAAO,GAA2B;YACvC,OAAO,EAAE,KAAK;YACd,MAAM;YACN,MAAM;YACN,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE;SACb,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;YACzB,OAAO,QAAQ,CAAC,KAAU,CAAC;QAC5B,CAAC;QACD,OAAO,QAAQ,CAAC,MAAgB,CAAC;IAClC,CAAC;IAED,MAAM,CAAkB,MAAc,EAAE,MAAkB;QACzD,MAAM,OAAO,GAA2B;YACvC,OAAO,EAAE,KAAK;YACd,MAAM;YACN,MAAM;SACN,CAAC;QACF,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;CACD;AAED,MAAM,UAAU,mBAAmB,CAClC,GAAW,EACX,QAAiB,EACjB,UAAkB,IAAI;IAEtB,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,KAAK,EAAE,GAA4B,EAAE,EAAE;QACvE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC;YACJ,MAAM,OAAO,GAA2B;gBACvC,cAAc,EAAE,kBAAkB;aAClC,CAAC;YACF,IAAI,QAAQ,EAAE,CAAC;gBACd,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,QAAQ,EAAE,CAAC;YACjD,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC5B,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;aACzB,CAAC,CAAC;YAEH,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAqC,CAAC;QAC/D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBACvD,MAAM,IAAI,KAAK,CAAC,qBAAqB,OAAO,IAAI,CAAC,CAAC;YACnD,CAAC;YACD,MAAM,GAAG,CAAC;QACX,CAAC;IACF,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AACf,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare enum JsonRpcErrorCodes {
|
|
2
|
+
INVALID_REQUEST = -32600,
|
|
3
|
+
METHOD_NOT_FOUND = -32601,
|
|
4
|
+
INVALID_PARAMS = -32602,
|
|
5
|
+
INTERNAL_ERROR = -32603,
|
|
6
|
+
PARSE_ERROR = -32700,
|
|
7
|
+
REQUEST_ABORTED = -32800
|
|
8
|
+
}
|
|
9
|
+
export type Handler<Result> = (params: any) => any | Promise<Result>;
|
|
10
|
+
export declare class JsonRpcServer {
|
|
11
|
+
private methods;
|
|
12
|
+
register(method: string, handler: Handler<any>): void;
|
|
13
|
+
handle<Result, Method = string>(raw: unknown): Promise<JsonRpcResponse<Result, JsonRpcErrorCodes | number> | JsonRpcResponse<Result, JsonRpcErrorCodes | number>[] | null>;
|
|
14
|
+
private error;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,oBAAY,iBAAiB;IAC5B,eAAe,SAAS;IACxB,gBAAgB,SAAS;IACzB,cAAc,SAAS;IACvB,cAAc,SAAS;IACvB,WAAW,SAAS;IACpB,eAAe,SAAS;CACxB;AACD,MAAM,MAAM,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAErE,qBAAa,aAAa;IACzB,OAAO,CAAC,OAAO,CAAmC;IAElD,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC;IAIxC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EACnC,GAAG,EAAE,OAAO,GACV,OAAO,CACP,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAAC,GACnD,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAAC,EAAE,GACrD,IAAI,CACN;IAsED,OAAO,CAAC,KAAK;CAYb"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export var JsonRpcErrorCodes;
|
|
2
|
+
(function (JsonRpcErrorCodes) {
|
|
3
|
+
JsonRpcErrorCodes[JsonRpcErrorCodes["INVALID_REQUEST"] = -32600] = "INVALID_REQUEST";
|
|
4
|
+
JsonRpcErrorCodes[JsonRpcErrorCodes["METHOD_NOT_FOUND"] = -32601] = "METHOD_NOT_FOUND";
|
|
5
|
+
JsonRpcErrorCodes[JsonRpcErrorCodes["INVALID_PARAMS"] = -32602] = "INVALID_PARAMS";
|
|
6
|
+
JsonRpcErrorCodes[JsonRpcErrorCodes["INTERNAL_ERROR"] = -32603] = "INTERNAL_ERROR";
|
|
7
|
+
JsonRpcErrorCodes[JsonRpcErrorCodes["PARSE_ERROR"] = -32700] = "PARSE_ERROR";
|
|
8
|
+
JsonRpcErrorCodes[JsonRpcErrorCodes["REQUEST_ABORTED"] = -32800] = "REQUEST_ABORTED";
|
|
9
|
+
})(JsonRpcErrorCodes || (JsonRpcErrorCodes = {}));
|
|
10
|
+
export class JsonRpcServer {
|
|
11
|
+
methods = new Map();
|
|
12
|
+
register(method, handler) {
|
|
13
|
+
this.methods.set(method, handler);
|
|
14
|
+
}
|
|
15
|
+
async handle(raw) {
|
|
16
|
+
// handle batch
|
|
17
|
+
if (Array.isArray(raw)) {
|
|
18
|
+
if (raw.length === 0) {
|
|
19
|
+
return this.error(null, JsonRpcErrorCodes.INVALID_REQUEST, "Invalid request");
|
|
20
|
+
}
|
|
21
|
+
const responses = await Promise.all(raw.map((item) => this.handle(item)));
|
|
22
|
+
const filtered = responses.filter((r) => r !== null);
|
|
23
|
+
return filtered.length > 0 ? filtered : null;
|
|
24
|
+
}
|
|
25
|
+
// handle single response
|
|
26
|
+
const req = raw;
|
|
27
|
+
if (typeof req !== "object" ||
|
|
28
|
+
req === null ||
|
|
29
|
+
req.jsonrpc !== "2.0" ||
|
|
30
|
+
typeof req.method !== "string") {
|
|
31
|
+
return this.error(null, JsonRpcErrorCodes.INVALID_REQUEST, "Invalid request");
|
|
32
|
+
}
|
|
33
|
+
const id = typeof req?.id === "string" ||
|
|
34
|
+
typeof req?.id === "number" ||
|
|
35
|
+
req?.id === null
|
|
36
|
+
? req.id
|
|
37
|
+
: null;
|
|
38
|
+
const handler = this.methods.get(req.method);
|
|
39
|
+
if (!handler) {
|
|
40
|
+
return id === null
|
|
41
|
+
? null
|
|
42
|
+
: this.error(req.id, JsonRpcErrorCodes.METHOD_NOT_FOUND, "Method not found");
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const result = await handler(req.params);
|
|
46
|
+
if (req.id === undefined)
|
|
47
|
+
return null; // notification
|
|
48
|
+
return {
|
|
49
|
+
jsonrpc: "2.0",
|
|
50
|
+
result,
|
|
51
|
+
id: req.id,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return this.error(req.id ?? null, JsonRpcErrorCodes.INTERNAL_ERROR, "Internal error", err instanceof Error ? err.message : err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
error(id, code, message, data) {
|
|
59
|
+
return {
|
|
60
|
+
jsonrpc: "2.0",
|
|
61
|
+
error: { code, message, data },
|
|
62
|
+
id: id ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/example.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { JsonRpcServer } from "./src";
|
|
2
|
+
|
|
3
|
+
const server = new JsonRpcServer();
|
|
4
|
+
|
|
5
|
+
server.register("add", ([a, b]: [number, number]) => a + b);
|
|
6
|
+
server.register("ping", () => "pong");
|
|
7
|
+
|
|
8
|
+
Bun.serve({
|
|
9
|
+
port: 4444,
|
|
10
|
+
async fetch(req) {
|
|
11
|
+
if (req.method !== "POST") {
|
|
12
|
+
return new Response("JSON-RPC only", { status: 405 });
|
|
13
|
+
}
|
|
14
|
+
const payload = await req.json();
|
|
15
|
+
const response = await server.handle(payload);
|
|
16
|
+
if (!response) {
|
|
17
|
+
return new Response(null, { status: 204 });
|
|
18
|
+
}
|
|
19
|
+
return Response.json(response);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
console.log("JSON-RPC running on http://localhost:4444");
|
|
24
|
+
|
|
25
|
+
import { initializeRpcClient } from "./src";
|
|
26
|
+
|
|
27
|
+
const url = "http://localhost:4444";
|
|
28
|
+
const client = initializeRpcClient(url);
|
|
29
|
+
const result = await client.call("ping", []);
|
|
30
|
+
console.log(result);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asyncswap/jsonrpc",
|
|
3
|
+
"description": "A minimal jsonrpc spec implementation.",
|
|
4
|
+
"author": "Meek Msaki",
|
|
5
|
+
"version": "0.3.1",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.mjs",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"jsonrpc",
|
|
12
|
+
"2.0",
|
|
13
|
+
"rpc",
|
|
14
|
+
"bun"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/asyncswap/eth-libs/tree/main/packages/jsonrpc",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/asyncswap/eth-libs.git",
|
|
20
|
+
"directory": "packages/jsonrpc"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/asyncswap/eth-libs/issues"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"directory": "dist"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"publish": "bun run build && bun publish --access public",
|
|
32
|
+
"format": "bun biome format --write"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "2.3.11",
|
|
36
|
+
"@types/bun": "latest"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"typescript": "^5"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export class JsonRpcClient {
|
|
2
|
+
private id = 0;
|
|
3
|
+
|
|
4
|
+
constructor(
|
|
5
|
+
private send: (
|
|
6
|
+
req: JsonRpcRequest<unknown>,
|
|
7
|
+
) => Promise<JsonRpcResponse<unknown, number>>,
|
|
8
|
+
) { }
|
|
9
|
+
|
|
10
|
+
async call<Method = string, Result = unknown, E = unknown>(
|
|
11
|
+
method: Method,
|
|
12
|
+
params?: unknown[],
|
|
13
|
+
): Promise<Result | E> {
|
|
14
|
+
const request: JsonRpcRequest<Method> = {
|
|
15
|
+
jsonrpc: "2.0",
|
|
16
|
+
method,
|
|
17
|
+
params,
|
|
18
|
+
id: ++this.id,
|
|
19
|
+
};
|
|
20
|
+
const response = await this.send(request);
|
|
21
|
+
if ("error" in response) {
|
|
22
|
+
return response.error as E;
|
|
23
|
+
}
|
|
24
|
+
return response.result as Result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
notify<Method = string>(method: Method, params?: unknown[]) {
|
|
28
|
+
const request: JsonRpcRequest<Method> = {
|
|
29
|
+
jsonrpc: "2.0",
|
|
30
|
+
method,
|
|
31
|
+
params,
|
|
32
|
+
};
|
|
33
|
+
return this.send(request);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function initializeRpcClient(
|
|
38
|
+
url: string,
|
|
39
|
+
jwtToken?: string,
|
|
40
|
+
timeout: number = 5000,
|
|
41
|
+
): JsonRpcClient {
|
|
42
|
+
const client = new JsonRpcClient(async (req: JsonRpcRequest<unknown>) => {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
45
|
+
try {
|
|
46
|
+
const headers: Record<string, string> = {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
};
|
|
49
|
+
if (jwtToken) {
|
|
50
|
+
headers["Authorization"] = `Bearer ${jwtToken}`;
|
|
51
|
+
}
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers,
|
|
55
|
+
body: JSON.stringify(req),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return (await res.json()) as JsonRpcResponse<unknown, number>;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
62
|
+
throw new Error(`Req timeout after ${timeout}ms`);
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return client;
|
|
68
|
+
}
|
package/src/index.ts
ADDED
package/src/server.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export enum JsonRpcErrorCodes {
|
|
2
|
+
INVALID_REQUEST = -32600,
|
|
3
|
+
METHOD_NOT_FOUND = -32601,
|
|
4
|
+
INVALID_PARAMS = -32602,
|
|
5
|
+
INTERNAL_ERROR = -32603,
|
|
6
|
+
PARSE_ERROR = -32700,
|
|
7
|
+
REQUEST_ABORTED = -32800,
|
|
8
|
+
}
|
|
9
|
+
export type Handler<Result> = (params: any) => any | Promise<Result>;
|
|
10
|
+
|
|
11
|
+
export class JsonRpcServer {
|
|
12
|
+
private methods = new Map<string, Handler<any>>();
|
|
13
|
+
|
|
14
|
+
register(method: string, handler: Handler<any>) {
|
|
15
|
+
this.methods.set(method, handler);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async handle<Result, Method = string>(
|
|
19
|
+
raw: unknown,
|
|
20
|
+
): Promise<
|
|
21
|
+
| JsonRpcResponse<Result, JsonRpcErrorCodes | number>
|
|
22
|
+
| JsonRpcResponse<Result, JsonRpcErrorCodes | number>[]
|
|
23
|
+
| null
|
|
24
|
+
> {
|
|
25
|
+
// handle batch
|
|
26
|
+
if (Array.isArray(raw)) {
|
|
27
|
+
if (raw.length === 0) {
|
|
28
|
+
return this.error(
|
|
29
|
+
null,
|
|
30
|
+
JsonRpcErrorCodes.INVALID_REQUEST,
|
|
31
|
+
"Invalid request",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const responses = await Promise.all(raw.map((item) => this.handle(item)));
|
|
35
|
+
const filtered = responses.filter(
|
|
36
|
+
(r): r is JsonRpcResponse<Result, JsonRpcErrorCodes | number> =>
|
|
37
|
+
r !== null,
|
|
38
|
+
);
|
|
39
|
+
return filtered.length > 0 ? filtered : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// handle single response
|
|
43
|
+
const req = raw as Partial<JsonRpcRequest<Method>>;
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
typeof req !== "object" ||
|
|
47
|
+
req === null ||
|
|
48
|
+
req.jsonrpc !== "2.0" ||
|
|
49
|
+
typeof req.method !== "string"
|
|
50
|
+
) {
|
|
51
|
+
return this.error(
|
|
52
|
+
null,
|
|
53
|
+
JsonRpcErrorCodes.INVALID_REQUEST,
|
|
54
|
+
"Invalid request",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const id =
|
|
59
|
+
typeof (req as any)?.id === "string" ||
|
|
60
|
+
typeof (req as any)?.id === "number" ||
|
|
61
|
+
(req as any)?.id === null
|
|
62
|
+
? (req as any).id
|
|
63
|
+
: null;
|
|
64
|
+
|
|
65
|
+
const handler = this.methods.get(req.method);
|
|
66
|
+
if (!handler) {
|
|
67
|
+
return id === null
|
|
68
|
+
? null
|
|
69
|
+
: this.error(
|
|
70
|
+
req.id,
|
|
71
|
+
JsonRpcErrorCodes.METHOD_NOT_FOUND,
|
|
72
|
+
"Method not found",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await handler(req.params);
|
|
78
|
+
if (req.id === undefined) return null; // notification
|
|
79
|
+
return {
|
|
80
|
+
jsonrpc: "2.0",
|
|
81
|
+
result,
|
|
82
|
+
id: req.id,
|
|
83
|
+
};
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return this.error(
|
|
86
|
+
req.id ?? null,
|
|
87
|
+
JsonRpcErrorCodes.INTERNAL_ERROR,
|
|
88
|
+
"Internal error",
|
|
89
|
+
err instanceof Error ? err.message : err,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private error<Result, Method = string>(
|
|
95
|
+
id: JsonRpcRequest<Method>["id"],
|
|
96
|
+
code: JsonRpcErrorCodes | number,
|
|
97
|
+
message: string,
|
|
98
|
+
data?: unknown,
|
|
99
|
+
): JsonRpcResponse<Result, JsonRpcErrorCodes | number> {
|
|
100
|
+
return {
|
|
101
|
+
jsonrpc: "2.0",
|
|
102
|
+
error: { code, message, data },
|
|
103
|
+
id: id ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
export interface JsonRpcSuccess<Result> {
|
|
3
|
+
jsonrpc: "2.0";
|
|
4
|
+
result: Result;
|
|
5
|
+
id: JsonRpcId;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface JsonRpcError<ErrorCode> {
|
|
9
|
+
jsonrpc: "2.0";
|
|
10
|
+
error: JsonRpcErrorObject<ErrorCode>;
|
|
11
|
+
id: JsonRpcId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JsonRpcErrorObject<ErrorCode> {
|
|
15
|
+
code: ErrorCode;
|
|
16
|
+
message: string;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export type JsonRpcResponse<Result, ErrorCode> =
|
|
20
|
+
| JsonRpcSuccess<Result>
|
|
21
|
+
| JsonRpcError<ErrorCode>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { };
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": [
|
|
5
|
+
"ESNext"
|
|
6
|
+
],
|
|
7
|
+
"target": "ESNext",
|
|
8
|
+
"module": "Preserve",
|
|
9
|
+
"moduleDetection": "force",
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
// Bundler mode
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"moduleResolution": "bundler",
|
|
17
|
+
"outDir": "dist",
|
|
18
|
+
"rootDir": "src",
|
|
19
|
+
"allowImportingTsExtensions": false,
|
|
20
|
+
"verbatimModuleSyntax": true,
|
|
21
|
+
"noEmit": false,
|
|
22
|
+
// Best practices
|
|
23
|
+
"strict": true,
|
|
24
|
+
"skipLibCheck": true,
|
|
25
|
+
"noFallthroughCasesInSwitch": true,
|
|
26
|
+
"noUncheckedIndexedAccess": true,
|
|
27
|
+
"noImplicitOverride": true,
|
|
28
|
+
// Some stricter flags (disabled by default)
|
|
29
|
+
"noUnusedLocals": false,
|
|
30
|
+
"noUnusedParameters": false,
|
|
31
|
+
"noPropertyAccessFromIndexSignature": false
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"src/**/*",
|
|
35
|
+
"global.d.ts"
|
|
36
|
+
],
|
|
37
|
+
"exclude": [
|
|
38
|
+
"example.ts"
|
|
39
|
+
]
|
|
40
|
+
}
|