@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 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,CAgFpE"}
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
  });
@@ -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 (!("result" in res)) {
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
- const requests = calls.map((c, index) =>
48
- // index = request position; responses are matched back by id
49
- buildRequest(c.method, c.params, index));
50
- const res = await transport(url, requests, {
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
- for (const item of res)
58
- if (isObject(item))
59
- byId.set(item.id, item);
60
- return calls.map((c, index) => {
61
- const envelope = byId.get(index);
62
- if (!envelope) {
63
- return {
64
- ok: false,
65
- error: new RpcError(RpcErrorCode.InternalError, `No response for "${c.method}"`),
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
- const value = c.parse ? c.parse(envelope.result) : envelope.result;
72
- return { ok: true, value };
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
  };
@@ -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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/comet",
3
- "version": "0.1.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 = ++nextId;
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 (!("result" in res)) {
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
- const requests = calls.map((c, index) =>
130
- // index = request position; responses are matched back by id
131
- 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 },
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
- for (const item of res) if (isObject(item)) byId.set(item.id, item);
144
- return calls.map((c, index) => {
145
- const envelope = byId.get(index);
146
- if (!envelope) {
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
- `No response for "${c.method}"`,
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
- /** 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),