@c9up/comet 0.1.2 → 0.1.3
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 +8 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +41 -13
- package/dist/client.js.map +1 -1
- package/dist/protocol.d.ts +33 -6
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +58 -11
- package/dist/protocol.js.map +1 -1
- package/package.json +3 -2
- package/src/client.ts +47 -13
- package/src/protocol.ts +59 -11
package/README.md
CHANGED
|
@@ -41,3 +41,11 @@ const results = await rpc.batch([{ method: "a" }, { method: "b" }]); // settled
|
|
|
41
41
|
The `@c9up/comet/protocol` surface exposes the spec primitives a server binding
|
|
42
42
|
needs: `parseRequest`, `isNotification`, `buildRequest`/`buildSuccess`/`buildError`,
|
|
43
43
|
`RpcError`/`toRpcError`/`isRpcShapedError`, and the reserved `RpcErrorCode`.
|
|
44
|
+
|
|
45
|
+
`parseRequest` enforces the envelope MUSTs — the version, the method, an `id`
|
|
46
|
+
that is a String/Number/Null, and a `params` that is a Structured value (§4.2:
|
|
47
|
+
an Array or an Object, never a scalar). A handler raises a domain error by
|
|
48
|
+
throwing `RpcError`; `isRpcShapedError` also accepts a plain object whose `code`
|
|
49
|
+
is a **negative integer**, the space the spec gives errors, so the numeric
|
|
50
|
+
`code` an aborted `fetch` or a gRPC status happens to carry is not mistaken for
|
|
51
|
+
one.
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAGN,QAAQ,EAGR,MAAM,eAAe,CAAC;AAEvB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAC1B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,OAAO,EACb,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,MAAM,WAAW,gBAAgB;IAChC,wDAAwD;IACxD,SAAS,EAAE,YAAY,CAAC;IACxB,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IAC1C;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,CAAC,CAAC;IAC7B,uFAAuF;IACvF,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,wFAAwF;AACxF,MAAM,WAAW,OAAO,CAAC,CAAC,GAAG,OAAO;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,CAAC,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,IAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAElC,MAAM,WAAW,SAAS;IACzB;;;;;OAKG;IACH,IAAI,CAAC,CAAC,GAAG,OAAO,EACf,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GACzB,OAAO,CAAC,CAAC,CAAC,CAAC;IACd;;;OAGG;IACH,KAAK,CACJ,KAAK,EAAE,OAAO,EAAE,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAChC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAGN,QAAQ,EAGR,MAAM,eAAe,CAAC;AAEvB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAC1B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,OAAO,EACb,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,MAAM,WAAW,gBAAgB;IAChC,wDAAwD;IACxD,SAAS,EAAE,YAAY,CAAC;IACxB,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IAC1C;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,CAAC,CAAC;IAC7B,uFAAuF;IACvF,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,wFAAwF;AACxF,MAAM,WAAW,OAAO,CAAC,CAAC,GAAG,OAAO;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,CAAC,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,IAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAElC,MAAM,WAAW,SAAS;IACzB;;;;;OAKG;IACH,IAAI,CAAC,CAAC,GAAG,OAAO,EACf,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GACzB,OAAO,CAAC,CAAC,CAAC,CAAC;IACd;;;OAGG;IACH,KAAK,CACJ,KAAK,EAAE,OAAO,EAAE,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAChC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CA2JpE"}
|
package/dist/client.js
CHANGED
|
@@ -15,9 +15,21 @@ export function createRpcClient(options) {
|
|
|
15
15
|
const { transport } = options;
|
|
16
16
|
const url = options.url ?? "/rpc";
|
|
17
17
|
let nextId = 0;
|
|
18
|
+
/**
|
|
19
|
+
* The next request id — one counter for single calls and batches alike.
|
|
20
|
+
*
|
|
21
|
+
* A batch used to number its entries from zero, so every batch sent the ids
|
|
22
|
+
* `0, 1, 2` again and a call sent `1` at the same time. Over HTTP the
|
|
23
|
+
* transport pairs each response with its own request and nothing shows; over
|
|
24
|
+
* a transport that multiplexes — one WebSocket carrying several requests,
|
|
25
|
+
* which is the reason the transport is injected at all — the correlation is
|
|
26
|
+
* the id, and two live requests carrying the same one is a response
|
|
27
|
+
* delivered to the wrong caller.
|
|
28
|
+
*/
|
|
29
|
+
const allocateId = () => ++nextId;
|
|
18
30
|
return {
|
|
19
31
|
async call(method, params, callOptions) {
|
|
20
|
-
const id =
|
|
32
|
+
const id = allocateId();
|
|
21
33
|
const res = await transport(url, buildRequest(method, params, id), {
|
|
22
34
|
signal: callOptions?.signal,
|
|
23
35
|
});
|
|
@@ -53,12 +65,10 @@ export function createRpcClient(options) {
|
|
|
53
65
|
async batch(calls, batchOptions) {
|
|
54
66
|
if (calls.length === 0)
|
|
55
67
|
return [];
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
const res = await transport(url,
|
|
60
|
-
signal: batchOptions?.signal,
|
|
61
|
-
});
|
|
68
|
+
// Each call keeps the id it was sent under; responses are matched
|
|
69
|
+
// back by it, in whatever order the server returns them.
|
|
70
|
+
const pending = calls.map((call) => ({ call, id: allocateId() }));
|
|
71
|
+
const res = await transport(url, pending.map((entry) => buildRequest(entry.call.method, entry.call.params, entry.id)), { signal: batchOptions?.signal });
|
|
62
72
|
if (!Array.isArray(res)) {
|
|
63
73
|
throw new RpcError(RpcErrorCode.InternalError, "Malformed JSON-RPC batch response");
|
|
64
74
|
}
|
|
@@ -77,16 +87,16 @@ export function createRpcClient(options) {
|
|
|
77
87
|
duplicated.add(item.id);
|
|
78
88
|
byId.set(item.id, item);
|
|
79
89
|
}
|
|
80
|
-
return
|
|
90
|
+
return pending.map(({ call: c, id }) => {
|
|
81
91
|
const fail = (message) => ({
|
|
82
92
|
ok: false,
|
|
83
93
|
error: new RpcError(RpcErrorCode.InternalError, message),
|
|
84
94
|
});
|
|
85
|
-
const envelope = byId.get(
|
|
95
|
+
const envelope = byId.get(id);
|
|
86
96
|
if (!envelope)
|
|
87
97
|
return fail(`No response for "${c.method}"`);
|
|
88
|
-
if (duplicated.has(
|
|
89
|
-
return fail(`More than one response carried id ${
|
|
98
|
+
if (duplicated.has(id)) {
|
|
99
|
+
return fail(`More than one response carried id ${id}`);
|
|
90
100
|
}
|
|
91
101
|
if (envelope.jsonrpc !== "2.0") {
|
|
92
102
|
return fail(`Malformed JSON-RPC response for "${c.method}" (bad jsonrpc version)`);
|
|
@@ -104,8 +114,26 @@ export function createRpcClient(options) {
|
|
|
104
114
|
if (!hasResult) {
|
|
105
115
|
return fail(`JSON-RPC response for "${c.method}" has neither result nor error`);
|
|
106
116
|
}
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
if (!c.parse)
|
|
118
|
+
return { ok: true, value: envelope.result };
|
|
119
|
+
try {
|
|
120
|
+
return { ok: true, value: c.parse(envelope.result) };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// A batch settles per call, and a result that fails ITS OWN
|
|
124
|
+
// validation is that entry's failure. Letting the throw out
|
|
125
|
+
// rejected the whole promise and took every other entry with
|
|
126
|
+
// it — the ones that had already succeeded included.
|
|
127
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: new RpcError(RpcErrorCode.InternalError, `Result for "${c.method}" failed validation: ${reason}`,
|
|
131
|
+
// Built here rather than received, so `data` carries what
|
|
132
|
+
// the validator threw — a caller checking WHY it failed
|
|
133
|
+
// has nowhere else to read it.
|
|
134
|
+
error),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
109
137
|
});
|
|
110
138
|
},
|
|
111
139
|
};
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACN,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,UAAU,GACV,MAAM,eAAe,CAAC;AAgEvB,MAAM,UAAU,eAAe,CAAC,OAAyB;IACxD,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAC9B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC;IAClC,IAAI,MAAM,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -54,7 +54,15 @@ export declare class RpcError extends Error {
|
|
|
54
54
|
}
|
|
55
55
|
/** Type guard for {@link RpcError}. */
|
|
56
56
|
export declare function isRpcError(value: unknown): value is RpcError;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Narrow an unknown to a JSON-RPC Object — non-null, and not an Array.
|
|
59
|
+
*
|
|
60
|
+
* Every envelope the spec defines is an Object; the one Array it has is the
|
|
61
|
+
* batch, which a binding frames before anything here sees it. Answering "yes"
|
|
62
|
+
* to an Array let a batch walk into the single-request path, where it was
|
|
63
|
+
* turned away for a missing `jsonrpc` rather than for being the wrong kind of
|
|
64
|
+
* thing.
|
|
65
|
+
*/
|
|
58
66
|
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
|
59
67
|
/**
|
|
60
68
|
* Turn a JSON-RPC `error` member (untrusted wire value) into an {@link RpcError}.
|
|
@@ -62,16 +70,34 @@ export declare function isObject(value: unknown): value is Record<string, unknow
|
|
|
62
70
|
*/
|
|
63
71
|
export declare function toRpcError(error: unknown): RpcError;
|
|
64
72
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
73
|
+
* Whether a thrown value is a JSON-RPC error a binding may answer the caller
|
|
74
|
+
* with, rather than an internal failure it has to keep to itself.
|
|
75
|
+
*
|
|
76
|
+
* {@link RpcError} always is: throwing one is a statement. A foreign object is
|
|
77
|
+
* only read as one when its `code` is a NEGATIVE integer — the space the spec
|
|
78
|
+
* gives errors (§5.1 reserves -32768..-32000 and the framework issues -32003,
|
|
79
|
+
* -32004 and the like), and the space a handler writing a domain code picks
|
|
80
|
+
* from.
|
|
81
|
+
*
|
|
82
|
+
* Accepting any number was accepting the numbers other things happen to carry.
|
|
83
|
+
* A `DOMException` has a legacy numeric `code` — 20 for `AbortError`, 23 for
|
|
84
|
+
* `TimeoutError` — so a handler whose outbound `fetch` timed out answered the
|
|
85
|
+
* caller with `{ code: 20, message: "This operation was aborted" }`, walking
|
|
86
|
+
* straight past the guard a binding puts there to keep internal messages off
|
|
87
|
+
* the wire in production. gRPC status codes (0..16) arrive the same way.
|
|
68
88
|
*/
|
|
69
89
|
export declare function isRpcShapedError(err: unknown): err is {
|
|
70
90
|
code: number;
|
|
71
91
|
message?: unknown;
|
|
72
92
|
data?: unknown;
|
|
73
93
|
};
|
|
74
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Build an outgoing request envelope. An absent `params` is left out, the way
|
|
96
|
+
* {@link buildError} leaves out an absent `data`: a transport that does not go
|
|
97
|
+
* through `JSON.stringify` — a worker, an in-process bus — otherwise carries a
|
|
98
|
+
* `params` member holding `undefined`, which is not a Structured value and is
|
|
99
|
+
* refused by the parser at the other end.
|
|
100
|
+
*/
|
|
75
101
|
export declare function buildRequest(method: string, params: unknown, id: JsonRpcId): JsonRpcRequest;
|
|
76
102
|
/** Build a success response envelope. */
|
|
77
103
|
export declare function buildSuccess(result: unknown, id: JsonRpcId): JsonRpcSuccessResponse;
|
|
@@ -89,7 +115,8 @@ export type ParsedRpcRequest = {
|
|
|
89
115
|
};
|
|
90
116
|
/**
|
|
91
117
|
* Validate an incoming JSON-RPC envelope and extract `method`/`params`/`id`.
|
|
92
|
-
* Returns an `InvalidRequest` error response when the version
|
|
118
|
+
* Returns an `InvalidRequest` error response when the version, the method, the
|
|
119
|
+
* id or the shape of `params` is wrong.
|
|
93
120
|
*/
|
|
94
121
|
export declare function parseRequest(request: unknown): ParsedRpcRequest;
|
|
95
122
|
/**
|
package/dist/protocol.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAE/C,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,SAAS,CAAC;CACd;AAED,2DAA2D;AAC3D,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,SAAS,CAAC;CACd;AAED,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,kBAAkB,CAAC;IAC1B,EAAE,EAAE,SAAS,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GAAG,sBAAsB,GAAG,oBAAoB,CAAC;AAE5E;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;;;CAMf,CAAC;AAEX,uFAAuF;AACvF,qBAAa,QAAS,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;gBACZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;CAMzD;AAED,uCAAuC;AACvC,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D;AAED
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAE/C,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,SAAS,CAAC;CACd;AAED,2DAA2D;AAC3D,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,SAAS,CAAC;CACd;AAED,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,kBAAkB,CAAC;IAC1B,EAAE,EAAE,SAAS,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GAAG,sBAAsB,GAAG,oBAAoB,CAAC;AAE5E;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;;;CAMf,CAAC;AAEX,uFAAuF;AACvF,qBAAa,QAAS,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;gBACZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;CAMzD;AAED,uCAAuC;AACvC,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAaD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAanD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,OAAO,GACV,GAAG,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAG5D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,EAAE,EAAE,SAAS,GACX,cAAc,CAIhB;AAED,yCAAyC;AACzC,wBAAgB,YAAY,CAC3B,MAAM,EAAE,OAAO,EACf,EAAE,EAAE,SAAS,GACX,sBAAsB,CAExB;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,SAAS,EACb,IAAI,CAAC,EAAE,OAAO,GACZ,oBAAoB,CAMtB;AAED,sCAAsC;AACtC,MAAM,MAAM,gBAAgB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,SAAS,CAAA;CAAE,GAC5D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAEjD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,CAgD/D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CASxD"}
|
package/dist/protocol.js
CHANGED
|
@@ -33,9 +33,27 @@ export class RpcError extends Error {
|
|
|
33
33
|
export function isRpcError(value) {
|
|
34
34
|
return value instanceof RpcError;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Narrow an unknown to a JSON-RPC Object — non-null, and not an Array.
|
|
38
|
+
*
|
|
39
|
+
* Every envelope the spec defines is an Object; the one Array it has is the
|
|
40
|
+
* batch, which a binding frames before anything here sees it. Answering "yes"
|
|
41
|
+
* to an Array let a batch walk into the single-request path, where it was
|
|
42
|
+
* turned away for a missing `jsonrpc` rather than for being the wrong kind of
|
|
43
|
+
* thing.
|
|
44
|
+
*/
|
|
37
45
|
export function isObject(value) {
|
|
38
|
-
return typeof value === "object" && value !== null;
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A JSON-RPC error code. Spec §5.1 on `code`: "A Number that indicates the
|
|
50
|
+
* error type that occurred. This MUST be an integer." A fractional code — or a
|
|
51
|
+
* `NaN`, which is what an arithmetic slip in a handler produces — is not one,
|
|
52
|
+
* and `NaN` does not survive `JSON.stringify`: it goes out as `null` and
|
|
53
|
+
* reaches the caller as an error whose code is missing.
|
|
54
|
+
*/
|
|
55
|
+
function isErrorCode(value) {
|
|
56
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
39
57
|
}
|
|
40
58
|
/**
|
|
41
59
|
* Turn a JSON-RPC `error` member (untrusted wire value) into an {@link RpcError}.
|
|
@@ -43,23 +61,45 @@ export function isObject(value) {
|
|
|
43
61
|
*/
|
|
44
62
|
export function toRpcError(error) {
|
|
45
63
|
if (isObject(error) &&
|
|
46
|
-
|
|
64
|
+
isErrorCode(error.code) &&
|
|
47
65
|
typeof error.message === "string") {
|
|
48
66
|
return new RpcError(error.code, error.message, error.data);
|
|
49
67
|
}
|
|
50
68
|
return new RpcError(RpcErrorCode.InternalError, "Malformed JSON-RPC error envelope", error);
|
|
51
69
|
}
|
|
52
70
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
71
|
+
* Whether a thrown value is a JSON-RPC error a binding may answer the caller
|
|
72
|
+
* with, rather than an internal failure it has to keep to itself.
|
|
73
|
+
*
|
|
74
|
+
* {@link RpcError} always is: throwing one is a statement. A foreign object is
|
|
75
|
+
* only read as one when its `code` is a NEGATIVE integer — the space the spec
|
|
76
|
+
* gives errors (§5.1 reserves -32768..-32000 and the framework issues -32003,
|
|
77
|
+
* -32004 and the like), and the space a handler writing a domain code picks
|
|
78
|
+
* from.
|
|
79
|
+
*
|
|
80
|
+
* Accepting any number was accepting the numbers other things happen to carry.
|
|
81
|
+
* A `DOMException` has a legacy numeric `code` — 20 for `AbortError`, 23 for
|
|
82
|
+
* `TimeoutError` — so a handler whose outbound `fetch` timed out answered the
|
|
83
|
+
* caller with `{ code: 20, message: "This operation was aborted" }`, walking
|
|
84
|
+
* straight past the guard a binding puts there to keep internal messages off
|
|
85
|
+
* the wire in production. gRPC status codes (0..16) arrive the same way.
|
|
56
86
|
*/
|
|
57
87
|
export function isRpcShapedError(err) {
|
|
58
|
-
|
|
88
|
+
if (err instanceof RpcError)
|
|
89
|
+
return true;
|
|
90
|
+
return isObject(err) && isErrorCode(err.code) && err.code < 0;
|
|
59
91
|
}
|
|
60
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Build an outgoing request envelope. An absent `params` is left out, the way
|
|
94
|
+
* {@link buildError} leaves out an absent `data`: a transport that does not go
|
|
95
|
+
* through `JSON.stringify` — a worker, an in-process bus — otherwise carries a
|
|
96
|
+
* `params` member holding `undefined`, which is not a Structured value and is
|
|
97
|
+
* refused by the parser at the other end.
|
|
98
|
+
*/
|
|
61
99
|
export function buildRequest(method, params, id) {
|
|
62
|
-
return
|
|
100
|
+
return params === undefined
|
|
101
|
+
? { jsonrpc: "2.0", method, id }
|
|
102
|
+
: { jsonrpc: "2.0", method, params, id };
|
|
63
103
|
}
|
|
64
104
|
/** Build a success response envelope. */
|
|
65
105
|
export function buildSuccess(result, id) {
|
|
@@ -75,7 +115,8 @@ export function buildError(code, message, id, data) {
|
|
|
75
115
|
}
|
|
76
116
|
/**
|
|
77
117
|
* Validate an incoming JSON-RPC envelope and extract `method`/`params`/`id`.
|
|
78
|
-
* Returns an `InvalidRequest` error response when the version
|
|
118
|
+
* Returns an `InvalidRequest` error response when the version, the method, the
|
|
119
|
+
* id or the shape of `params` is wrong.
|
|
79
120
|
*/
|
|
80
121
|
export function parseRequest(request) {
|
|
81
122
|
if (!isObject(request)) {
|
|
@@ -103,7 +144,13 @@ export function parseRequest(request) {
|
|
|
103
144
|
const id = rawId === null || typeof rawId === "string" || typeof rawId === "number"
|
|
104
145
|
? rawId
|
|
105
146
|
: null;
|
|
106
|
-
|
|
147
|
+
// §4.2: "If present, parameters for the rpc call MUST be provided as a
|
|
148
|
+
// Structured value. Either by-position through an Array or by-name through
|
|
149
|
+
// an Object." A string, a number or a `null` is none of those, and passing
|
|
150
|
+
// one on hands the method a `params` no handler was written to read — the
|
|
151
|
+
// envelope check saying yes to something the method can only say no to.
|
|
152
|
+
const paramsValid = params === undefined || isObject(params) || Array.isArray(params);
|
|
153
|
+
if (jsonrpc !== "2.0" || !method || !idValid || !paramsValid) {
|
|
107
154
|
return {
|
|
108
155
|
ok: false,
|
|
109
156
|
response: buildError(RpcErrorCode.InvalidRequest, "Invalid Request", id),
|
package/dist/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoCH;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,UAAU,EAAE,CAAC,KAAK;IAClB,cAAc,EAAE,CAAC,KAAK;IACtB,cAAc,EAAE,CAAC,KAAK;IACtB,aAAa,EAAE,CAAC,KAAK;IACrB,aAAa,EAAE,CAAC,KAAK;CACZ,CAAC;AAEX,uFAAuF;AACvF,MAAM,OAAO,QAAS,SAAQ,KAAK;IACzB,IAAI,CAAS;IACb,IAAI,CAAW;IACxB,YAAY,IAAY,EAAE,OAAe,EAAE,IAAc;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AAED,uCAAuC;AACvC,MAAM,UAAU,UAAU,CAAC,KAAc;IACxC,OAAO,KAAK,YAAY,QAAQ,CAAC;AAClC,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/comet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Comet — agnostic JSON-RPC 2.0 protocol + isomorphic, transport-injectable client for the Ream framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@biomejs/biome": "^2.4.10",
|
|
25
25
|
"@types/node": "^22.19.15",
|
|
26
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
26
27
|
"typescript": "^6.0.2",
|
|
27
28
|
"vitest": "^4.1.2"
|
|
28
29
|
},
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "tsc -p tsconfig.build.json",
|
|
47
48
|
"test": "vitest run",
|
|
48
|
-
"lint": "biome check src/",
|
|
49
|
+
"lint": "biome check src/ tests/",
|
|
49
50
|
"test:coverage": "vitest run --coverage",
|
|
50
51
|
"typecheck": "tsc --noEmit"
|
|
51
52
|
}
|
package/src/client.ts
CHANGED
|
@@ -84,6 +84,18 @@ export function createRpcClient(options: RpcClientOptions): RpcClient {
|
|
|
84
84
|
const { transport } = options;
|
|
85
85
|
const url = options.url ?? "/rpc";
|
|
86
86
|
let nextId = 0;
|
|
87
|
+
/**
|
|
88
|
+
* The next request id — one counter for single calls and batches alike.
|
|
89
|
+
*
|
|
90
|
+
* A batch used to number its entries from zero, so every batch sent the ids
|
|
91
|
+
* `0, 1, 2` again and a call sent `1` at the same time. Over HTTP the
|
|
92
|
+
* transport pairs each response with its own request and nothing shows; over
|
|
93
|
+
* a transport that multiplexes — one WebSocket carrying several requests,
|
|
94
|
+
* which is the reason the transport is injected at all — the correlation is
|
|
95
|
+
* the id, and two live requests carrying the same one is a response
|
|
96
|
+
* delivered to the wrong caller.
|
|
97
|
+
*/
|
|
98
|
+
const allocateId = (): number => ++nextId;
|
|
87
99
|
|
|
88
100
|
return {
|
|
89
101
|
async call<T>(
|
|
@@ -91,7 +103,7 @@ export function createRpcClient(options: RpcClientOptions): RpcClient {
|
|
|
91
103
|
params?: unknown,
|
|
92
104
|
callOptions?: RpcCallOptions<T>,
|
|
93
105
|
): Promise<T> {
|
|
94
|
-
const id =
|
|
106
|
+
const id = allocateId();
|
|
95
107
|
const res = await transport(url, buildRequest(method, params, id), {
|
|
96
108
|
signal: callOptions?.signal,
|
|
97
109
|
});
|
|
@@ -138,13 +150,16 @@ export function createRpcClient(options: RpcClientOptions): RpcClient {
|
|
|
138
150
|
batchOptions?: { signal?: AbortSignal },
|
|
139
151
|
): Promise<RpcResult[]> {
|
|
140
152
|
if (calls.length === 0) return [];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
153
|
+
// Each call keeps the id it was sent under; responses are matched
|
|
154
|
+
// back by it, in whatever order the server returns them.
|
|
155
|
+
const pending = calls.map((call) => ({ call, id: allocateId() }));
|
|
156
|
+
const res = await transport(
|
|
157
|
+
url,
|
|
158
|
+
pending.map((entry) =>
|
|
159
|
+
buildRequest(entry.call.method, entry.call.params, entry.id),
|
|
160
|
+
),
|
|
161
|
+
{ signal: batchOptions?.signal },
|
|
144
162
|
);
|
|
145
|
-
const res = await transport(url, requests, {
|
|
146
|
-
signal: batchOptions?.signal,
|
|
147
|
-
});
|
|
148
163
|
if (!Array.isArray(res)) {
|
|
149
164
|
throw new RpcError(
|
|
150
165
|
RpcErrorCode.InternalError,
|
|
@@ -164,15 +179,15 @@ export function createRpcClient(options: RpcClientOptions): RpcClient {
|
|
|
164
179
|
if (byId.has(item.id)) duplicated.add(item.id);
|
|
165
180
|
byId.set(item.id, item);
|
|
166
181
|
}
|
|
167
|
-
return
|
|
182
|
+
return pending.map(({ call: c, id }) => {
|
|
168
183
|
const fail = (message: string): RpcResult => ({
|
|
169
184
|
ok: false,
|
|
170
185
|
error: new RpcError(RpcErrorCode.InternalError, message),
|
|
171
186
|
});
|
|
172
|
-
const envelope = byId.get(
|
|
187
|
+
const envelope = byId.get(id);
|
|
173
188
|
if (!envelope) return fail(`No response for "${c.method}"`);
|
|
174
|
-
if (duplicated.has(
|
|
175
|
-
return fail(`More than one response carried id ${
|
|
189
|
+
if (duplicated.has(id)) {
|
|
190
|
+
return fail(`More than one response carried id ${id}`);
|
|
176
191
|
}
|
|
177
192
|
if (envelope.jsonrpc !== "2.0") {
|
|
178
193
|
return fail(
|
|
@@ -196,8 +211,27 @@ export function createRpcClient(options: RpcClientOptions): RpcClient {
|
|
|
196
211
|
`JSON-RPC response for "${c.method}" has neither result nor error`,
|
|
197
212
|
);
|
|
198
213
|
}
|
|
199
|
-
|
|
200
|
-
|
|
214
|
+
if (!c.parse) return { ok: true, value: envelope.result };
|
|
215
|
+
try {
|
|
216
|
+
return { ok: true, value: c.parse(envelope.result) };
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// A batch settles per call, and a result that fails ITS OWN
|
|
219
|
+
// validation is that entry's failure. Letting the throw out
|
|
220
|
+
// rejected the whole promise and took every other entry with
|
|
221
|
+
// it — the ones that had already succeeded included.
|
|
222
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
error: new RpcError(
|
|
226
|
+
RpcErrorCode.InternalError,
|
|
227
|
+
`Result for "${c.method}" failed validation: ${reason}`,
|
|
228
|
+
// Built here rather than received, so `data` carries what
|
|
229
|
+
// the validator threw — a caller checking WHY it failed
|
|
230
|
+
// has nowhere else to read it.
|
|
231
|
+
error,
|
|
232
|
+
),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
201
235
|
});
|
|
202
236
|
},
|
|
203
237
|
};
|
package/src/protocol.ts
CHANGED
|
@@ -71,9 +71,28 @@ export function isRpcError(value: unknown): value is RpcError {
|
|
|
71
71
|
return value instanceof RpcError;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Narrow an unknown to a JSON-RPC Object — non-null, and not an Array.
|
|
76
|
+
*
|
|
77
|
+
* Every envelope the spec defines is an Object; the one Array it has is the
|
|
78
|
+
* batch, which a binding frames before anything here sees it. Answering "yes"
|
|
79
|
+
* to an Array let a batch walk into the single-request path, where it was
|
|
80
|
+
* turned away for a missing `jsonrpc` rather than for being the wrong kind of
|
|
81
|
+
* thing.
|
|
82
|
+
*/
|
|
75
83
|
export function isObject(value: unknown): value is Record<string, unknown> {
|
|
76
|
-
return typeof value === "object" && value !== null;
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A JSON-RPC error code. Spec §5.1 on `code`: "A Number that indicates the
|
|
89
|
+
* error type that occurred. This MUST be an integer." A fractional code — or a
|
|
90
|
+
* `NaN`, which is what an arithmetic slip in a handler produces — is not one,
|
|
91
|
+
* and `NaN` does not survive `JSON.stringify`: it goes out as `null` and
|
|
92
|
+
* reaches the caller as an error whose code is missing.
|
|
93
|
+
*/
|
|
94
|
+
function isErrorCode(value: unknown): value is number {
|
|
95
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
/**
|
|
@@ -83,7 +102,7 @@ export function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
83
102
|
export function toRpcError(error: unknown): RpcError {
|
|
84
103
|
if (
|
|
85
104
|
isObject(error) &&
|
|
86
|
-
|
|
105
|
+
isErrorCode(error.code) &&
|
|
87
106
|
typeof error.message === "string"
|
|
88
107
|
) {
|
|
89
108
|
return new RpcError(error.code, error.message, error.data);
|
|
@@ -96,23 +115,44 @@ export function toRpcError(error: unknown): RpcError {
|
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
118
|
+
* Whether a thrown value is a JSON-RPC error a binding may answer the caller
|
|
119
|
+
* with, rather than an internal failure it has to keep to itself.
|
|
120
|
+
*
|
|
121
|
+
* {@link RpcError} always is: throwing one is a statement. A foreign object is
|
|
122
|
+
* only read as one when its `code` is a NEGATIVE integer — the space the spec
|
|
123
|
+
* gives errors (§5.1 reserves -32768..-32000 and the framework issues -32003,
|
|
124
|
+
* -32004 and the like), and the space a handler writing a domain code picks
|
|
125
|
+
* from.
|
|
126
|
+
*
|
|
127
|
+
* Accepting any number was accepting the numbers other things happen to carry.
|
|
128
|
+
* A `DOMException` has a legacy numeric `code` — 20 for `AbortError`, 23 for
|
|
129
|
+
* `TimeoutError` — so a handler whose outbound `fetch` timed out answered the
|
|
130
|
+
* caller with `{ code: 20, message: "This operation was aborted" }`, walking
|
|
131
|
+
* straight past the guard a binding puts there to keep internal messages off
|
|
132
|
+
* the wire in production. gRPC status codes (0..16) arrive the same way.
|
|
102
133
|
*/
|
|
103
134
|
export function isRpcShapedError(
|
|
104
135
|
err: unknown,
|
|
105
136
|
): err is { code: number; message?: unknown; data?: unknown } {
|
|
106
|
-
|
|
137
|
+
if (err instanceof RpcError) return true;
|
|
138
|
+
return isObject(err) && isErrorCode(err.code) && err.code < 0;
|
|
107
139
|
}
|
|
108
140
|
|
|
109
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Build an outgoing request envelope. An absent `params` is left out, the way
|
|
143
|
+
* {@link buildError} leaves out an absent `data`: a transport that does not go
|
|
144
|
+
* through `JSON.stringify` — a worker, an in-process bus — otherwise carries a
|
|
145
|
+
* `params` member holding `undefined`, which is not a Structured value and is
|
|
146
|
+
* refused by the parser at the other end.
|
|
147
|
+
*/
|
|
110
148
|
export function buildRequest(
|
|
111
149
|
method: string,
|
|
112
150
|
params: unknown,
|
|
113
151
|
id: JsonRpcId,
|
|
114
152
|
): JsonRpcRequest {
|
|
115
|
-
return
|
|
153
|
+
return params === undefined
|
|
154
|
+
? { jsonrpc: "2.0", method, id }
|
|
155
|
+
: { jsonrpc: "2.0", method, params, id };
|
|
116
156
|
}
|
|
117
157
|
|
|
118
158
|
/** Build a success response envelope. */
|
|
@@ -144,7 +184,8 @@ export type ParsedRpcRequest =
|
|
|
144
184
|
|
|
145
185
|
/**
|
|
146
186
|
* Validate an incoming JSON-RPC envelope and extract `method`/`params`/`id`.
|
|
147
|
-
* Returns an `InvalidRequest` error response when the version
|
|
187
|
+
* Returns an `InvalidRequest` error response when the version, the method, the
|
|
188
|
+
* id or the shape of `params` is wrong.
|
|
148
189
|
*/
|
|
149
190
|
export function parseRequest(request: unknown): ParsedRpcRequest {
|
|
150
191
|
if (!isObject(request)) {
|
|
@@ -180,7 +221,14 @@ export function parseRequest(request: unknown): ParsedRpcRequest {
|
|
|
180
221
|
rawId === null || typeof rawId === "string" || typeof rawId === "number"
|
|
181
222
|
? rawId
|
|
182
223
|
: null;
|
|
183
|
-
|
|
224
|
+
// §4.2: "If present, parameters for the rpc call MUST be provided as a
|
|
225
|
+
// Structured value. Either by-position through an Array or by-name through
|
|
226
|
+
// an Object." A string, a number or a `null` is none of those, and passing
|
|
227
|
+
// one on hands the method a `params` no handler was written to read — the
|
|
228
|
+
// envelope check saying yes to something the method can only say no to.
|
|
229
|
+
const paramsValid =
|
|
230
|
+
params === undefined || isObject(params) || Array.isArray(params);
|
|
231
|
+
if (jsonrpc !== "2.0" || !method || !idValid || !paramsValid) {
|
|
184
232
|
return {
|
|
185
233
|
ok: false,
|
|
186
234
|
response: buildError(RpcErrorCode.InvalidRequest, "Invalid Request", id),
|