@c9up/comet 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C9up
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @c9up/comet
2
+
3
+ Agnostic **JSON-RPC 2.0** protocol + an **isomorphic, transport-injectable client**
4
+ for the [Ream](https://github.com/C9up) framework. Zero framework, zero transport,
5
+ zero dependency — the browser binding (`@c9up/aurora`) and the server binding
6
+ (`@c9up/ream`'s `RpcRouter`) both build on this core instead of hand-rolling the
7
+ envelope and error codes.
8
+
9
+ ## Client
10
+
11
+ The client owns the JSON-RPC logic (single + batch, id matching, error mapping)
12
+ and delegates the actual bytes to an injected `transport` — so the same client
13
+ runs in the browser or in Node:
14
+
15
+ ```ts
16
+ import { createRpcClient } from "@c9up/comet";
17
+
18
+ const rpc = createRpcClient({
19
+ url: "/rpc",
20
+ transport: (url, body, { signal }) =>
21
+ fetch(url, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/json" },
24
+ body: JSON.stringify(body),
25
+ signal,
26
+ }).then((r) => r.json()),
27
+ });
28
+
29
+ const out = await rpc.call("task.validate", { id: 7 }); // typed via call<T>()
30
+ await rpc.call("user.find", { id }, { parse: isUser }); // runtime-validated
31
+ await rpc.call("slow.op", p, { signal: ac.signal }); // abortable
32
+ const results = await rpc.batch([{ method: "a" }, { method: "b" }]); // settled per call
33
+ ```
34
+
35
+ > In the browser, prefer `@c9up/aurora`'s `createRpcClient` — it wires aurora's
36
+ > `HttpClient` (base URL, auth headers, timeouts) as the transport and pairs with
37
+ > `command()`.
38
+
39
+ ## Protocol
40
+
41
+ The `@c9up/comet/protocol` surface exposes the spec primitives a server binding
42
+ needs: `parseRequest`, `isNotification`, `buildRequest`/`buildSuccess`/`buildError`,
43
+ `RpcError`/`toRpcError`/`isRpcShapedError`, and the reserved `RpcErrorCode`.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Isomorphic JSON-RPC 2.0 client — the protocol logic (single + batch) over an
3
+ * INJECTED transport. It owns the envelope/id/error handling (from
4
+ * {@link "./protocol"}) and knows nothing about how bytes travel: pass a browser
5
+ * transport (aurora's HttpClient) or a Node one (fetch/undici). This is the
6
+ * adapter seam that lets the client live outside any UI or server framework.
7
+ *
8
+ * const rpc = createRpcClient({ transport: (url, body, { signal }) =>
9
+ * fetch(url, { method: 'POST', body: JSON.stringify(body), signal })
10
+ * .then((r) => r.json()) })
11
+ * const out = await rpc.call('task.validate', { id })
12
+ */
13
+ import { RpcError } from "./protocol.js";
14
+ /**
15
+ * Sends a JSON body to `url` and resolves the parsed JSON response. Injected via
16
+ * {@link RpcClientOptions.transport}; the client never touches `fetch` directly.
17
+ */
18
+ export type RpcTransport = (url: string, body: unknown, options: {
19
+ signal?: AbortSignal;
20
+ }) => Promise<unknown>;
21
+ export interface RpcClientOptions {
22
+ /** How requests are sent — the only required wiring. */
23
+ transport: RpcTransport;
24
+ /** Endpoint path. Default `/rpc`. */
25
+ url?: string;
26
+ }
27
+ /** Per-call options for {@link RpcClient.call}. */
28
+ export interface RpcCallOptions<T = unknown> {
29
+ /**
30
+ * Validate the result at runtime, returning the typed value — skips the
31
+ * unchecked `T` assertion (the cast-free escape hatch).
32
+ */
33
+ parse?: (data: unknown) => T;
34
+ /** Abort signal — abort it to cancel the request (e.g. on unmount / new keystroke). */
35
+ signal?: AbortSignal;
36
+ }
37
+ /** One call in a batch. `parse` optionally validates that call's result (cast-free). */
38
+ export interface RpcCall<T = unknown> {
39
+ method: string;
40
+ params?: unknown;
41
+ parse?: (data: unknown) => T;
42
+ }
43
+ /** A settled batch entry — the result, or the JSON-RPC error for that call. */
44
+ export type RpcResult<T = unknown> = {
45
+ ok: true;
46
+ value: T;
47
+ } | {
48
+ ok: false;
49
+ error: RpcError;
50
+ };
51
+ export interface RpcClient {
52
+ /**
53
+ * Call one method. Returns the result, or throws {@link RpcError} on a
54
+ * JSON-RPC error. The `jsonrpc`/`id` envelope is handled internally. Pass
55
+ * `options.parse` to validate the result at runtime (skips the unchecked `T`
56
+ * assertion) and `options.signal` to make the call abortable.
57
+ */
58
+ call<T = unknown>(method: string, params?: unknown, options?: RpcCallOptions<T>): Promise<T>;
59
+ /**
60
+ * Send a JSON-RPC batch. Returns one settled entry per call, in request
61
+ * order. `options.signal` aborts the whole batch (it is one HTTP request).
62
+ */
63
+ batch(calls: RpcCall[], options?: {
64
+ signal?: AbortSignal;
65
+ }): Promise<RpcResult[]>;
66
+ }
67
+ export declare function createRpcClient(options: RpcClientOptions): RpcClient;
68
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +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,CAoEpE"}
package/dist/client.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Isomorphic JSON-RPC 2.0 client — the protocol logic (single + batch) over an
3
+ * INJECTED transport. It owns the envelope/id/error handling (from
4
+ * {@link "./protocol"}) and knows nothing about how bytes travel: pass a browser
5
+ * transport (aurora's HttpClient) or a Node one (fetch/undici). This is the
6
+ * adapter seam that lets the client live outside any UI or server framework.
7
+ *
8
+ * const rpc = createRpcClient({ transport: (url, body, { signal }) =>
9
+ * fetch(url, { method: 'POST', body: JSON.stringify(body), signal })
10
+ * .then((r) => r.json()) })
11
+ * const out = await rpc.call('task.validate', { id })
12
+ */
13
+ import { buildRequest, isObject, RpcError, RpcErrorCode, toRpcError, } from "./protocol.js";
14
+ export function createRpcClient(options) {
15
+ const { transport } = options;
16
+ const url = options.url ?? "/rpc";
17
+ let nextId = 0;
18
+ return {
19
+ async call(method, params, callOptions) {
20
+ const id = ++nextId;
21
+ const res = await transport(url, buildRequest(method, params, id), {
22
+ signal: callOptions?.signal,
23
+ });
24
+ if (!isObject(res)) {
25
+ throw new RpcError(RpcErrorCode.InternalError, `Malformed JSON-RPC response for "${method}"`);
26
+ }
27
+ if (res.error !== undefined)
28
+ throw toRpcError(res.error);
29
+ // Result boundary — the same unchecked `T` assertion HTTP clients use,
30
+ // with `parse` as the cast-free, runtime-validated escape hatch.
31
+ return callOptions?.parse
32
+ ? callOptions.parse(res.result)
33
+ : res.result;
34
+ },
35
+ async batch(calls, batchOptions) {
36
+ if (calls.length === 0)
37
+ return [];
38
+ const requests = calls.map((c, index) =>
39
+ // index = request position; responses are matched back by id
40
+ buildRequest(c.method, c.params, index));
41
+ const res = await transport(url, requests, {
42
+ signal: batchOptions?.signal,
43
+ });
44
+ if (!Array.isArray(res)) {
45
+ throw new RpcError(RpcErrorCode.InternalError, "Malformed JSON-RPC batch response");
46
+ }
47
+ const byId = new Map();
48
+ for (const item of res)
49
+ if (isObject(item))
50
+ byId.set(item.id, item);
51
+ return calls.map((c, index) => {
52
+ const envelope = byId.get(index);
53
+ if (!envelope) {
54
+ return {
55
+ ok: false,
56
+ error: new RpcError(RpcErrorCode.InternalError, `No response for "${c.method}"`),
57
+ };
58
+ }
59
+ if (envelope.error !== undefined) {
60
+ return { ok: false, error: toRpcError(envelope.error) };
61
+ }
62
+ const value = c.parse ? c.parse(envelope.result) : envelope.result;
63
+ return { ok: true, value };
64
+ });
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Comet — agnostic JSON-RPC 2.0 protocol + isomorphic, transport-injectable
3
+ * client. Zero framework, zero transport, zero dependency. The browser binding
4
+ * (aurora) and the server binding (Ream's `RpcRouter`) both build on this.
5
+ */
6
+ export { createRpcClient, type RpcCall, type RpcCallOptions, type RpcClient, type RpcClientOptions, type RpcResult, type RpcTransport, } from "./client.js";
7
+ export { buildError, buildRequest, buildSuccess, isNotification, isObject, isRpcError, isRpcShapedError, type JsonRpcErrorObject, type JsonRpcErrorResponse, type JsonRpcId, type JsonRpcRequest, type JsonRpcResponse, type JsonRpcSuccessResponse, type ParsedRpcRequest, parseRequest, RpcError, RpcErrorCode, toRpcError, } from "./protocol.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACN,eAAe,EACf,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,YAAY,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACrB,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,UAAU,GACV,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Comet — agnostic JSON-RPC 2.0 protocol + isomorphic, transport-injectable
3
+ * client. Zero framework, zero transport, zero dependency. The browser binding
4
+ * (aurora) and the server binding (Ream's `RpcRouter`) both build on this.
5
+ */
6
+ export { createRpcClient, } from "./client.js";
7
+ export { buildError, buildRequest, buildSuccess, isNotification, isObject, isRpcError, isRpcShapedError, parseRequest, RpcError, RpcErrorCode, toRpcError, } from "./protocol.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACN,eAAe,GAOf,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,UAAU,EACV,gBAAgB,EAQhB,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,UAAU,GACV,MAAM,eAAe,CAAC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * JSON-RPC 2.0 protocol primitives — the agnostic core shared by every Comet
3
+ * consumer: the isomorphic {@link "./client".createRpcClient} AND any server
4
+ * binding (Ream's `RpcRouter` builds on these instead of hand-rolling its own).
5
+ *
6
+ * Zero transport, zero framework, zero dependency — just the envelope shapes,
7
+ * the reserved error codes, builders, and the request parser/notification rule
8
+ * from the spec (https://www.jsonrpc.org/specification).
9
+ */
10
+ /** A JSON-RPC id — a string, a number, or `null` (spec §4). */
11
+ export type JsonRpcId = string | number | null;
12
+ /** An outgoing JSON-RPC 2.0 request envelope. */
13
+ export interface JsonRpcRequest {
14
+ jsonrpc: "2.0";
15
+ method: string;
16
+ params?: unknown;
17
+ id: JsonRpcId;
18
+ }
19
+ /** The `error` member of a JSON-RPC 2.0 error response. */
20
+ export interface JsonRpcErrorObject {
21
+ code: number;
22
+ message: string;
23
+ data?: unknown;
24
+ }
25
+ /** A successful JSON-RPC 2.0 response envelope. */
26
+ export interface JsonRpcSuccessResponse {
27
+ jsonrpc: "2.0";
28
+ result: unknown;
29
+ id: JsonRpcId;
30
+ }
31
+ /** An error JSON-RPC 2.0 response envelope. */
32
+ export interface JsonRpcErrorResponse {
33
+ jsonrpc: "2.0";
34
+ error: JsonRpcErrorObject;
35
+ id: JsonRpcId;
36
+ }
37
+ export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
38
+ /**
39
+ * The reserved JSON-RPC 2.0 error codes (spec §5.1). Domain handlers are free to
40
+ * use codes outside the reserved `-32768..-32000` range for their own errors.
41
+ */
42
+ export declare const RpcErrorCode: {
43
+ readonly ParseError: -32700;
44
+ readonly InvalidRequest: -32600;
45
+ readonly MethodNotFound: -32601;
46
+ readonly InvalidParams: -32602;
47
+ readonly InternalError: -32603;
48
+ };
49
+ /** A JSON-RPC 2.0 error surfaced as a throwable (carries `code` + optional `data`). */
50
+ export declare class RpcError extends Error {
51
+ readonly code: number;
52
+ readonly data?: unknown;
53
+ constructor(code: number, message: string, data?: unknown);
54
+ }
55
+ /** Type guard for {@link RpcError}. */
56
+ export declare function isRpcError(value: unknown): value is RpcError;
57
+ /** Narrow an unknown to a plain object (non-null). */
58
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
59
+ /**
60
+ * Turn a JSON-RPC `error` member (untrusted wire value) into an {@link RpcError}.
61
+ * Falls back to an internal-error when the shape is malformed.
62
+ */
63
+ export declare function toRpcError(error: unknown): RpcError;
64
+ /**
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.
68
+ */
69
+ export declare function isRpcShapedError(err: unknown): err is {
70
+ code: number;
71
+ message?: unknown;
72
+ data?: unknown;
73
+ };
74
+ /** Build an outgoing request envelope. */
75
+ export declare function buildRequest(method: string, params: unknown, id: JsonRpcId): JsonRpcRequest;
76
+ /** Build a success response envelope. */
77
+ export declare function buildSuccess(result: unknown, id: JsonRpcId): JsonRpcSuccessResponse;
78
+ /** Build an error response envelope (omits `data` when not supplied). */
79
+ export declare function buildError(code: number, message: string, id: JsonRpcId, data?: unknown): JsonRpcErrorResponse;
80
+ /** Result of {@link parseRequest}. */
81
+ export type ParsedRpcRequest = {
82
+ ok: true;
83
+ method: string;
84
+ params: unknown;
85
+ id: JsonRpcId;
86
+ } | {
87
+ ok: false;
88
+ response: JsonRpcErrorResponse;
89
+ };
90
+ /**
91
+ * Validate an incoming JSON-RPC envelope and extract `method`/`params`/`id`.
92
+ * Returns an `InvalidRequest` error response when the version/method are wrong.
93
+ */
94
+ export declare function parseRequest(request: unknown): ParsedRpcRequest;
95
+ /**
96
+ * A JSON-RPC notification is a well-formed request with NO `id` member. The spec
97
+ * (§4.1) says the server MUST NOT reply to one — it still runs for side-effects.
98
+ * A malformed object (no method / wrong version) is NOT a notification.
99
+ */
100
+ export declare function isNotification(request: unknown): boolean;
101
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +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,CA8B/D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CASxD"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * JSON-RPC 2.0 protocol primitives — the agnostic core shared by every Comet
3
+ * consumer: the isomorphic {@link "./client".createRpcClient} AND any server
4
+ * binding (Ream's `RpcRouter` builds on these instead of hand-rolling its own).
5
+ *
6
+ * Zero transport, zero framework, zero dependency — just the envelope shapes,
7
+ * the reserved error codes, builders, and the request parser/notification rule
8
+ * from the spec (https://www.jsonrpc.org/specification).
9
+ */
10
+ /**
11
+ * The reserved JSON-RPC 2.0 error codes (spec §5.1). Domain handlers are free to
12
+ * use codes outside the reserved `-32768..-32000` range for their own errors.
13
+ */
14
+ export const RpcErrorCode = {
15
+ ParseError: -32700,
16
+ InvalidRequest: -32600,
17
+ MethodNotFound: -32601,
18
+ InvalidParams: -32602,
19
+ InternalError: -32603,
20
+ };
21
+ /** A JSON-RPC 2.0 error surfaced as a throwable (carries `code` + optional `data`). */
22
+ export class RpcError extends Error {
23
+ code;
24
+ data;
25
+ constructor(code, message, data) {
26
+ super(message);
27
+ this.name = "RpcError";
28
+ this.code = code;
29
+ this.data = data;
30
+ }
31
+ }
32
+ /** Type guard for {@link RpcError}. */
33
+ export function isRpcError(value) {
34
+ return value instanceof RpcError;
35
+ }
36
+ /** Narrow an unknown to a plain object (non-null). */
37
+ export function isObject(value) {
38
+ return typeof value === "object" && value !== null;
39
+ }
40
+ /**
41
+ * Turn a JSON-RPC `error` member (untrusted wire value) into an {@link RpcError}.
42
+ * Falls back to an internal-error when the shape is malformed.
43
+ */
44
+ export function toRpcError(error) {
45
+ if (isObject(error) &&
46
+ typeof error.code === "number" &&
47
+ typeof error.message === "string") {
48
+ return new RpcError(error.code, error.message, error.data);
49
+ }
50
+ return new RpcError(RpcErrorCode.InternalError, "Malformed JSON-RPC error envelope", error);
51
+ }
52
+ /**
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.
56
+ */
57
+ export function isRpcShapedError(err) {
58
+ return isObject(err) && typeof err.code === "number";
59
+ }
60
+ /** Build an outgoing request envelope. */
61
+ export function buildRequest(method, params, id) {
62
+ return { jsonrpc: "2.0", method, params, id };
63
+ }
64
+ /** Build a success response envelope. */
65
+ export function buildSuccess(result, id) {
66
+ return { jsonrpc: "2.0", result, id };
67
+ }
68
+ /** Build an error response envelope (omits `data` when not supplied). */
69
+ export function buildError(code, message, id, data) {
70
+ return {
71
+ jsonrpc: "2.0",
72
+ error: data === undefined ? { code, message } : { code, message, data },
73
+ id,
74
+ };
75
+ }
76
+ /**
77
+ * Validate an incoming JSON-RPC envelope and extract `method`/`params`/`id`.
78
+ * Returns an `InvalidRequest` error response when the version/method are wrong.
79
+ */
80
+ export function parseRequest(request) {
81
+ if (!isObject(request)) {
82
+ return {
83
+ ok: false,
84
+ response: buildError(RpcErrorCode.InvalidRequest, "Invalid Request", null),
85
+ };
86
+ }
87
+ const jsonrpc = "jsonrpc" in request && typeof request.jsonrpc === "string"
88
+ ? request.jsonrpc
89
+ : undefined;
90
+ const method = "method" in request && typeof request.method === "string"
91
+ ? request.method
92
+ : undefined;
93
+ const params = "params" in request ? request.params : undefined;
94
+ const rawId = "id" in request ? request.id : undefined;
95
+ const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
96
+ if (jsonrpc !== "2.0" || !method) {
97
+ return {
98
+ ok: false,
99
+ response: buildError(RpcErrorCode.InvalidRequest, "Invalid Request", id),
100
+ };
101
+ }
102
+ return { ok: true, method, params, id };
103
+ }
104
+ /**
105
+ * A JSON-RPC notification is a well-formed request with NO `id` member. The spec
106
+ * (§4.1) says the server MUST NOT reply to one — it still runs for side-effects.
107
+ * A malformed object (no method / wrong version) is NOT a notification.
108
+ */
109
+ export function isNotification(request) {
110
+ return (isObject(request) &&
111
+ "jsonrpc" in request &&
112
+ request.jsonrpc === "2.0" &&
113
+ "method" in request &&
114
+ typeof request.method === "string" &&
115
+ !("id" in request));
116
+ }
117
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@c9up/comet",
3
+ "version": "0.1.0",
4
+ "description": "Comet — agnostic JSON-RPC 2.0 protocol + isomorphic, transport-injectable client for the Ream framework",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./protocol": {
15
+ "types": "./dist/protocol.d.ts",
16
+ "import": "./dist/protocol.js"
17
+ },
18
+ "./client": {
19
+ "types": "./dist/client.d.ts",
20
+ "import": "./dist/client.js"
21
+ }
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.19.15",
25
+ "typescript": "^6.0.2",
26
+ "vitest": "^4.1.2"
27
+ },
28
+ "engines": {
29
+ "node": ">=22.0.0"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/C9up/comet.git"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "test": "vitest run",
47
+ "lint": "biome check src/",
48
+ "test:coverage": "vitest run --coverage",
49
+ "typecheck": "tsc --noEmit"
50
+ }
51
+ }
package/src/client.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Isomorphic JSON-RPC 2.0 client — the protocol logic (single + batch) over an
3
+ * INJECTED transport. It owns the envelope/id/error handling (from
4
+ * {@link "./protocol"}) and knows nothing about how bytes travel: pass a browser
5
+ * transport (aurora's HttpClient) or a Node one (fetch/undici). This is the
6
+ * adapter seam that lets the client live outside any UI or server framework.
7
+ *
8
+ * const rpc = createRpcClient({ transport: (url, body, { signal }) =>
9
+ * fetch(url, { method: 'POST', body: JSON.stringify(body), signal })
10
+ * .then((r) => r.json()) })
11
+ * const out = await rpc.call('task.validate', { id })
12
+ */
13
+ import {
14
+ buildRequest,
15
+ isObject,
16
+ RpcError,
17
+ RpcErrorCode,
18
+ toRpcError,
19
+ } from "./protocol.js";
20
+
21
+ /**
22
+ * Sends a JSON body to `url` and resolves the parsed JSON response. Injected via
23
+ * {@link RpcClientOptions.transport}; the client never touches `fetch` directly.
24
+ */
25
+ export type RpcTransport = (
26
+ url: string,
27
+ body: unknown,
28
+ options: { signal?: AbortSignal },
29
+ ) => Promise<unknown>;
30
+
31
+ export interface RpcClientOptions {
32
+ /** How requests are sent — the only required wiring. */
33
+ transport: RpcTransport;
34
+ /** Endpoint path. Default `/rpc`. */
35
+ url?: string;
36
+ }
37
+
38
+ /** Per-call options for {@link RpcClient.call}. */
39
+ export interface RpcCallOptions<T = unknown> {
40
+ /**
41
+ * Validate the result at runtime, returning the typed value — skips the
42
+ * unchecked `T` assertion (the cast-free escape hatch).
43
+ */
44
+ parse?: (data: unknown) => T;
45
+ /** Abort signal — abort it to cancel the request (e.g. on unmount / new keystroke). */
46
+ signal?: AbortSignal;
47
+ }
48
+
49
+ /** One call in a batch. `parse` optionally validates that call's result (cast-free). */
50
+ export interface RpcCall<T = unknown> {
51
+ method: string;
52
+ params?: unknown;
53
+ parse?: (data: unknown) => T;
54
+ }
55
+
56
+ /** A settled batch entry — the result, or the JSON-RPC error for that call. */
57
+ export type RpcResult<T = unknown> =
58
+ | { ok: true; value: T }
59
+ | { ok: false; error: RpcError };
60
+
61
+ export interface RpcClient {
62
+ /**
63
+ * Call one method. Returns the result, or throws {@link RpcError} on a
64
+ * JSON-RPC error. The `jsonrpc`/`id` envelope is handled internally. Pass
65
+ * `options.parse` to validate the result at runtime (skips the unchecked `T`
66
+ * assertion) and `options.signal` to make the call abortable.
67
+ */
68
+ call<T = unknown>(
69
+ method: string,
70
+ params?: unknown,
71
+ options?: RpcCallOptions<T>,
72
+ ): Promise<T>;
73
+ /**
74
+ * Send a JSON-RPC batch. Returns one settled entry per call, in request
75
+ * order. `options.signal` aborts the whole batch (it is one HTTP request).
76
+ */
77
+ batch(
78
+ calls: RpcCall[],
79
+ options?: { signal?: AbortSignal },
80
+ ): Promise<RpcResult[]>;
81
+ }
82
+
83
+ export function createRpcClient(options: RpcClientOptions): RpcClient {
84
+ const { transport } = options;
85
+ const url = options.url ?? "/rpc";
86
+ let nextId = 0;
87
+
88
+ return {
89
+ async call<T>(
90
+ method: string,
91
+ params?: unknown,
92
+ callOptions?: RpcCallOptions<T>,
93
+ ): Promise<T> {
94
+ const id = ++nextId;
95
+ const res = await transport(url, buildRequest(method, params, id), {
96
+ signal: callOptions?.signal,
97
+ });
98
+ if (!isObject(res)) {
99
+ throw new RpcError(
100
+ RpcErrorCode.InternalError,
101
+ `Malformed JSON-RPC response for "${method}"`,
102
+ );
103
+ }
104
+ if (res.error !== undefined) throw toRpcError(res.error);
105
+ // Result boundary — the same unchecked `T` assertion HTTP clients use,
106
+ // with `parse` as the cast-free, runtime-validated escape hatch.
107
+ return callOptions?.parse
108
+ ? callOptions.parse(res.result)
109
+ : (res.result as T);
110
+ },
111
+
112
+ async batch(
113
+ calls: RpcCall[],
114
+ batchOptions?: { signal?: AbortSignal },
115
+ ): Promise<RpcResult[]> {
116
+ if (calls.length === 0) return [];
117
+ const requests = calls.map((c, index) =>
118
+ // index = request position; responses are matched back by id
119
+ buildRequest(c.method, c.params, index),
120
+ );
121
+ const res = await transport(url, requests, {
122
+ signal: batchOptions?.signal,
123
+ });
124
+ if (!Array.isArray(res)) {
125
+ throw new RpcError(
126
+ RpcErrorCode.InternalError,
127
+ "Malformed JSON-RPC batch response",
128
+ );
129
+ }
130
+ const byId = new Map<unknown, Record<string, unknown>>();
131
+ for (const item of res) if (isObject(item)) byId.set(item.id, item);
132
+ return calls.map((c, index) => {
133
+ const envelope = byId.get(index);
134
+ if (!envelope) {
135
+ return {
136
+ ok: false,
137
+ error: new RpcError(
138
+ RpcErrorCode.InternalError,
139
+ `No response for "${c.method}"`,
140
+ ),
141
+ };
142
+ }
143
+ if (envelope.error !== undefined) {
144
+ return { ok: false, error: toRpcError(envelope.error) };
145
+ }
146
+ const value = c.parse ? c.parse(envelope.result) : envelope.result;
147
+ return { ok: true, value };
148
+ });
149
+ },
150
+ };
151
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Comet — agnostic JSON-RPC 2.0 protocol + isomorphic, transport-injectable
3
+ * client. Zero framework, zero transport, zero dependency. The browser binding
4
+ * (aurora) and the server binding (Ream's `RpcRouter`) both build on this.
5
+ */
6
+ export {
7
+ createRpcClient,
8
+ type RpcCall,
9
+ type RpcCallOptions,
10
+ type RpcClient,
11
+ type RpcClientOptions,
12
+ type RpcResult,
13
+ type RpcTransport,
14
+ } from "./client.js";
15
+ export {
16
+ buildError,
17
+ buildRequest,
18
+ buildSuccess,
19
+ isNotification,
20
+ isObject,
21
+ isRpcError,
22
+ isRpcShapedError,
23
+ type JsonRpcErrorObject,
24
+ type JsonRpcErrorResponse,
25
+ type JsonRpcId,
26
+ type JsonRpcRequest,
27
+ type JsonRpcResponse,
28
+ type JsonRpcSuccessResponse,
29
+ type ParsedRpcRequest,
30
+ parseRequest,
31
+ RpcError,
32
+ RpcErrorCode,
33
+ toRpcError,
34
+ } from "./protocol.js";
@@ -0,0 +1,195 @@
1
+ /**
2
+ * JSON-RPC 2.0 protocol primitives — the agnostic core shared by every Comet
3
+ * consumer: the isomorphic {@link "./client".createRpcClient} AND any server
4
+ * binding (Ream's `RpcRouter` builds on these instead of hand-rolling its own).
5
+ *
6
+ * Zero transport, zero framework, zero dependency — just the envelope shapes,
7
+ * the reserved error codes, builders, and the request parser/notification rule
8
+ * from the spec (https://www.jsonrpc.org/specification).
9
+ */
10
+
11
+ /** A JSON-RPC id — a string, a number, or `null` (spec §4). */
12
+ export type JsonRpcId = string | number | null;
13
+
14
+ /** An outgoing JSON-RPC 2.0 request envelope. */
15
+ export interface JsonRpcRequest {
16
+ jsonrpc: "2.0";
17
+ method: string;
18
+ params?: unknown;
19
+ id: JsonRpcId;
20
+ }
21
+
22
+ /** The `error` member of a JSON-RPC 2.0 error response. */
23
+ export interface JsonRpcErrorObject {
24
+ code: number;
25
+ message: string;
26
+ data?: unknown;
27
+ }
28
+
29
+ /** A successful JSON-RPC 2.0 response envelope. */
30
+ export interface JsonRpcSuccessResponse {
31
+ jsonrpc: "2.0";
32
+ result: unknown;
33
+ id: JsonRpcId;
34
+ }
35
+
36
+ /** An error JSON-RPC 2.0 response envelope. */
37
+ export interface JsonRpcErrorResponse {
38
+ jsonrpc: "2.0";
39
+ error: JsonRpcErrorObject;
40
+ id: JsonRpcId;
41
+ }
42
+
43
+ export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
44
+
45
+ /**
46
+ * The reserved JSON-RPC 2.0 error codes (spec §5.1). Domain handlers are free to
47
+ * use codes outside the reserved `-32768..-32000` range for their own errors.
48
+ */
49
+ export const RpcErrorCode = {
50
+ ParseError: -32700,
51
+ InvalidRequest: -32600,
52
+ MethodNotFound: -32601,
53
+ InvalidParams: -32602,
54
+ InternalError: -32603,
55
+ } as const;
56
+
57
+ /** A JSON-RPC 2.0 error surfaced as a throwable (carries `code` + optional `data`). */
58
+ export class RpcError extends Error {
59
+ readonly code: number;
60
+ readonly data?: unknown;
61
+ constructor(code: number, message: string, data?: unknown) {
62
+ super(message);
63
+ this.name = "RpcError";
64
+ this.code = code;
65
+ this.data = data;
66
+ }
67
+ }
68
+
69
+ /** Type guard for {@link RpcError}. */
70
+ export function isRpcError(value: unknown): value is RpcError {
71
+ return value instanceof RpcError;
72
+ }
73
+
74
+ /** Narrow an unknown to a plain object (non-null). */
75
+ export function isObject(value: unknown): value is Record<string, unknown> {
76
+ return typeof value === "object" && value !== null;
77
+ }
78
+
79
+ /**
80
+ * Turn a JSON-RPC `error` member (untrusted wire value) into an {@link RpcError}.
81
+ * Falls back to an internal-error when the shape is malformed.
82
+ */
83
+ export function toRpcError(error: unknown): RpcError {
84
+ if (
85
+ isObject(error) &&
86
+ typeof error.code === "number" &&
87
+ typeof error.message === "string"
88
+ ) {
89
+ return new RpcError(error.code, error.message, error.data);
90
+ }
91
+ return new RpcError(
92
+ RpcErrorCode.InternalError,
93
+ "Malformed JSON-RPC error envelope",
94
+ error,
95
+ );
96
+ }
97
+
98
+ /**
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.
102
+ */
103
+ export function isRpcShapedError(
104
+ err: unknown,
105
+ ): err is { code: number; message?: unknown; data?: unknown } {
106
+ return isObject(err) && typeof err.code === "number";
107
+ }
108
+
109
+ /** Build an outgoing request envelope. */
110
+ export function buildRequest(
111
+ method: string,
112
+ params: unknown,
113
+ id: JsonRpcId,
114
+ ): JsonRpcRequest {
115
+ return { jsonrpc: "2.0", method, params, id };
116
+ }
117
+
118
+ /** Build a success response envelope. */
119
+ export function buildSuccess(
120
+ result: unknown,
121
+ id: JsonRpcId,
122
+ ): JsonRpcSuccessResponse {
123
+ return { jsonrpc: "2.0", result, id };
124
+ }
125
+
126
+ /** Build an error response envelope (omits `data` when not supplied). */
127
+ export function buildError(
128
+ code: number,
129
+ message: string,
130
+ id: JsonRpcId,
131
+ data?: unknown,
132
+ ): JsonRpcErrorResponse {
133
+ return {
134
+ jsonrpc: "2.0",
135
+ error: data === undefined ? { code, message } : { code, message, data },
136
+ id,
137
+ };
138
+ }
139
+
140
+ /** Result of {@link parseRequest}. */
141
+ export type ParsedRpcRequest =
142
+ | { ok: true; method: string; params: unknown; id: JsonRpcId }
143
+ | { ok: false; response: JsonRpcErrorResponse };
144
+
145
+ /**
146
+ * Validate an incoming JSON-RPC envelope and extract `method`/`params`/`id`.
147
+ * Returns an `InvalidRequest` error response when the version/method are wrong.
148
+ */
149
+ export function parseRequest(request: unknown): ParsedRpcRequest {
150
+ if (!isObject(request)) {
151
+ return {
152
+ ok: false,
153
+ response: buildError(
154
+ RpcErrorCode.InvalidRequest,
155
+ "Invalid Request",
156
+ null,
157
+ ),
158
+ };
159
+ }
160
+ const jsonrpc =
161
+ "jsonrpc" in request && typeof request.jsonrpc === "string"
162
+ ? request.jsonrpc
163
+ : undefined;
164
+ const method =
165
+ "method" in request && typeof request.method === "string"
166
+ ? request.method
167
+ : undefined;
168
+ const params = "params" in request ? request.params : undefined;
169
+ const rawId = "id" in request ? request.id : undefined;
170
+ const id: JsonRpcId =
171
+ typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
172
+ if (jsonrpc !== "2.0" || !method) {
173
+ return {
174
+ ok: false,
175
+ response: buildError(RpcErrorCode.InvalidRequest, "Invalid Request", id),
176
+ };
177
+ }
178
+ return { ok: true, method, params, id };
179
+ }
180
+
181
+ /**
182
+ * A JSON-RPC notification is a well-formed request with NO `id` member. The spec
183
+ * (§4.1) says the server MUST NOT reply to one — it still runs for side-effects.
184
+ * A malformed object (no method / wrong version) is NOT a notification.
185
+ */
186
+ export function isNotification(request: unknown): boolean {
187
+ return (
188
+ isObject(request) &&
189
+ "jsonrpc" in request &&
190
+ request.jsonrpc === "2.0" &&
191
+ "method" in request &&
192
+ typeof request.method === "string" &&
193
+ !("id" in request)
194
+ );
195
+ }