@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 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.
@@ -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,CAyHpE"}
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 = ++nextId;
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
- const requests = calls.map((c, index) =>
57
- // index = request position; responses are matched back by id
58
- buildRequest(c.method, c.params, index));
59
- const res = await transport(url, requests, {
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 calls.map((c, index) => {
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(index);
95
+ const envelope = byId.get(id);
86
96
  if (!envelope)
87
97
  return fail(`No response for "${c.method}"`);
88
- if (duplicated.has(index)) {
89
- return fail(`More than one response carried id ${index}`);
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
- const value = c.parse ? c.parse(envelope.result) : envelope.result;
108
- return { ok: true, value };
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
  };
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -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
- /** Narrow an unknown to a plain object (non-null). */
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
- * A domain error shaped like a JSON-RPC error it carries a numeric `code`. A
66
- * server binding can map such a throw to a JSON-RPC error response instead of
67
- * collapsing every throw to InternalError.
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
- /** Build an outgoing request envelope. */
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/method are wrong.
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
  /**
@@ -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,sDAAsD;AACtD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAanD;AAED;;;;GAIG;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,CAE5D;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,EAAE,EAAE,SAAS,GACX,cAAc,CAEhB;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;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,CAyC/D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CASxD"}
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
- /** Narrow an unknown to a plain object (non-null). */
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
- typeof error.code === "number" &&
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
- * A domain error shaped like a JSON-RPC error it carries a numeric `code`. A
54
- * server binding can map such a throw to a JSON-RPC error response instead of
55
- * collapsing every throw to InternalError.
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
- return isObject(err) && typeof err.code === "number";
88
+ if (err instanceof RpcError)
89
+ return true;
90
+ return isObject(err) && isErrorCode(err.code) && err.code < 0;
59
91
  }
60
- /** Build an outgoing request envelope. */
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 { jsonrpc: "2.0", method, params, id };
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/method are wrong.
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
- if (jsonrpc !== "2.0" || !method || !idValid) {
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),
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
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;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAc;IACxC,IACC,QAAQ,CAAC,KAAK,CAAC;QACf,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC;QACvB,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAChC,CAAC;QACF,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,QAAQ,CAClB,YAAY,CAAC,aAAa,EAC1B,mCAAmC,EACnC,KAAK,CACL,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAC/B,GAAY;IAEZ,IAAI,GAAG,YAAY,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC3B,MAAc,EACd,MAAe,EACf,EAAa;IAEb,OAAO,MAAM,KAAK,SAAS;QAC1B,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAChC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAC3C,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,YAAY,CAC3B,MAAe,EACf,EAAa;IAEb,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AACvC,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,UAAU,CACzB,IAAY,EACZ,OAAe,EACf,EAAa,EACb,IAAc;IAEd,OAAO;QACN,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;QACvE,EAAE;KACF,CAAC;AACH,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC5C,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,OAAO;YACN,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE,UAAU,CACnB,YAAY,CAAC,cAAc,EAC3B,iBAAiB,EACjB,IAAI,CACJ;SACD,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GACZ,SAAS,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;QAC1D,CAAC,CAAC,OAAO,CAAC,OAAO;QACjB,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,MAAM,GACX,QAAQ,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ;QACxD,CAAC,CAAC,OAAO,CAAC,MAAM;QAChB,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,MAAM,GAAG,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,MAAM,SAAS,GAAG,IAAI,IAAI,OAAO,CAAC;IAClC,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACjD,4EAA4E;IAC5E,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,OAAO,GACZ,CAAC,SAAS;QACV,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,QAAQ,CAAC;IAC3B,MAAM,EAAE,GACP,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ;QACvE,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAC;IACT,uEAAuE;IACvE,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,WAAW,GAChB,MAAM,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,OAAO,KAAK,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;QAC9D,OAAO;YACN,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE,UAAU,CAAC,YAAY,CAAC,cAAc,EAAE,iBAAiB,EAAE,EAAE,CAAC;SACxE,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC9C,OAAO,CACN,QAAQ,CAAC,OAAO,CAAC;QACjB,SAAS,IAAI,OAAO;QACpB,OAAO,CAAC,OAAO,KAAK,KAAK;QACzB,QAAQ,IAAI,OAAO;QACnB,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ;QAClC,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,CAClB,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/comet",
3
- "version": "0.1.2",
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 = ++nextId;
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
- const requests = calls.map((c, index) =>
142
- // index = request position; responses are matched back by id
143
- buildRequest(c.method, c.params, index),
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 calls.map((c, index) => {
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(index);
187
+ const envelope = byId.get(id);
173
188
  if (!envelope) return fail(`No response for "${c.method}"`);
174
- if (duplicated.has(index)) {
175
- return fail(`More than one response carried id ${index}`);
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
- const value = c.parse ? c.parse(envelope.result) : envelope.result;
200
- return { ok: true, value };
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
- /** Narrow an unknown to a plain object (non-null). */
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
- typeof error.code === "number" &&
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
- * A domain error shaped like a JSON-RPC error it carries a numeric `code`. A
100
- * server binding can map such a throw to a JSON-RPC error response instead of
101
- * collapsing every throw to InternalError.
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
- return isObject(err) && typeof err.code === "number";
137
+ if (err instanceof RpcError) return true;
138
+ return isObject(err) && isErrorCode(err.code) && err.code < 0;
107
139
  }
108
140
 
109
- /** Build an outgoing request envelope. */
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 { jsonrpc: "2.0", method, params, id };
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/method are wrong.
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
- if (jsonrpc !== "2.0" || !method || !idValid) {
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),