@c9up/comet 0.1.1 → 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 +84 -20
- 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 +93 -18
- 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
|
});
|
|
@@ -28,11 +40,20 @@ export function createRpcClient(options) {
|
|
|
28
40
|
if (!isObject(res) || res.jsonrpc !== "2.0" || res.id !== id) {
|
|
29
41
|
throw new RpcError(RpcErrorCode.InternalError, `Malformed or mismatched JSON-RPC response for "${method}" (bad jsonrpc/id envelope)`);
|
|
30
42
|
}
|
|
43
|
+
// §5: `result` and `error` are mutually exclusive — "Either the
|
|
44
|
+
// result member or error member MUST be included, but both members
|
|
45
|
+
// MUST NOT be included." A response carrying both is malformed, and
|
|
46
|
+
// reading the error out of it is guessing at which half the sender
|
|
47
|
+
// meant. Refused, the way a bad jsonrpc/id envelope already is.
|
|
48
|
+
const hasResult = "result" in res;
|
|
49
|
+
if (hasResult && res.error !== undefined) {
|
|
50
|
+
throw new RpcError(RpcErrorCode.InternalError, `Malformed JSON-RPC response for "${method}" (carries both result and error)`);
|
|
51
|
+
}
|
|
31
52
|
if (res.error !== undefined)
|
|
32
53
|
throw toRpcError(res.error);
|
|
33
54
|
// A conformant success response carries `result` (any JSON value,
|
|
34
55
|
// including null) and no error — neither key present is malformed.
|
|
35
|
-
if (!
|
|
56
|
+
if (!hasResult) {
|
|
36
57
|
throw new RpcError(RpcErrorCode.InternalError, `JSON-RPC response for "${method}" has neither result nor error`);
|
|
37
58
|
}
|
|
38
59
|
// Result boundary — the same unchecked `T` assertion HTTP clients use,
|
|
@@ -44,32 +65,75 @@ export function createRpcClient(options) {
|
|
|
44
65
|
async batch(calls, batchOptions) {
|
|
45
66
|
if (calls.length === 0)
|
|
46
67
|
return [];
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
const res = await transport(url,
|
|
51
|
-
signal: batchOptions?.signal,
|
|
52
|
-
});
|
|
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 });
|
|
53
72
|
if (!Array.isArray(res)) {
|
|
54
73
|
throw new RpcError(RpcErrorCode.InternalError, "Malformed JSON-RPC batch response");
|
|
55
74
|
}
|
|
75
|
+
// Envelope conformance, per item, exactly as `call` applies it — this
|
|
76
|
+
// checked none of it, so `{ jsonrpc: "1.0", id: 0 }` came back as
|
|
77
|
+
// `{ ok: true, value: undefined }` where the single-call path would
|
|
78
|
+
// have refused it.
|
|
56
79
|
const byId = new Map();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
const duplicated = new Set();
|
|
81
|
+
for (const item of res) {
|
|
82
|
+
if (!isObject(item))
|
|
83
|
+
continue;
|
|
84
|
+
// A repeated id is a malformed batch, and silently keeping the last
|
|
85
|
+
// one lets a response answer a call it was not for.
|
|
86
|
+
if (byId.has(item.id))
|
|
87
|
+
duplicated.add(item.id);
|
|
88
|
+
byId.set(item.id, item);
|
|
89
|
+
}
|
|
90
|
+
return pending.map(({ call: c, id }) => {
|
|
91
|
+
const fail = (message) => ({
|
|
92
|
+
ok: false,
|
|
93
|
+
error: new RpcError(RpcErrorCode.InternalError, message),
|
|
94
|
+
});
|
|
95
|
+
const envelope = byId.get(id);
|
|
96
|
+
if (!envelope)
|
|
97
|
+
return fail(`No response for "${c.method}"`);
|
|
98
|
+
if (duplicated.has(id)) {
|
|
99
|
+
return fail(`More than one response carried id ${id}`);
|
|
100
|
+
}
|
|
101
|
+
if (envelope.jsonrpc !== "2.0") {
|
|
102
|
+
return fail(`Malformed JSON-RPC response for "${c.method}" (bad jsonrpc version)`);
|
|
103
|
+
}
|
|
104
|
+
const hasResult = "result" in envelope;
|
|
105
|
+
// §5: mutually exclusive. See the single-call path.
|
|
106
|
+
if (hasResult && envelope.error !== undefined) {
|
|
107
|
+
return fail(`Malformed JSON-RPC response for "${c.method}" (carries both result and error)`);
|
|
67
108
|
}
|
|
68
109
|
if (envelope.error !== undefined) {
|
|
69
110
|
return { ok: false, error: toRpcError(envelope.error) };
|
|
70
111
|
}
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
// A conformant success carries `result` — any JSON value, null
|
|
113
|
+
// included — so its absence is malformed, not an undefined value.
|
|
114
|
+
if (!hasResult) {
|
|
115
|
+
return fail(`JSON-RPC response for "${c.method}" has neither result nor error`);
|
|
116
|
+
}
|
|
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
|
+
}
|
|
73
137
|
});
|
|
74
138
|
},
|
|
75
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":"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;IACf;;;;;;;;;;OAUG;IACH,MAAM,UAAU,GAAG,GAAW,EAAE,CAAC,EAAE,MAAM,CAAC;IAE1C,OAAO;QACN,KAAK,CAAC,IAAI,CACT,MAAc,EACd,MAAgB,EAChB,WAA+B;YAE/B,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE;gBAClE,MAAM,EAAE,WAAW,EAAE,MAAM;aAC3B,CAAC,CAAC;YACH,yEAAyE;YACzE,mEAAmE;YACnE,uEAAuE;YACvE,YAAY;YACZ,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,KAAK,KAAK,IAAI,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;gBAC9D,MAAM,IAAI,QAAQ,CACjB,YAAY,CAAC,aAAa,EAC1B,kDAAkD,MAAM,6BAA6B,CACrF,CAAC;YACH,CAAC;YACD,gEAAgE;YAChE,mEAAmE;YACnE,oEAAoE;YACpE,mEAAmE;YACnE,gEAAgE;YAChE,MAAM,SAAS,GAAG,QAAQ,IAAI,GAAG,CAAC;YAClC,IAAI,SAAS,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC1C,MAAM,IAAI,QAAQ,CACjB,YAAY,CAAC,aAAa,EAC1B,oCAAoC,MAAM,mCAAmC,CAC7E,CAAC;YACH,CAAC;YACD,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS;gBAAE,MAAM,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACzD,kEAAkE;YAClE,mEAAmE;YACnE,IAAI,CAAC,SAAS,EAAE,CAAC;gBAChB,MAAM,IAAI,QAAQ,CACjB,YAAY,CAAC,aAAa,EAC1B,0BAA0B,MAAM,gCAAgC,CAChE,CAAC;YACH,CAAC;YACD,uEAAuE;YACvE,iEAAiE;YACjE,OAAO,WAAW,EAAE,KAAK;gBACxB,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;gBAC/B,CAAC,CAAE,GAAG,CAAC,MAAY,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,KAAK,CACV,KAAgB,EAChB,YAAuC;YAEvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAClC,kEAAkE;YAClE,yDAAyD;YACzD,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;YAClE,MAAM,GAAG,GAAG,MAAM,SAAS,CAC1B,GAAG,EACH,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACrB,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,CAC5D,EACD,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,CAChC,CAAC;YACF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,QAAQ,CACjB,YAAY,CAAC,aAAa,EAC1B,mCAAmC,CACnC,CAAC;YACH,CAAC;YACD,sEAAsE;YACtE,kEAAkE;YAClE,oEAAoE;YACpE,mBAAmB;YACnB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAoC,CAAC;YACzD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAW,CAAC;YACtC,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;gBACxB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,SAAS;gBAC9B,oEAAoE;gBACpE,oDAAoD;gBACpD,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBAAE,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAC/C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACzB,CAAC;YACD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;gBACtC,MAAM,IAAI,GAAG,CAAC,OAAe,EAAa,EAAE,CAAC,CAAC;oBAC7C,EAAE,EAAE,KAAK;oBACT,KAAK,EAAE,IAAI,QAAQ,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC;iBACxD,CAAC,CAAC;gBACH,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC9B,IAAI,CAAC,QAAQ;oBAAE,OAAO,IAAI,CAAC,oBAAoB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;gBAC5D,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;oBACxB,OAAO,IAAI,CAAC,qCAAqC,EAAE,EAAE,CAAC,CAAC;gBACxD,CAAC;gBACD,IAAI,QAAQ,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;oBAChC,OAAO,IAAI,CACV,oCAAoC,CAAC,CAAC,MAAM,yBAAyB,CACrE,CAAC;gBACH,CAAC;gBACD,MAAM,SAAS,GAAG,QAAQ,IAAI,QAAQ,CAAC;gBACvC,oDAAoD;gBACpD,IAAI,SAAS,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;oBAC/C,OAAO,IAAI,CACV,oCAAoC,CAAC,CAAC,MAAM,mCAAmC,CAC/E,CAAC;gBACH,CAAC;gBACD,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;oBAClC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzD,CAAC;gBACD,+DAA+D;gBAC/D,kEAAkE;gBAClE,IAAI,CAAC,SAAS,EAAE,CAAC;oBAChB,OAAO,IAAI,CACV,0BAA0B,CAAC,CAAC,MAAM,gCAAgC,CAClE,CAAC;gBACH,CAAC;gBACD,IAAI,CAAC,CAAC,CAAC,KAAK;oBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAC1D,IAAI,CAAC;oBACJ,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtD,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBAChB,4DAA4D;oBAC5D,4DAA4D;oBAC5D,6DAA6D;oBAC7D,qDAAqD;oBACrD,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;oBACtE,OAAO;wBACN,EAAE,EAAE,KAAK;wBACT,KAAK,EAAE,IAAI,QAAQ,CAClB,YAAY,CAAC,aAAa,EAC1B,eAAe,CAAC,CAAC,MAAM,wBAAwB,MAAM,EAAE;wBACvD,0DAA0D;wBAC1D,wDAAwD;wBACxD,+BAA+B;wBAC/B,KAAK,CACL;qBACD,CAAC;gBACH,CAAC;YACF,CAAC,CAAC,CAAC;QACJ,CAAC;KACD,CAAC;AACH,CAAC"}
|
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
|
});
|
|
@@ -105,10 +117,22 @@ export function createRpcClient(options: RpcClientOptions): RpcClient {
|
|
|
105
117
|
`Malformed or mismatched JSON-RPC response for "${method}" (bad jsonrpc/id envelope)`,
|
|
106
118
|
);
|
|
107
119
|
}
|
|
120
|
+
// §5: `result` and `error` are mutually exclusive — "Either the
|
|
121
|
+
// result member or error member MUST be included, but both members
|
|
122
|
+
// MUST NOT be included." A response carrying both is malformed, and
|
|
123
|
+
// reading the error out of it is guessing at which half the sender
|
|
124
|
+
// meant. Refused, the way a bad jsonrpc/id envelope already is.
|
|
125
|
+
const hasResult = "result" in res;
|
|
126
|
+
if (hasResult && res.error !== undefined) {
|
|
127
|
+
throw new RpcError(
|
|
128
|
+
RpcErrorCode.InternalError,
|
|
129
|
+
`Malformed JSON-RPC response for "${method}" (carries both result and error)`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
108
132
|
if (res.error !== undefined) throw toRpcError(res.error);
|
|
109
133
|
// A conformant success response carries `result` (any JSON value,
|
|
110
134
|
// including null) and no error — neither key present is malformed.
|
|
111
|
-
if (!
|
|
135
|
+
if (!hasResult) {
|
|
112
136
|
throw new RpcError(
|
|
113
137
|
RpcErrorCode.InternalError,
|
|
114
138
|
`JSON-RPC response for "${method}" has neither result nor error`,
|
|
@@ -126,37 +150,88 @@ export function createRpcClient(options: RpcClientOptions): RpcClient {
|
|
|
126
150
|
batchOptions?: { signal?: AbortSignal },
|
|
127
151
|
): Promise<RpcResult[]> {
|
|
128
152
|
if (calls.length === 0) return [];
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 },
|
|
132
162
|
);
|
|
133
|
-
const res = await transport(url, requests, {
|
|
134
|
-
signal: batchOptions?.signal,
|
|
135
|
-
});
|
|
136
163
|
if (!Array.isArray(res)) {
|
|
137
164
|
throw new RpcError(
|
|
138
165
|
RpcErrorCode.InternalError,
|
|
139
166
|
"Malformed JSON-RPC batch response",
|
|
140
167
|
);
|
|
141
168
|
}
|
|
169
|
+
// Envelope conformance, per item, exactly as `call` applies it — this
|
|
170
|
+
// checked none of it, so `{ jsonrpc: "1.0", id: 0 }` came back as
|
|
171
|
+
// `{ ok: true, value: undefined }` where the single-call path would
|
|
172
|
+
// have refused it.
|
|
142
173
|
const byId = new Map<unknown, Record<string, unknown>>();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
174
|
+
const duplicated = new Set<unknown>();
|
|
175
|
+
for (const item of res) {
|
|
176
|
+
if (!isObject(item)) continue;
|
|
177
|
+
// A repeated id is a malformed batch, and silently keeping the last
|
|
178
|
+
// one lets a response answer a call it was not for.
|
|
179
|
+
if (byId.has(item.id)) duplicated.add(item.id);
|
|
180
|
+
byId.set(item.id, item);
|
|
181
|
+
}
|
|
182
|
+
return pending.map(({ call: c, id }) => {
|
|
183
|
+
const fail = (message: string): RpcResult => ({
|
|
184
|
+
ok: false,
|
|
185
|
+
error: new RpcError(RpcErrorCode.InternalError, message),
|
|
186
|
+
});
|
|
187
|
+
const envelope = byId.get(id);
|
|
188
|
+
if (!envelope) return fail(`No response for "${c.method}"`);
|
|
189
|
+
if (duplicated.has(id)) {
|
|
190
|
+
return fail(`More than one response carried id ${id}`);
|
|
191
|
+
}
|
|
192
|
+
if (envelope.jsonrpc !== "2.0") {
|
|
193
|
+
return fail(
|
|
194
|
+
`Malformed JSON-RPC response for "${c.method}" (bad jsonrpc version)`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const hasResult = "result" in envelope;
|
|
198
|
+
// §5: mutually exclusive. See the single-call path.
|
|
199
|
+
if (hasResult && envelope.error !== undefined) {
|
|
200
|
+
return fail(
|
|
201
|
+
`Malformed JSON-RPC response for "${c.method}" (carries both result and error)`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (envelope.error !== undefined) {
|
|
205
|
+
return { ok: false, error: toRpcError(envelope.error) };
|
|
206
|
+
}
|
|
207
|
+
// A conformant success carries `result` — any JSON value, null
|
|
208
|
+
// included — so its absence is malformed, not an undefined value.
|
|
209
|
+
if (!hasResult) {
|
|
210
|
+
return fail(
|
|
211
|
+
`JSON-RPC response for "${c.method}" has neither result nor error`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
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);
|
|
147
223
|
return {
|
|
148
224
|
ok: false,
|
|
149
225
|
error: new RpcError(
|
|
150
226
|
RpcErrorCode.InternalError,
|
|
151
|
-
`
|
|
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,
|
|
152
232
|
),
|
|
153
233
|
};
|
|
154
234
|
}
|
|
155
|
-
if (envelope.error !== undefined) {
|
|
156
|
-
return { ok: false, error: toRpcError(envelope.error) };
|
|
157
|
-
}
|
|
158
|
-
const value = c.parse ? c.parse(envelope.result) : envelope.result;
|
|
159
|
-
return { ok: true, value };
|
|
160
235
|
});
|
|
161
236
|
},
|
|
162
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),
|