@graphorin/client 0.6.1 → 0.7.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/CHANGELOG.md +21 -0
- package/README.md +2 -2
- package/dist/errors.d.ts +7 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/graphorin-client.d.ts +22 -0
- package/dist/graphorin-client.d.ts.map +1 -1
- package/dist/graphorin-client.js +17 -2
- package/dist/graphorin-client.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/reconnect.d.ts +3 -3
- package/dist/reconnect.js +2 -2
- package/dist/reconnect.js.map +1 -1
- package/dist/transport/types.d.ts +1 -1
- package/package.json +12 -11
- package/src/errors.ts +170 -0
- package/src/graphorin-client.ts +1075 -0
- package/src/index.ts +49 -0
- package/src/reconnect.ts +91 -0
- package/src/transport/index.ts +16 -0
- package/src/transport/sse.ts +174 -0
- package/src/transport/types.ts +106 -0
- package/src/transport/ws.ts +211 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @graphorin/client
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#154](https://github.com/o-stepper/graphorin/pull/154) [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04) Thanks [@o-stepper](https://github.com/o-stepper)! - W-072: every export map's `import` condition becomes `default`, and the Node floor rises to `>=22.12.0`.
|
|
8
|
+
|
|
9
|
+
CJS consumers previously hit a bewildering `ERR_PACKAGE_PATH_NOT_EXPORTED` instead of a clear ESM-only signal. With the `default` condition, plain `require('@graphorin/core')` works via Node's stable `require(esm)` - which shipped in 22.12, hence the engines bump across every workspace manifest (packages, examples, benchmarks, docs; enforced by the widened mvp-readiness sweep). No dual-instance hazard: there is no CJS build, `require()` returns the same ESM module instance. ESM consumers are unaffected (`default` serves both paths; `types` stays first). The pack gate now runs attw under the full `node16` profile (was `esm-only`) and adds a runtime `require(esm)` smoke against the packed tarballs. Installs on Node 22.0-22.11 with `engine-strict` will refuse - upgrade Node (see the migration guide).
|
|
10
|
+
|
|
11
|
+
- [#158](https://github.com/o-stepper/graphorin/pull/158) [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab) Thanks [@o-stepper](https://github.com/o-stepper)! - The per-subscription client event buffer is bounded (W-152): when a slow `for await` consumer lets the queue reach `GraphorinClientOptions.subscriptionQueueLimit` (default 10000, 10x the server's per-connection default; `0` restores the old unbounded behavior), the subscription closes with the new typed `flow-overflow` `GraphorinClientErrorKind` - a deterministic error in the iterator instead of unbounded heap growth or silent frame loss, mirroring the server's queue-overflow close. Frames arriving after close no longer grow the dead queue, and `SubscriptionMetadata.queuedEvents` exposes the live buffer depth. The `@stable` error-kind union grows additively; exhaustive switches without a default branch must add the member.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#160](https://github.com/o-stepper/graphorin/pull/160) [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156) Thanks [@o-stepper](https://github.com/o-stepper)! - W-108: when a `transport: 'auto'` client reconnects and falls back from WebSocket to SSE, surviving WS subscriptions are now closed with a typed `TransportFailedError` instead of hanging their `for await` consumers forever. SSE carries exactly one bound-session subject, so agent/workflow subscriptions are fundamentally unresumable over it - the deterministic error (with a recovery hint: force `transport: 'ws'` or read the bound session subject) hands the decision to the application; an endless WS retry would wedge against servers with WS disabled. The bound `'__sse__'` subscription is untouched, so the SSE-first resume path (periphery-03) is unchanged. The `transport` option TSDoc documents the behaviour.
|
|
16
|
+
|
|
17
|
+
- [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - TSDoc `{@link}` hygiene sweep (W-130): all 55 broken links found by TypeDoc's now-enabled `validation.invalidLink` are fixed - two resolved to their real targets (`GraphorinMCPError` was misnamed `MCPError`), the rest (cross-package, `import()`-form, unexported-constant, and DOM-type references that have never rendered as hrefs) converted to plain inline code. The docs build now fails on any new broken `{@link}` via a scoped gate.
|
|
18
|
+
|
|
19
|
+
- [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Tarballs now ship `src/` so the published `dist/**/*.d.ts.map` files actually work (W-136): the maps referenced `../src/*.ts` that the `files` whitelist excluded, so go-to-definition fell back into `.d.ts` and the shipped maps were dead weight. The pack gate gains a `map-integrity` leg: every source referenced by a shipped map must resolve inside the tarball (or be embedded via `sourcesContent`), with an anti-vacuous guard - a package whose tsdown config emits declaration maps must contain a non-zero number of `.d.ts.map` files, so a cache-restored dist that silently dropped maps fails the gate instead of passing vacuously. `mvp-readiness` now requires `src` in every publishable `files` array.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [[`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04)]:
|
|
22
|
+
- @graphorin/protocol@0.7.0
|
|
23
|
+
|
|
3
24
|
## 0.6.1
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
- **Version:** v0.
|
|
8
|
+
- **Version:** v0.7.0
|
|
9
9
|
- **License:** [MIT](./LICENSE) (© 2026 Oleksiy Stepurenko)
|
|
10
10
|
- **Repository:** <https://github.com/o-stepper/graphorin/tree/main/packages/client>
|
|
11
11
|
- **Issues:** <https://github.com/o-stepper/graphorin/issues>
|
|
@@ -89,4 +89,4 @@ MIT © 2026 Oleksiy Stepurenko. See [`LICENSE`](./LICENSE).
|
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
92
|
-
**Project Graphorin** · v0.
|
|
92
|
+
**Project Graphorin** · v0.7.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
package/dist/errors.d.ts
CHANGED
|
@@ -19,7 +19,13 @@ declare function kindForRpcCode(code: number): GraphorinClientErrorKind;
|
|
|
19
19
|
*
|
|
20
20
|
* @stable
|
|
21
21
|
*/
|
|
22
|
-
type GraphorinClientErrorKind = 'client-not-connected' | 'transport-failed' | 'subprotocol-mismatch' | 'auth-failed' | 'protocol-violation' | 'subscription-not-found' | 'rate-limited' | 'scope-denied' | 'run-not-found' | 'server-error' | 'aborted'
|
|
22
|
+
type GraphorinClientErrorKind = 'client-not-connected' | 'transport-failed' | 'subprotocol-mismatch' | 'auth-failed' | 'protocol-violation' | 'subscription-not-found' | 'rate-limited' | 'scope-denied' | 'run-not-found' | 'server-error' | 'aborted'
|
|
23
|
+
/**
|
|
24
|
+
* W-152: the per-subscription client buffer hit
|
|
25
|
+
* `subscriptionQueueLimit` because the `for await` consumer fell
|
|
26
|
+
* behind the event flow; the subscription is closed with this typed
|
|
27
|
+
* error instead of growing the heap or silently dropping frames.
|
|
28
|
+
*/ | 'flow-overflow' | 'invalid-server-frame';
|
|
23
29
|
/**
|
|
24
30
|
* Base class for every error raised by `@graphorin/client`. Carries a
|
|
25
31
|
* stable {@link GraphorinClientErrorKind} discriminator and an
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"sourcesContent":[],"mappings":";;AAiBA;AA2BA;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"sourcesContent":[],"mappings":";;AAiBA;AA2BA;AA8BA;;;;;;AAWA;AAQA;AAWA;AAgBA;AAQA;AAQa,iBAvHG,cAAA,CAuHuB,IAAQ,EAAA,MAAA,CAAA,EAvHD,wBAuHqB;AAcnE;AAQA;;;;AAKY,KAvHA,wBAAA,GAuHA,sBAAA,GAAA,kBAAA,GAAA,sBAAA,GAAA,aAAA,GAAA,oBAAA,GAAA,wBAAA,GAAA,cAAA,GAAA,cAAA,GAAA,eAAA,GAAA,cAAA,GAAA;;;;;;;;;;;;;;cAzFC,oBAAA,SAA6B,KAAA;iBACzB;oBAEG,qDAAoD;;;cAQ3D,uBAAA,SAAgC,oBAAA;;;;cAQhC,oBAAA,SAA6B,oBAAA;;yCAGF;;;;;cAQ3B,wBAAA,SAAiC,oBAAA;;;;;;cAgBjC,eAAA,SAAwB,oBAAA;;;;cAQxB,sBAAA,SAA+B,oBAAA;yCACJ;;;cAO3B,yBAAA,SAAkC,oBAAA;;;;;cAclC,kBAAA,SAA2B,oBAAA;;;;cAQ3B,uBAAA,SAAgC,oBAAA;mBAC1B;UAAsB;;;uCAI7B;UAAsB"}
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Typed error hierarchy surfaced by `@graphorin/client`. Every error\n * class extends the JavaScript built-in `Error` and exposes a stable\n * `kind` discriminator so consumers can pattern-match without\n * relying on `instanceof` (which behaves badly across module-system\n * boundaries when the package is dual-loaded).\n *\n * @packageDocumentation\n */\n\nimport { RPC_ERROR_CODES } from '@graphorin/protocol';\n\n/**\n * IP-19: map a JSON-RPC error code from a server `RpcFailure` frame to the\n * client's discriminated {@link GraphorinClientErrorKind}, so a rate-limited\n * or scope-denied RPC is distinguishable from a genuine protocol violation.\n */\nexport function kindForRpcCode(code: number): GraphorinClientErrorKind {\n switch (code) {\n case RPC_ERROR_CODES.RATE_LIMITED:\n return 'rate-limited';\n case RPC_ERROR_CODES.SCOPE_DENIED:\n return 'scope-denied';\n case RPC_ERROR_CODES.AUTH_REQUIRED:\n case RPC_ERROR_CODES.AUTH_INVALID:\n return 'auth-failed';\n case RPC_ERROR_CODES.RUN_NOT_FOUND:\n return 'run-not-found';\n case RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND:\n return 'subscription-not-found';\n case RPC_ERROR_CODES.INTERNAL_ERROR:\n return 'server-error';\n default:\n // INVALID_REQUEST / INVALID_PARAMS / METHOD_NOT_FOUND / PROTOCOL_VIOLATION\n // and any unknown code stay a protocol violation.\n return 'protocol-violation';\n }\n}\n\n/**\n * Discriminator union of every error kind raised by the client.\n *\n * @stable\n */\nexport type GraphorinClientErrorKind =\n | 'client-not-connected'\n | 'transport-failed'\n | 'subprotocol-mismatch'\n | 'auth-failed'\n | 'protocol-violation'\n | 'subscription-not-found'\n // IP-19: discriminated RPC-failure kinds so callers can branch on the\n // server's error class instead of pattern-matching the message string.\n | 'rate-limited'\n | 'scope-denied'\n | 'run-not-found'\n | 'server-error'\n | 'aborted'\n | 'invalid-server-frame';\n\n/**\n * Base class for every error raised by `@graphorin/client`. Carries a\n * stable {@link GraphorinClientErrorKind} discriminator and an\n * optional `cause` chain.\n *\n * @stable\n */\nexport class GraphorinClientError extends Error {\n readonly kind: GraphorinClientErrorKind;\n\n constructor(kind: GraphorinClientErrorKind, message: string, options: ErrorOptions = {}) {\n super(message, options);\n this.kind = kind;\n this.name = 'GraphorinClientError';\n }\n}\n\n/** @stable */\nexport class ClientNotConnectedError extends GraphorinClientError {\n constructor(message = 'GraphorinClient is not connected. Call connect() first.') {\n super('client-not-connected', message);\n this.name = 'ClientNotConnectedError';\n }\n}\n\n/** @stable */\nexport class TransportFailedError extends GraphorinClientError {\n readonly code: number | undefined;\n\n constructor(message: string, options: ErrorOptions & { readonly code?: number } = {}) {\n super('transport-failed', message, options);\n this.name = 'TransportFailedError';\n this.code = options.code;\n }\n}\n\n/** @stable */\nexport class SubprotocolMismatchError extends GraphorinClientError {\n readonly expected: string;\n readonly actual: string | null;\n\n constructor(expected: string, actual: string | null) {\n super(\n 'subprotocol-mismatch',\n `Server selected subprotocol '${actual ?? '<none>'}'; expected '${expected}'.`,\n );\n this.name = 'SubprotocolMismatchError';\n this.expected = expected;\n this.actual = actual;\n }\n}\n\n/** @stable */\nexport class AuthFailedError extends GraphorinClientError {\n constructor(message = 'Authentication failed. Re-mint a token or request a new ticket.') {\n super('auth-failed', message);\n this.name = 'AuthFailedError';\n }\n}\n\n/** @stable */\nexport class ProtocolViolationError extends GraphorinClientError {\n constructor(message: string, options: ErrorOptions = {}) {\n super('protocol-violation', message, options);\n this.name = 'ProtocolViolationError';\n }\n}\n\n/** @stable */\nexport class SubscriptionNotFoundError extends GraphorinClientError {\n readonly subscriptionId: string;\n\n constructor(subscriptionId: string) {\n super(\n 'subscription-not-found',\n `Subscription '${subscriptionId}' is not active on this client.`,\n );\n this.name = 'SubscriptionNotFoundError';\n this.subscriptionId = subscriptionId;\n }\n}\n\n/** @stable */\nexport class ClientAbortedError extends GraphorinClientError {\n constructor(message = 'Operation aborted before completion.') {\n super('aborted', message);\n this.name = 'ClientAbortedError';\n }\n}\n\n/** @stable */\nexport class InvalidServerFrameError extends GraphorinClientError {\n readonly issues: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>;\n\n constructor(\n message: string,\n issues: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>,\n ) {\n super('invalid-server-frame', message);\n this.name = 'InvalidServerFrameError';\n this.issues = issues;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAe,MAAwC;AACrE,SAAQ,MAAR;EACE,KAAK,gBAAgB,aACnB,QAAO;EACT,KAAK,gBAAgB,aACnB,QAAO;EACT,KAAK,gBAAgB;EACrB,KAAK,gBAAgB,aACnB,QAAO;EACT,KAAK,gBAAgB,cACnB,QAAO;EACT,KAAK,gBAAgB,uBACnB,QAAO;EACT,KAAK,gBAAgB,eACnB,QAAO;EACT,QAGE,QAAO;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Typed error hierarchy surfaced by `@graphorin/client`. Every error\n * class extends the JavaScript built-in `Error` and exposes a stable\n * `kind` discriminator so consumers can pattern-match without\n * relying on `instanceof` (which behaves badly across module-system\n * boundaries when the package is dual-loaded).\n *\n * @packageDocumentation\n */\n\nimport { RPC_ERROR_CODES } from '@graphorin/protocol';\n\n/**\n * IP-19: map a JSON-RPC error code from a server `RpcFailure` frame to the\n * client's discriminated {@link GraphorinClientErrorKind}, so a rate-limited\n * or scope-denied RPC is distinguishable from a genuine protocol violation.\n */\nexport function kindForRpcCode(code: number): GraphorinClientErrorKind {\n switch (code) {\n case RPC_ERROR_CODES.RATE_LIMITED:\n return 'rate-limited';\n case RPC_ERROR_CODES.SCOPE_DENIED:\n return 'scope-denied';\n case RPC_ERROR_CODES.AUTH_REQUIRED:\n case RPC_ERROR_CODES.AUTH_INVALID:\n return 'auth-failed';\n case RPC_ERROR_CODES.RUN_NOT_FOUND:\n return 'run-not-found';\n case RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND:\n return 'subscription-not-found';\n case RPC_ERROR_CODES.INTERNAL_ERROR:\n return 'server-error';\n default:\n // INVALID_REQUEST / INVALID_PARAMS / METHOD_NOT_FOUND / PROTOCOL_VIOLATION\n // and any unknown code stay a protocol violation.\n return 'protocol-violation';\n }\n}\n\n/**\n * Discriminator union of every error kind raised by the client.\n *\n * @stable\n */\nexport type GraphorinClientErrorKind =\n | 'client-not-connected'\n | 'transport-failed'\n | 'subprotocol-mismatch'\n | 'auth-failed'\n | 'protocol-violation'\n | 'subscription-not-found'\n // IP-19: discriminated RPC-failure kinds so callers can branch on the\n // server's error class instead of pattern-matching the message string.\n | 'rate-limited'\n | 'scope-denied'\n | 'run-not-found'\n | 'server-error'\n | 'aborted'\n /**\n * W-152: the per-subscription client buffer hit\n * `subscriptionQueueLimit` because the `for await` consumer fell\n * behind the event flow; the subscription is closed with this typed\n * error instead of growing the heap or silently dropping frames.\n */\n | 'flow-overflow'\n | 'invalid-server-frame';\n\n/**\n * Base class for every error raised by `@graphorin/client`. Carries a\n * stable {@link GraphorinClientErrorKind} discriminator and an\n * optional `cause` chain.\n *\n * @stable\n */\nexport class GraphorinClientError extends Error {\n readonly kind: GraphorinClientErrorKind;\n\n constructor(kind: GraphorinClientErrorKind, message: string, options: ErrorOptions = {}) {\n super(message, options);\n this.kind = kind;\n this.name = 'GraphorinClientError';\n }\n}\n\n/** @stable */\nexport class ClientNotConnectedError extends GraphorinClientError {\n constructor(message = 'GraphorinClient is not connected. Call connect() first.') {\n super('client-not-connected', message);\n this.name = 'ClientNotConnectedError';\n }\n}\n\n/** @stable */\nexport class TransportFailedError extends GraphorinClientError {\n readonly code: number | undefined;\n\n constructor(message: string, options: ErrorOptions & { readonly code?: number } = {}) {\n super('transport-failed', message, options);\n this.name = 'TransportFailedError';\n this.code = options.code;\n }\n}\n\n/** @stable */\nexport class SubprotocolMismatchError extends GraphorinClientError {\n readonly expected: string;\n readonly actual: string | null;\n\n constructor(expected: string, actual: string | null) {\n super(\n 'subprotocol-mismatch',\n `Server selected subprotocol '${actual ?? '<none>'}'; expected '${expected}'.`,\n );\n this.name = 'SubprotocolMismatchError';\n this.expected = expected;\n this.actual = actual;\n }\n}\n\n/** @stable */\nexport class AuthFailedError extends GraphorinClientError {\n constructor(message = 'Authentication failed. Re-mint a token or request a new ticket.') {\n super('auth-failed', message);\n this.name = 'AuthFailedError';\n }\n}\n\n/** @stable */\nexport class ProtocolViolationError extends GraphorinClientError {\n constructor(message: string, options: ErrorOptions = {}) {\n super('protocol-violation', message, options);\n this.name = 'ProtocolViolationError';\n }\n}\n\n/** @stable */\nexport class SubscriptionNotFoundError extends GraphorinClientError {\n readonly subscriptionId: string;\n\n constructor(subscriptionId: string) {\n super(\n 'subscription-not-found',\n `Subscription '${subscriptionId}' is not active on this client.`,\n );\n this.name = 'SubscriptionNotFoundError';\n this.subscriptionId = subscriptionId;\n }\n}\n\n/** @stable */\nexport class ClientAbortedError extends GraphorinClientError {\n constructor(message = 'Operation aborted before completion.') {\n super('aborted', message);\n this.name = 'ClientAbortedError';\n }\n}\n\n/** @stable */\nexport class InvalidServerFrameError extends GraphorinClientError {\n readonly issues: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>;\n\n constructor(\n message: string,\n issues: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>,\n ) {\n super('invalid-server-frame', message);\n this.name = 'InvalidServerFrameError';\n this.issues = issues;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAe,MAAwC;AACrE,SAAQ,MAAR;EACE,KAAK,gBAAgB,aACnB,QAAO;EACT,KAAK,gBAAgB,aACnB,QAAO;EACT,KAAK,gBAAgB;EACrB,KAAK,gBAAgB,aACnB,QAAO;EACT,KAAK,gBAAgB,cACnB,QAAO;EACT,KAAK,gBAAgB,uBACnB,QAAO;EACT,KAAK,gBAAgB,eACnB,QAAO;EACT,QAGE,QAAO;;;;;;;;;;AAuCb,IAAa,uBAAb,cAA0C,MAAM;CAC9C,AAAS;CAET,YAAY,MAAgC,SAAiB,UAAwB,EAAE,EAAE;AACvF,QAAM,SAAS,QAAQ;AACvB,OAAK,OAAO;AACZ,OAAK,OAAO;;;;AAKhB,IAAa,0BAAb,cAA6C,qBAAqB;CAChE,YAAY,UAAU,2DAA2D;AAC/E,QAAM,wBAAwB,QAAQ;AACtC,OAAK,OAAO;;;;AAKhB,IAAa,uBAAb,cAA0C,qBAAqB;CAC7D,AAAS;CAET,YAAY,SAAiB,UAAqD,EAAE,EAAE;AACpF,QAAM,oBAAoB,SAAS,QAAQ;AAC3C,OAAK,OAAO;AACZ,OAAK,OAAO,QAAQ;;;;AAKxB,IAAa,2BAAb,cAA8C,qBAAqB;CACjE,AAAS;CACT,AAAS;CAET,YAAY,UAAkB,QAAuB;AACnD,QACE,wBACA,gCAAgC,UAAU,SAAS,eAAe,SAAS,IAC5E;AACD,OAAK,OAAO;AACZ,OAAK,WAAW;AAChB,OAAK,SAAS;;;;AAKlB,IAAa,kBAAb,cAAqC,qBAAqB;CACxD,YAAY,UAAU,mEAAmE;AACvF,QAAM,eAAe,QAAQ;AAC7B,OAAK,OAAO;;;;AAKhB,IAAa,yBAAb,cAA4C,qBAAqB;CAC/D,YAAY,SAAiB,UAAwB,EAAE,EAAE;AACvD,QAAM,sBAAsB,SAAS,QAAQ;AAC7C,OAAK,OAAO;;;;AAKhB,IAAa,4BAAb,cAA+C,qBAAqB;CAClE,AAAS;CAET,YAAY,gBAAwB;AAClC,QACE,0BACA,iBAAiB,eAAe,iCACjC;AACD,OAAK,OAAO;AACZ,OAAK,iBAAiB;;;;AAK1B,IAAa,qBAAb,cAAwC,qBAAqB;CAC3D,YAAY,UAAU,wCAAwC;AAC5D,QAAM,WAAW,QAAQ;AACzB,OAAK,OAAO;;;;AAKhB,IAAa,0BAAb,cAA6C,qBAAqB;CAChE,AAAS;CAET,YACE,SACA,QACA;AACA,QAAM,wBAAwB,QAAQ;AACtC,OAAK,OAAO;AACZ,OAAK,SAAS"}
|
|
@@ -58,7 +58,27 @@ interface GraphorinClientOptions {
|
|
|
58
58
|
*/
|
|
59
59
|
readonly baseUrl: string;
|
|
60
60
|
readonly auth: TransportAuth;
|
|
61
|
+
/**
|
|
62
|
+
* Transport selection. Default `'auto'`: WebSocket first, SSE
|
|
63
|
+
* fallback. W-108 caveat for `'auto'`: SSE carries only the bound
|
|
64
|
+
* session subject, so when a RECONNECT falls back from WS to SSE,
|
|
65
|
+
* live WS subscriptions (agent / workflow subjects) cannot be
|
|
66
|
+
* resumed - they are closed with a `TransportFailedError` (their
|
|
67
|
+
* `for await` consumers reject immediately instead of hanging).
|
|
68
|
+
* Force `'ws'` when your application depends on those subscriptions
|
|
69
|
+
* surviving reconnects.
|
|
70
|
+
*/
|
|
61
71
|
readonly transport?: TransportPreference;
|
|
72
|
+
/**
|
|
73
|
+
* W-152: per-subscription buffer cap. When the `for await` consumer
|
|
74
|
+
* falls behind and the buffered queue reaches this many frames, the
|
|
75
|
+
* subscription closes with a typed `flow-overflow` error (mirroring
|
|
76
|
+
* the server's queue-overflow close) instead of growing the heap
|
|
77
|
+
* without bound or silently dropping events. Default 10000 (10x the
|
|
78
|
+
* server's default per-connection limit); `0` disables the cap and
|
|
79
|
+
* restores the old unbounded behavior.
|
|
80
|
+
*/
|
|
81
|
+
readonly subscriptionQueueLimit?: number;
|
|
62
82
|
/** Override the WS path (default `'/v1/ws'`). */
|
|
63
83
|
readonly wsPath?: string;
|
|
64
84
|
/**
|
|
@@ -98,6 +118,8 @@ interface SubscriptionMetadata {
|
|
|
98
118
|
readonly snapshotEventId: string | undefined;
|
|
99
119
|
readonly lastEventId: string | undefined;
|
|
100
120
|
readonly closed: boolean;
|
|
121
|
+
/** W-152: current buffered (undelivered) event-frame count. */
|
|
122
|
+
readonly queuedEvents: number;
|
|
101
123
|
}
|
|
102
124
|
/**
|
|
103
125
|
* Public surface returned by {@link GraphorinClient.subscribe}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphorin-client.d.ts","names":[],"sources":["../src/graphorin-client.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;KAuEY,kBAAA;;;;;;;;;;;;;;;;;;;;;KAoBA,mBAAA,GAAsB;;;;;;UAOjB,sBAAA;;;;;;;;;;;;;;iBAcA
|
|
1
|
+
{"version":3,"file":"graphorin-client.d.ts","names":[],"sources":["../src/graphorin-client.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;KAuEY,kBAAA;;;;;;;;;;;;;;;;;;;;;KAoBA,mBAAA,GAAsB;;;;;;UAOjB,sBAAA;;;;;;;;;;;;;;iBAcA;;;;;;;;;;;uBAWM;;;;;;;;;;;;;;;;;;;;uBAoBA;;8BAEO;;gCAEE;;0BAEN;;;;;;;;;;;;;;;;;UAkBT,oBAAA;;;mBAGE;;;;;;;;;;;;UAaF,YAAA;;;YAGL,cAAc;;;;iBAIT;cACH;;;;;cAmBD,eAAA;;uBAYU;;;;;;;;;aAeJ;;UAkCH;;;;;;;oBAWJ;;MAEP,QAAQ;;;;;;;;;;MAuER;;;;;;;;;;;;MA+BA;;;;;;;;;;gBA4DiB;;uBAwBC"}
|
package/dist/graphorin-client.js
CHANGED
|
@@ -339,7 +339,15 @@ var GraphorinClient = class {
|
|
|
339
339
|
await this.connect();
|
|
340
340
|
const transport = this.#transport;
|
|
341
341
|
if (transport === void 0) continue;
|
|
342
|
-
if (transport.kind === "sse")
|
|
342
|
+
if (transport.kind === "sse") {
|
|
343
|
+
const fellBack = new TransportFailedError("Reconnect fell back to SSE; WebSocket subscriptions cannot be resumed over SSE - resubscribe on the bound session subject or force transport: 'ws'.");
|
|
344
|
+
for (const [id, sub] of [...this.#subscriptions]) {
|
|
345
|
+
if (id === "__sse__") continue;
|
|
346
|
+
this.#subscriptions.delete(id);
|
|
347
|
+
sub.__close("aborted", fellBack);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
343
351
|
for (const [oldId, sub] of [...this.#subscriptions]) {
|
|
344
352
|
this.#subscriptions.delete(oldId);
|
|
345
353
|
try {
|
|
@@ -371,7 +379,9 @@ var GraphorinClient = class {
|
|
|
371
379
|
let closed = false;
|
|
372
380
|
let closeError;
|
|
373
381
|
let lastEventId = args.snapshotEventId;
|
|
382
|
+
const queueLimit = this.#options.subscriptionQueueLimit ?? 1e4;
|
|
374
383
|
const push = (frame) => {
|
|
384
|
+
if (closed) return;
|
|
375
385
|
lastEventId = frame.eventId;
|
|
376
386
|
const waiter = waiters.shift();
|
|
377
387
|
if (waiter !== void 0) {
|
|
@@ -381,6 +391,10 @@ var GraphorinClient = class {
|
|
|
381
391
|
});
|
|
382
392
|
return;
|
|
383
393
|
}
|
|
394
|
+
if (queueLimit > 0 && queue.length >= queueLimit) {
|
|
395
|
+
close("aborted", new GraphorinClientError("flow-overflow", `Subscription '${args.subscriptionId}' buffered ${queue.length} events with no consumer progress (subscriptionQueueLimit=${queueLimit}). Consume the events() iterator faster, raise the limit, or set 0 to disable the cap.`));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
384
398
|
queue.push(frame);
|
|
385
399
|
};
|
|
386
400
|
const close = (reason, err) => {
|
|
@@ -448,7 +462,8 @@ var GraphorinClient = class {
|
|
|
448
462
|
target: args.target,
|
|
449
463
|
snapshotEventId: args.snapshotEventId,
|
|
450
464
|
lastEventId,
|
|
451
|
-
closed
|
|
465
|
+
closed,
|
|
466
|
+
queuedEvents: queue.length
|
|
452
467
|
}),
|
|
453
468
|
__push: push,
|
|
454
469
|
__pushLifecycle: (frame) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphorin-client.js","names":["#options","#pending","#subscriptions","#closed","#transport","#connectingPromise","#openWs","#initializeRpc","#openSse","#sendRpc","pkg.version","subject","#sseSubscription","#createSubscription","params: { subject: string; sinceEventId?: string }","params: {\n runId: string;\n drain?: boolean;\n reason?: string;\n onPendingApprovals?: 'deny' | 'preserve';\n }","headers: Record<string, string>","frame: ClientMessage","#abortController","#wsUrl","#listeners","#sseUrl","#handleFrame","#handleTransportError","#handleTransportClose","#sseFallback","#reconnect","queue: ServerEventFrame[]","lifecycle: ServerLifecycleFrame[]","replayMarkers: ServerReplayMarkerFrame[]","waiters: Array<{\n resolve: (value: IteratorResult<ServerEventFrame>) => void;\n reject: (err: Error) => void;\n }>","closeError: Error | undefined","lastEventId: string | undefined","#nextId","timer: ReturnType<typeof setTimeout> | undefined","pending: PendingRpc","#idCounter"],"sources":["../src/graphorin-client.ts"],"sourcesContent":["import pkg from '../package.json' with { type: 'json' };\n/**\n * `GraphorinClient` - ergonomic façade over the\n * {@link Transport} contract. Handles:\n *\n * - WS handshake (`openWebSocketTransport`) with optional ticket flow.\n * - Optional SSE fallback (`openSseTransport`) for environments\n * that block WebSocket upgrades.\n * - JSON-RPC request / response correlation for `subscribe` /\n * `unsubscribe` / `cancel` / `resume` / `ping` calls.\n * - Async-iterable subscriptions (`for await (const event of\n * sub.events())`).\n * - Exponential-backoff reconnect with `lastEventId` resume against\n * the server replay buffer.\n *\n * The class is intentionally small (≈ 400 LOC) so the production\n * cross-cuts (telemetry, sticky reconnect on transient errors,\n * load-shedding) live in higher-level wrappers consumers build on\n * top of `GraphorinClient` instead of leaking into the protocol\n * adapter itself.\n *\n * @packageDocumentation\n */\n\nimport {\n type ClientMessage,\n type ClientMessageId,\n isErrorFrame,\n isEventFrame,\n isLifecycleFrame,\n isPongFrame,\n isReplayMarkerFrame,\n isRpcFailure,\n isRpcSuccess,\n isSubscribedFrame,\n isUnsubscribedFrame,\n type ServerEventFrame,\n type ServerLifecycleFrame,\n type ServerMessage,\n type ServerReplayMarkerFrame,\n} from '@graphorin/protocol';\n\nimport {\n ClientAbortedError,\n ClientNotConnectedError,\n GraphorinClientError,\n kindForRpcCode,\n ProtocolViolationError,\n TransportFailedError,\n} from './errors.js';\nimport { type BackoffPolicy, computeBackoffMs, sleep } from './reconnect.js';\nimport {\n openSseTransport,\n openWebSocketTransport,\n type Transport,\n type TransportAuth,\n type TransportCloseReason,\n type TransportKind,\n} from './transport/index.js';\n\n/**\n * Discriminator for the subscription target. Mirrors the strict\n * subject grammar enforced by the server:\n * - `'session'`/`<id>` ⇒ `'session:<id>/events'`\n * - `'agent'`/`<id>` + `runId` ⇒ `'agent:<id>/runs/<runId>/events'`\n * - `'run'`/`<runId>` ⇒ `'session:<sessionId>/runs/<runId>/events'`\n * (when `sessionId` is provided)\n * - `'workflow'`/`<id>` ⇒ `'workflow:<id>/events'`\n *\n * @stable\n */\nexport type SubscriptionTarget =\n | { readonly target: 'session'; readonly id: string }\n | {\n readonly target: 'agent';\n readonly id: string;\n readonly runId: string;\n }\n | {\n readonly target: 'run';\n readonly runId: string;\n readonly sessionId?: string;\n }\n | { readonly target: 'workflow'; readonly id: string };\n\n/**\n * Transport selector. `'auto'` (default) attempts a WebSocket\n * handshake first and falls back to SSE on failure.\n *\n * @stable\n */\nexport type TransportPreference = TransportKind | 'auto';\n\n/**\n * Public configuration accepted by {@link GraphorinClient}.\n *\n * @stable\n */\nexport interface GraphorinClientOptions {\n /**\n * Session bound to the SSE fallback (IP-3): substituted into the\n * `:sessionId` slot of `sseSessionPath`. Required to connect over\n * SSE - the old client sent the literal template and could never\n * receive an event.\n */\n readonly sessionId?: string;\n /**\n * Server base URL. Examples: `'wss://graphorin.example.com'` or\n * `'http://localhost:8080'`. The path `/v1/ws` is appended for\n * the WebSocket transport.\n */\n readonly baseUrl: string;\n readonly auth: TransportAuth;\n readonly transport?: TransportPreference;\n /** Override the WS path (default `'/v1/ws'`). */\n readonly wsPath?: string;\n /**\n * SSE path template. The placeholder `:sessionId` is replaced at\n * subscribe time. Default: `'/v1/sessions/:sessionId/events'`.\n * Required when the transport is `'sse'` or when the WS handshake\n * fails on `'auto'`.\n */\n readonly sseSessionPath?: string;\n readonly reconnect?: BackoffPolicy;\n /** Inject a `WebSocket` constructor (Node SDKs / tests). */\n readonly WebSocket?: typeof WebSocket;\n /** Inject an `EventSource` constructor (Node SDKs / tests). */\n readonly EventSource?: typeof EventSource;\n /** Inject a `fetch` implementation (defaults to `globalThis.fetch`). */\n readonly fetch?: typeof fetch;\n /** Optional client identifier surfaced on diagnostics. */\n readonly clientId?: string;\n /**\n * IP-19: per-RPC reply timeout in milliseconds. When set (and > 0), an RPC\n * that receives no matching reply within this window rejects with a\n * {@link TransportFailedError} instead of hanging forever on a\n * non-responsive server. Default: unset - no timeout, so a legitimately\n * slow server reply is never aborted (opt-in).\n */\n readonly rpcTimeoutMs?: number;\n}\n\n/**\n * Snapshot returned by {@link Subscription.metadata}.\n *\n * @stable\n */\nexport interface SubscriptionMetadata {\n readonly id: string;\n readonly subject: string;\n readonly target: SubscriptionTarget;\n readonly snapshotEventId: string | undefined;\n readonly lastEventId: string | undefined;\n readonly closed: boolean;\n}\n\n/**\n * Public surface returned by {@link GraphorinClient.subscribe}.\n *\n * @stable\n */\nexport interface Subscription {\n readonly subscriptionId: string;\n readonly subject: string;\n events(): AsyncIterable<ServerEventFrame>;\n /**\n * Close the subscription on the server. Idempotent.\n */\n unsubscribe(): Promise<void>;\n metadata(): SubscriptionMetadata;\n}\n\ninterface SubscriptionInternal extends Subscription {\n __push(frame: ServerEventFrame): void;\n __pushLifecycle(frame: ServerLifecycleFrame): void;\n __pushReplayMarker(frame: ServerReplayMarkerFrame): void;\n __close(reason: 'unsubscribed' | 'transport-closed' | 'aborted', error?: Error): void;\n __subject(): string;\n __target(): SubscriptionTarget;\n __snapshotEventId(): string | undefined;\n __lastEventId(): string | undefined;\n /** IP-7: re-point this subscription at a new server subscriptionId. */\n __rebind(newSubscriptionId: string): void;\n}\n\n/**\n * @stable\n */\nexport class GraphorinClient {\n readonly #options: GraphorinClientOptions;\n readonly #pending: Map<ClientMessageId, PendingRpc> = new Map();\n readonly #subscriptions: Map<string, SubscriptionInternal> = new Map();\n /** IP-3: the synthetic single subscription an SSE connection carries. */\n #sseSubscription: SubscriptionInternal | undefined;\n #transport: Transport | undefined;\n #idCounter = 0;\n #closed = false;\n #connectingPromise: Promise<void> | undefined;\n #abortController: AbortController = new AbortController();\n\n constructor(options: GraphorinClientOptions) {\n if (typeof options.baseUrl !== 'string' || options.baseUrl.length === 0) {\n throw new TypeError('GraphorinClient: baseUrl must be a non-empty string.');\n }\n this.#options = options;\n }\n\n /**\n * Open the underlying transport. Resolves once the server has\n * accepted the handshake (`'open'`); rejects with a typed\n * {@link GraphorinClientError} otherwise.\n *\n * Calling `connect()` while already connected is a no-op; calling\n * it during another `connect()` returns the same promise.\n */\n async connect(): Promise<void> {\n if (this.#closed) {\n throw new ClientAbortedError('GraphorinClient was disconnected; create a new instance.');\n }\n if (this.#transport !== undefined) return;\n if (this.#connectingPromise !== undefined) return this.#connectingPromise;\n this.#connectingPromise = (async () => {\n const preference = this.#options.transport ?? 'auto';\n try {\n if (preference === 'ws' || preference === 'auto') {\n try {\n this.#transport = await this.#openWs();\n await this.#initializeRpc();\n return;\n } catch (err) {\n if (preference === 'ws') throw err;\n }\n }\n this.#transport = await this.#openSse();\n } finally {\n this.#connectingPromise = undefined;\n }\n })();\n return this.#connectingPromise;\n }\n\n async #initializeRpc(): Promise<void> {\n if (this.#transport?.kind !== 'ws') return;\n await this.#sendRpc('initialize', {\n clientInfo: { name: 'graphorin-client', version: pkg.version },\n });\n }\n\n /** Send a `ping` RPC and resolve when the server replies with `pong`. */\n async ping(): Promise<void> {\n await this.#sendRpc('ping');\n }\n\n /**\n * Subscribe to a server-side event stream. Resolves with a\n * {@link Subscription} once the server confirms with the matching\n * `subscribed` frame; rejects when the server returns an\n * `error` instead.\n */\n async subscribe(\n target: SubscriptionTarget,\n opts?: { readonly sinceEventId?: string },\n ): Promise<Subscription> {\n if (this.#transport === undefined) throw new ClientNotConnectedError();\n if (this.#transport.kind === 'sse') {\n // IP-3: the SSE connection IS one implicit subscription to the\n // bound session's events. Frames arrive with a server-generated\n // subscriptionId the client cannot know upfront, so routing\n // falls through to this synthetic subscription (see\n // #handleFrame). Other subjects still require the WS transport.\n const subject = subjectFor(target);\n const boundSubject = `session:${this.#options.sessionId ?? ''}/events`;\n if (subject !== boundSubject) {\n throw new TransportFailedError(\n `subscribe('${subject}') requires the WebSocket transport - the SSE fallback carries only the bound session stream ('${boundSubject}').`,\n );\n }\n // periphery-03: a CLOSED bound subscription (a prior terminal\n // reconnect failure) must not be handed back - recreate it so a\n // recovered client delivers events again.\n if (this.#sseSubscription === undefined || this.#sseSubscription.metadata().closed) {\n this.#sseSubscription = this.#createSubscription({\n subscriptionId: '__sse__',\n subject,\n target,\n snapshotEventId: undefined,\n });\n this.#subscriptions.set('__sse__', this.#sseSubscription);\n }\n return this.#sseSubscription;\n }\n const subject = subjectFor(target);\n // IP-7: a resubscribe passes the SUBSCRIPTION's own cursor - the\n // fresh transport's lastEventId is always undefined, so the old\n // code never consulted the server replay buffer.\n const lastEventId = opts?.sinceEventId ?? this.#transport.lastEventId;\n const params: { subject: string; sinceEventId?: string } = {\n subject,\n };\n if (lastEventId !== undefined) params.sinceEventId = lastEventId;\n const reply = await this.#sendRpc('subscription.subscribe', params);\n const result = reply as { subscriptionId?: unknown; snapshotEventId?: unknown };\n const subscriptionId =\n typeof result.subscriptionId === 'string' && result.subscriptionId.length > 0\n ? result.subscriptionId\n : undefined;\n if (subscriptionId === undefined) {\n throw new ProtocolViolationError('Server subscribe reply missing subscriptionId.');\n }\n const snapshotEventId =\n typeof result.snapshotEventId === 'string' ? result.snapshotEventId : undefined;\n const sub = this.#createSubscription({\n subscriptionId,\n subject,\n target,\n snapshotEventId,\n });\n this.#subscriptions.set(subscriptionId, sub);\n return sub;\n }\n\n /**\n * Cancel a server-side run. Sends the `run.cancel` RPC and\n * resolves with the server's `result` payload (typically\n * `{ cancelled: true, partialStateAvailable: true }`).\n */\n async cancel(\n runId: string,\n opts: {\n readonly drain?: boolean;\n readonly reason?: string;\n readonly onPendingApprovals?: 'deny' | 'preserve';\n } = {},\n ): Promise<unknown> {\n if (typeof runId !== 'string' || runId.length === 0) {\n throw new TypeError('cancel: runId must be a non-empty string.');\n }\n const params: {\n runId: string;\n drain?: boolean;\n reason?: string;\n onPendingApprovals?: 'deny' | 'preserve';\n } = { runId };\n if (opts.drain !== undefined) params.drain = opts.drain;\n if (opts.reason !== undefined) params.reason = opts.reason;\n if (opts.onPendingApprovals !== undefined) {\n params.onPendingApprovals = opts.onPendingApprovals;\n }\n return this.#sendRpc('run.cancel', params);\n }\n\n /**\n * Resume a paused (HITL) run. The WebSocket protocol intentionally\n * does NOT carry a `resume` control message - resumes are durable\n * + idempotent + body-carrying, which maps onto the REST endpoint\n * `POST /v1/runs/:runId/resume`. NOTE (IP-14): the server endpoint\n * currently answers **501** - server-side durable resume is not\n * implemented yet. Library-mode callers resume directly:\n * `agent.run(result.state, { directive })`.\n */\n async resume(\n runId: string,\n directive?: unknown,\n opts: { readonly idempotencyKey?: string } = {},\n ): Promise<unknown> {\n if (typeof runId !== 'string' || runId.length === 0) {\n throw new TypeError('resume: runId must be a non-empty string.');\n }\n const fetchImpl = this.#options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new TransportFailedError(\n 'No fetch implementation available; pass `fetch` via the client options.',\n );\n }\n const url = joinUrl(this.#options.baseUrl, `/v1/runs/${encodeURIComponent(runId)}/resume`, {\n ws: false,\n });\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (this.#options.auth.kind === 'bearer') {\n headers.Authorization = `Bearer ${this.#options.auth.token}`;\n }\n if (opts.idempotencyKey !== undefined) headers['Idempotency-Key'] = opts.idempotencyKey;\n const res = await fetchImpl(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(directive === undefined ? {} : { directive }),\n });\n if (!res.ok) {\n throw new TransportFailedError(\n `resume(${runId}): server responded ${res.status} ${res.statusText}.`,\n { code: res.status },\n );\n }\n try {\n return await res.json();\n } catch {\n return undefined;\n }\n }\n\n /**\n * Send an MCP-compatible cancellation notification. Does not wait\n * for a server reply (notifications have no `id`).\n */\n cancelNotify(requestId: string): void {\n if (this.#transport === undefined) throw new ClientNotConnectedError();\n if (typeof requestId !== 'string' || requestId.length === 0) {\n throw new TypeError('cancelNotify: requestId must be a non-empty string.');\n }\n const frame: ClientMessage = {\n v: '1',\n jsonrpc: '2.0',\n method: 'notifications/cancelled',\n params: { requestId },\n };\n this.#transport.send(frame);\n }\n\n /**\n * Disconnect the underlying transport and abort every pending RPC\n * + subscription. Idempotent.\n */\n async disconnect(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n const transport = this.#transport;\n this.#transport = undefined;\n this.#abortController.abort(new ClientAbortedError());\n for (const pending of this.#pending.values()) {\n pending.reject(new ClientAbortedError('Client disconnected before reply.'));\n }\n this.#pending.clear();\n for (const sub of this.#subscriptions.values()) {\n sub.__close('aborted', new ClientAbortedError('Client disconnected.'));\n }\n this.#subscriptions.clear();\n if (transport !== undefined) {\n try {\n transport.close(1000, 'client-disconnect');\n } catch {\n // Best-effort close: never throw from disconnect().\n }\n }\n }\n\n /** Return the active transport kind (or `undefined` if not connected). */\n get transportKind(): TransportKind | undefined {\n return this.#transport?.kind;\n }\n\n // -----------------------------------------------------------------\n // Internal helpers\n // -----------------------------------------------------------------\n\n async #openWs(): Promise<Transport> {\n const url = this.#wsUrl();\n const transport = await openWebSocketTransport(\n {\n url,\n auth: this.#options.auth,\n ...(this.#options.WebSocket !== undefined ? { WebSocket: this.#options.WebSocket } : {}),\n ...(this.#options.clientId !== undefined ? { clientId: this.#options.clientId } : {}),\n },\n this.#listeners('ws'),\n );\n return transport;\n }\n\n async #openSse(): Promise<Transport> {\n if (this.#options.auth.kind !== 'bearer') {\n throw new TransportFailedError(\n \"SSE fallback requires the 'bearer' auth strategy. The WebSocket ticket flow does not extend to EventSource.\",\n );\n }\n const url = this.#sseUrl();\n // periphery-03: resume from where the bound session subscription\n // left off - without the cursor every reconnect replays the whole\n // server buffer into the consumer.\n const resumeCursor = this.#sseSubscription?.__lastEventId();\n const transport = await openSseTransport(\n {\n url,\n auth: this.#options.auth,\n // IP-3: the fetch-streaming transport uses the client's fetch\n // seam (the EventSource seam is gone with the rewrite).\n ...(this.#options.fetch !== undefined ? { fetch: this.#options.fetch } : {}),\n ...(this.#options.clientId !== undefined ? { clientId: this.#options.clientId } : {}),\n ...(resumeCursor !== undefined ? { lastEventId: resumeCursor } : {}),\n },\n this.#listeners('sse'),\n );\n return transport;\n }\n\n #wsUrl(): string {\n const path = this.#options.wsPath ?? '/v1/ws';\n return joinUrl(this.#options.baseUrl, path, { ws: true });\n }\n\n #sseUrl(): string {\n const template = this.#options.sseSessionPath ?? '/v1/sessions/:sessionId/events';\n // IP-3: bind the session into the path - the literal template was\n // previously sent as-is, so the SSE fallback could never connect\n // to a real session stream.\n if (template.includes(':sessionId')) {\n const sessionId = this.#options.sessionId;\n if (sessionId === undefined || sessionId.length === 0) {\n throw new TransportFailedError(\n \"The SSE fallback needs a session binding - pass `sessionId` in the client options (it fills ':sessionId' in sseSessionPath).\",\n );\n }\n return joinUrl(\n this.#options.baseUrl,\n template.replace(':sessionId', encodeURIComponent(sessionId)),\n { ws: false },\n );\n }\n return joinUrl(this.#options.baseUrl, template, { ws: false });\n }\n\n #listeners(kind: TransportKind): {\n onOpen(): void;\n onFrame(frame: ServerMessage): void;\n onError(err: Error): void;\n onClose(reason: TransportCloseReason): void;\n } {\n return {\n onOpen: () => {},\n onFrame: (frame) => this.#handleFrame(frame),\n onError: (err) => this.#handleTransportError(err, kind),\n onClose: (reason) => this.#handleTransportClose(reason, kind),\n };\n }\n\n #handleFrame(frame: ServerMessage): void {\n if (isRpcSuccess(frame)) {\n const pending = this.#pending.get(frame.id);\n if (pending !== undefined) {\n this.#pending.delete(frame.id);\n pending.resolve(frame.result);\n }\n return;\n }\n if (isRpcFailure(frame)) {\n const pending = this.#pending.get(frame.id);\n if (pending !== undefined) {\n this.#pending.delete(frame.id);\n // IP-19: surface the server's error class (rate-limited, scope-denied,\n // auth-failed, …) instead of collapsing every RPC failure to\n // 'protocol-violation'.\n pending.reject(\n new GraphorinClientError(\n kindForRpcCode(frame.error.code),\n `Server returned an error for RPC '${frame.id}': ${frame.error.message} (code=${frame.error.code}).`,\n ),\n );\n }\n return;\n }\n if (isSubscribedFrame(frame)) {\n // Subscribed frames are acknowledged by the matching RPC reply.\n return;\n }\n if (isUnsubscribedFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId);\n if (sub !== undefined) {\n sub.__close('unsubscribed');\n this.#subscriptions.delete(frame.subscriptionId);\n }\n return;\n }\n if (isPongFrame(frame)) return;\n if (isLifecycleFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId) ?? this.#sseFallback();\n if (sub !== undefined) sub.__pushLifecycle(frame);\n return;\n }\n if (isReplayMarkerFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId) ?? this.#sseFallback();\n if (sub !== undefined) sub.__pushReplayMarker(frame);\n return;\n }\n if (isEventFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId) ?? this.#sseFallback();\n if (sub !== undefined) sub.__push(frame);\n return;\n }\n if (isErrorFrame(frame)) {\n const target =\n frame.subscriptionId !== undefined\n ? this.#subscriptions.get(frame.subscriptionId)\n : undefined;\n const error = new GraphorinClientError(\n 'protocol-violation',\n `Server error frame: ${frame.message} (code=${frame.code}).`,\n );\n if (target !== undefined) {\n target.__close('aborted', error);\n if (frame.subscriptionId !== undefined) this.#subscriptions.delete(frame.subscriptionId);\n }\n }\n }\n\n /**\n * IP-3: on the SSE transport every frame belongs to the single\n * implicit session subscription regardless of the server-generated\n * subscriptionId it carries.\n */\n #sseFallback(): SubscriptionInternal | undefined {\n return this.#transport?.kind === 'sse' ? this.#sseSubscription : undefined;\n }\n\n #handleTransportError(err: Error, _kind: TransportKind): void {\n for (const pending of this.#pending.values()) {\n pending.reject(err);\n }\n this.#pending.clear();\n }\n\n async #handleTransportClose(reason: TransportCloseReason, _kind: TransportKind): Promise<void> {\n if (this.#closed) return;\n this.#transport = undefined;\n const closeError = new TransportFailedError(\n `Transport closed: ${reason.reason || reason.graphorinReason || 'unknown'} (code=${reason.code}).`,\n { code: reason.code },\n );\n // Best-effort drain of pending RPCs so callers see the failure\n // immediately rather than hanging until the reconnect window\n // closes.\n for (const pending of this.#pending.values()) {\n pending.reject(closeError);\n }\n this.#pending.clear();\n if (\n reason.graphorinReason === 'auth.required' ||\n reason.graphorinReason === 'auth.invalid' ||\n reason.graphorinReason === 'auth.revoked' ||\n reason.graphorinReason === 'protocol.violation'\n ) {\n // Fatal: stop trying to reconnect and surface the failure.\n for (const sub of this.#subscriptions.values()) {\n sub.__close('aborted', closeError);\n }\n this.#subscriptions.clear();\n return;\n }\n await this.#reconnect();\n }\n\n async #reconnect(): Promise<void> {\n if (this.#closed) return;\n let attempt = 0;\n while (!this.#closed) {\n attempt += 1;\n const delay = computeBackoffMs(attempt, this.#options.reconnect);\n if (delay === null) {\n const exhausted = new TransportFailedError(\n `Reconnect attempts exhausted after ${attempt - 1} retries.`,\n );\n for (const sub of this.#subscriptions.values()) {\n sub.__close('aborted', exhausted);\n }\n this.#subscriptions.clear();\n return;\n }\n try {\n await sleep(delay, this.#abortController.signal);\n await this.connect();\n // Best-effort resubscribe with the recorded lastEventId so\n // the server replays missed events from the buffer.\n const transport = this.#transport;\n if (transport === undefined) continue;\n // periphery-03: on the SSE fallback the connection IS the\n // subscription - the reconnect already carried the resume\n // cursor as `Last-Event-ID`, and frames keep flowing into the\n // same bound subscription object. The RPC resubscribe below\n // would call `transport.send(...)`, which the read-only SSE\n // transport rejects; pre-fix that error CLOSED the\n // subscription and the client silently stopped delivering\n // events for the life of the object.\n if (transport.kind === 'sse') return;\n for (const [oldId, sub] of [...this.#subscriptions]) {\n this.#subscriptions.delete(oldId);\n try {\n // IP-7: re-establish the server-side subscription with the\n // SUBSCRIPTION's own replay cursor, then re-point the SAME\n // object at the new server id - the consumer's in-flight\n // `for await` survives and the replayed events arrive in\n // the iterator it is already reading. The old code created\n // a NEW subscription and closed this one: the consumer's\n // loop ended `{done: true}` while events piled up unread\n // in an orphan.\n const cursor = sub.__lastEventId();\n const reply = await this.#sendRpc('subscription.subscribe', {\n subject: sub.__subject(),\n ...(cursor !== undefined ? { sinceEventId: cursor } : {}),\n });\n const result = reply as { subscriptionId?: unknown };\n const newId =\n typeof result.subscriptionId === 'string' && result.subscriptionId.length > 0\n ? result.subscriptionId\n : undefined;\n if (newId === undefined) {\n throw new ProtocolViolationError('Server resubscribe reply missing subscriptionId.');\n }\n sub.__rebind(newId);\n this.#subscriptions.set(newId, sub);\n } catch (err) {\n sub.__close('aborted', err instanceof Error ? err : new Error(String(err)));\n }\n }\n return;\n } catch (err) {\n if (this.#closed) return;\n // Loop and retry.\n if (err instanceof ClientAbortedError) return;\n }\n }\n }\n\n #createSubscription(args: {\n subscriptionId: string;\n readonly subject: string;\n readonly target: SubscriptionTarget;\n readonly snapshotEventId: string | undefined;\n }): SubscriptionInternal {\n const queue: ServerEventFrame[] = [];\n const lifecycle: ServerLifecycleFrame[] = [];\n const replayMarkers: ServerReplayMarkerFrame[] = [];\n const waiters: Array<{\n resolve: (value: IteratorResult<ServerEventFrame>) => void;\n reject: (err: Error) => void;\n }> = [];\n let closed = false;\n let closeError: Error | undefined;\n let lastEventId: string | undefined = args.snapshotEventId;\n\n const push = (frame: ServerEventFrame): void => {\n lastEventId = frame.eventId;\n const waiter = waiters.shift();\n if (waiter !== undefined) {\n waiter.resolve({ value: frame, done: false });\n return;\n }\n queue.push(frame);\n };\n\n const close = (reason: 'unsubscribed' | 'transport-closed' | 'aborted', err?: Error): void => {\n if (closed) return;\n closed = true;\n if (err !== undefined) closeError = err;\n else if (reason === 'aborted') closeError = new ClientAbortedError();\n while (waiters.length > 0) {\n const waiter = waiters.shift();\n if (waiter === undefined) continue;\n // IP-19(c): a waiter blocked in next() must observe the SAME outcome a\n // fresh next() would after close - reject with closeError when the\n // stream tore down abnormally, rather than silently resolving done:true\n // and swallowing the transport/abort failure.\n if (closeError !== undefined) {\n waiter.reject(closeError);\n } else {\n waiter.resolve({ value: undefined as unknown as ServerEventFrame, done: true });\n }\n }\n };\n\n const sub: SubscriptionInternal = {\n get subscriptionId(): string {\n return args.subscriptionId;\n },\n subject: args.subject,\n events: () => ({\n [Symbol.asyncIterator]: () => ({\n next: (): Promise<IteratorResult<ServerEventFrame>> => {\n const buffered = queue.shift();\n if (buffered !== undefined) {\n return Promise.resolve({ value: buffered, done: false });\n }\n if (closed) {\n if (closeError !== undefined) return Promise.reject(closeError);\n return Promise.resolve({\n value: undefined as unknown as ServerEventFrame,\n done: true,\n });\n }\n return new Promise<IteratorResult<ServerEventFrame>>((resolve, reject) => {\n waiters.push({ resolve, reject });\n });\n },\n return: (): Promise<IteratorResult<ServerEventFrame>> => {\n close('unsubscribed');\n return Promise.resolve({ value: undefined as unknown as ServerEventFrame, done: true });\n },\n }),\n }),\n unsubscribe: async () => {\n if (closed) return;\n try {\n await this.#sendRpc('subscription.unsubscribe', {\n subscriptionId: args.subscriptionId,\n });\n } catch (err) {\n if (!(err instanceof ClientAbortedError)) throw err;\n }\n close('unsubscribed');\n this.#subscriptions.delete(args.subscriptionId);\n },\n metadata: () => ({\n id: args.subscriptionId,\n subject: args.subject,\n target: args.target,\n snapshotEventId: args.snapshotEventId,\n lastEventId,\n closed,\n }),\n __push: push,\n __pushLifecycle: (frame) => {\n lifecycle.push(frame);\n if (\n frame.status === 'completed' ||\n frame.status === 'aborted' ||\n frame.status === 'failed'\n ) {\n close(frame.status === 'completed' ? 'unsubscribed' : 'aborted');\n this.#subscriptions.delete(args.subscriptionId);\n }\n },\n __pushReplayMarker: (frame) => {\n replayMarkers.push(frame);\n },\n __close: close,\n __subject: () => args.subject,\n __target: () => args.target,\n __snapshotEventId: () => args.snapshotEventId,\n __lastEventId: () => lastEventId,\n __rebind: (newSubscriptionId: string) => {\n args.subscriptionId = newSubscriptionId;\n },\n };\n return sub;\n }\n\n async #sendRpc(method: ClientMessage['method'], params?: object): Promise<unknown> {\n if (this.#transport === undefined) throw new ClientNotConnectedError();\n const id = this.#nextId();\n const frame = buildRpcFrame(method, id, params);\n const timeoutMs = this.#options.rpcTimeoutMs;\n return new Promise((resolve, reject) => {\n // IP-19: arm an optional reply timer. Wrapping resolve/reject so that\n // EVERY settle path (matching reply, disconnect, transport error) clears\n // the timer - otherwise a stray timer could reject an already-settled id.\n let timer: ReturnType<typeof setTimeout> | undefined;\n const clear = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n const pending: PendingRpc = {\n resolve: (value) => {\n clear();\n resolve(value);\n },\n reject: (reason) => {\n clear();\n reject(reason);\n },\n };\n this.#pending.set(id, pending);\n if (timeoutMs !== undefined && timeoutMs > 0) {\n timer = setTimeout(() => {\n // Fire only if this exact request is still outstanding.\n if (this.#pending.get(id) === pending) {\n this.#pending.delete(id);\n reject(\n new TransportFailedError(\n `RPC '${method}' (id=${id}) timed out after ${timeoutMs}ms with no server reply.`,\n ),\n );\n }\n }, timeoutMs);\n }\n try {\n // Asserting non-null because we just verified above.\n (this.#transport as Transport).send(frame);\n } catch (err) {\n clear();\n this.#pending.delete(id);\n reject(err);\n }\n });\n }\n\n #nextId(): string {\n this.#idCounter += 1;\n const prefix = this.#options.clientId ?? 'graphorin';\n return `${prefix}-${this.#idCounter.toString(36)}`;\n }\n}\n\ninterface PendingRpc {\n readonly resolve: (value: unknown) => void;\n readonly reject: (reason: Error) => void;\n}\n\nfunction subjectFor(target: SubscriptionTarget): string {\n switch (target.target) {\n case 'session':\n assertNonEmpty(target.id, 'session.id');\n return `session:${target.id}/events`;\n case 'agent':\n assertNonEmpty(target.id, 'agent.id');\n assertNonEmpty(target.runId, 'agent.runId');\n return `agent:${target.id}/runs/${target.runId}/events`;\n case 'run':\n assertNonEmpty(target.runId, 'run.runId');\n if (target.sessionId !== undefined) {\n assertNonEmpty(target.sessionId, 'run.sessionId');\n return `session:${target.sessionId}/runs/${target.runId}/events`;\n }\n // periphery-09: the server subject grammar has NO bare `run:`\n // form - the old `run:<id>/events` fallback always failed\n // server-side with an opaque unknown-subject error. Fail fast\n // with an actionable message instead.\n throw new TypeError(\n \"subscribe({ target: 'run' }) requires `sessionId` - the server subject grammar has no bare run form. \" +\n \"Pass { kind: 'run', runId, sessionId } or use the 'agent' target with the agent id.\",\n );\n case 'workflow':\n assertNonEmpty(target.id, 'workflow.id');\n return `workflow:${target.id}/events`;\n }\n}\n\nfunction assertNonEmpty(value: string, fieldName: string): void {\n if (typeof value !== 'string' || value.length === 0) {\n throw new TypeError(`${fieldName} must be a non-empty string.`);\n }\n}\n\nfunction joinUrl(baseUrl: string, path: string, opts: { readonly ws: boolean }): string {\n let normalized = baseUrl;\n if (opts.ws) {\n if (normalized.startsWith('http://')) normalized = normalized.replace(/^http:\\/\\//, 'ws://');\n else if (normalized.startsWith('https://'))\n normalized = normalized.replace(/^https:\\/\\//, 'wss://');\n } else {\n if (normalized.startsWith('ws://')) normalized = normalized.replace(/^ws:\\/\\//, 'http://');\n else if (normalized.startsWith('wss://'))\n normalized = normalized.replace(/^wss:\\/\\//, 'https://');\n }\n if (normalized.endsWith('/')) normalized = normalized.slice(0, -1);\n const tail = path.startsWith('/') ? path : `/${path}`;\n return `${normalized}${tail}`;\n}\n\nfunction buildRpcFrame(\n method: ClientMessage['method'],\n id: ClientMessageId,\n params?: object,\n): ClientMessage {\n switch (method) {\n case 'initialize':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'initialize',\n params: (params ?? {\n clientInfo: { name: 'graphorin-client', version: pkg.version },\n }) as never,\n };\n case 'subscription.subscribe':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'subscription.subscribe',\n params: (params ?? { subject: '' }) as never,\n };\n case 'subscription.unsubscribe':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'subscription.unsubscribe',\n params: (params ?? { subscriptionId: '' }) as never,\n };\n case 'run.cancel':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'run.cancel',\n params: (params ?? { runId: '' }) as never,\n };\n case 'ping':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'ping',\n ...(params !== undefined ? { params: params as never } : {}),\n };\n case 'notifications/cancelled':\n throw new TypeError(\n \"buildRpcFrame: 'notifications/cancelled' is a notification, not an RPC.\",\n );\n }\n // Exhaustiveness check; should be unreachable.\n throw new TypeError(`Unknown RPC method '${method}'.`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"graphorin-client.js","names":["#options","#pending","#subscriptions","#closed","#transport","#connectingPromise","#openWs","#initializeRpc","#openSse","#sendRpc","pkg.version","subject","#sseSubscription","#createSubscription","params: { subject: string; sinceEventId?: string }","params: {\n runId: string;\n drain?: boolean;\n reason?: string;\n onPendingApprovals?: 'deny' | 'preserve';\n }","headers: Record<string, string>","frame: ClientMessage","#abortController","#wsUrl","#listeners","#sseUrl","#handleFrame","#handleTransportError","#handleTransportClose","#sseFallback","#reconnect","queue: ServerEventFrame[]","lifecycle: ServerLifecycleFrame[]","replayMarkers: ServerReplayMarkerFrame[]","waiters: Array<{\n resolve: (value: IteratorResult<ServerEventFrame>) => void;\n reject: (err: Error) => void;\n }>","closeError: Error | undefined","lastEventId: string | undefined","#nextId","timer: ReturnType<typeof setTimeout> | undefined","pending: PendingRpc","#idCounter"],"sources":["../src/graphorin-client.ts"],"sourcesContent":["import pkg from '../package.json' with { type: 'json' };\n/**\n * `GraphorinClient` - ergonomic façade over the\n * {@link Transport} contract. Handles:\n *\n * - WS handshake (`openWebSocketTransport`) with optional ticket flow.\n * - Optional SSE fallback (`openSseTransport`) for environments\n * that block WebSocket upgrades.\n * - JSON-RPC request / response correlation for `subscribe` /\n * `unsubscribe` / `cancel` / `resume` / `ping` calls.\n * - Async-iterable subscriptions (`for await (const event of\n * sub.events())`).\n * - Exponential-backoff reconnect with `lastEventId` resume against\n * the server replay buffer.\n *\n * The class is intentionally small (≈ 400 LOC) so the production\n * cross-cuts (telemetry, sticky reconnect on transient errors,\n * load-shedding) live in higher-level wrappers consumers build on\n * top of `GraphorinClient` instead of leaking into the protocol\n * adapter itself.\n *\n * @packageDocumentation\n */\n\nimport {\n type ClientMessage,\n type ClientMessageId,\n isErrorFrame,\n isEventFrame,\n isLifecycleFrame,\n isPongFrame,\n isReplayMarkerFrame,\n isRpcFailure,\n isRpcSuccess,\n isSubscribedFrame,\n isUnsubscribedFrame,\n type ServerEventFrame,\n type ServerLifecycleFrame,\n type ServerMessage,\n type ServerReplayMarkerFrame,\n} from '@graphorin/protocol';\n\nimport {\n ClientAbortedError,\n ClientNotConnectedError,\n GraphorinClientError,\n kindForRpcCode,\n ProtocolViolationError,\n TransportFailedError,\n} from './errors.js';\nimport { type BackoffPolicy, computeBackoffMs, sleep } from './reconnect.js';\nimport {\n openSseTransport,\n openWebSocketTransport,\n type Transport,\n type TransportAuth,\n type TransportCloseReason,\n type TransportKind,\n} from './transport/index.js';\n\n/**\n * Discriminator for the subscription target. Mirrors the strict\n * subject grammar enforced by the server:\n * - `'session'`/`<id>` ⇒ `'session:<id>/events'`\n * - `'agent'`/`<id>` + `runId` ⇒ `'agent:<id>/runs/<runId>/events'`\n * - `'run'`/`<runId>` ⇒ `'session:<sessionId>/runs/<runId>/events'`\n * (when `sessionId` is provided)\n * - `'workflow'`/`<id>` ⇒ `'workflow:<id>/events'`\n *\n * @stable\n */\nexport type SubscriptionTarget =\n | { readonly target: 'session'; readonly id: string }\n | {\n readonly target: 'agent';\n readonly id: string;\n readonly runId: string;\n }\n | {\n readonly target: 'run';\n readonly runId: string;\n readonly sessionId?: string;\n }\n | { readonly target: 'workflow'; readonly id: string };\n\n/**\n * Transport selector. `'auto'` (default) attempts a WebSocket\n * handshake first and falls back to SSE on failure.\n *\n * @stable\n */\nexport type TransportPreference = TransportKind | 'auto';\n\n/**\n * Public configuration accepted by {@link GraphorinClient}.\n *\n * @stable\n */\nexport interface GraphorinClientOptions {\n /**\n * Session bound to the SSE fallback (IP-3): substituted into the\n * `:sessionId` slot of `sseSessionPath`. Required to connect over\n * SSE - the old client sent the literal template and could never\n * receive an event.\n */\n readonly sessionId?: string;\n /**\n * Server base URL. Examples: `'wss://graphorin.example.com'` or\n * `'http://localhost:8080'`. The path `/v1/ws` is appended for\n * the WebSocket transport.\n */\n readonly baseUrl: string;\n readonly auth: TransportAuth;\n /**\n * Transport selection. Default `'auto'`: WebSocket first, SSE\n * fallback. W-108 caveat for `'auto'`: SSE carries only the bound\n * session subject, so when a RECONNECT falls back from WS to SSE,\n * live WS subscriptions (agent / workflow subjects) cannot be\n * resumed - they are closed with a `TransportFailedError` (their\n * `for await` consumers reject immediately instead of hanging).\n * Force `'ws'` when your application depends on those subscriptions\n * surviving reconnects.\n */\n readonly transport?: TransportPreference;\n /**\n * W-152: per-subscription buffer cap. When the `for await` consumer\n * falls behind and the buffered queue reaches this many frames, the\n * subscription closes with a typed `flow-overflow` error (mirroring\n * the server's queue-overflow close) instead of growing the heap\n * without bound or silently dropping events. Default 10000 (10x the\n * server's default per-connection limit); `0` disables the cap and\n * restores the old unbounded behavior.\n */\n readonly subscriptionQueueLimit?: number;\n /** Override the WS path (default `'/v1/ws'`). */\n readonly wsPath?: string;\n /**\n * SSE path template. The placeholder `:sessionId` is replaced at\n * subscribe time. Default: `'/v1/sessions/:sessionId/events'`.\n * Required when the transport is `'sse'` or when the WS handshake\n * fails on `'auto'`.\n */\n readonly sseSessionPath?: string;\n readonly reconnect?: BackoffPolicy;\n /** Inject a `WebSocket` constructor (Node SDKs / tests). */\n readonly WebSocket?: typeof WebSocket;\n /** Inject an `EventSource` constructor (Node SDKs / tests). */\n readonly EventSource?: typeof EventSource;\n /** Inject a `fetch` implementation (defaults to `globalThis.fetch`). */\n readonly fetch?: typeof fetch;\n /** Optional client identifier surfaced on diagnostics. */\n readonly clientId?: string;\n /**\n * IP-19: per-RPC reply timeout in milliseconds. When set (and > 0), an RPC\n * that receives no matching reply within this window rejects with a\n * {@link TransportFailedError} instead of hanging forever on a\n * non-responsive server. Default: unset - no timeout, so a legitimately\n * slow server reply is never aborted (opt-in).\n */\n readonly rpcTimeoutMs?: number;\n}\n\n/**\n * Snapshot returned by {@link Subscription.metadata}.\n *\n * @stable\n */\nexport interface SubscriptionMetadata {\n readonly id: string;\n readonly subject: string;\n readonly target: SubscriptionTarget;\n readonly snapshotEventId: string | undefined;\n readonly lastEventId: string | undefined;\n readonly closed: boolean;\n /** W-152: current buffered (undelivered) event-frame count. */\n readonly queuedEvents: number;\n}\n\n/**\n * Public surface returned by {@link GraphorinClient.subscribe}.\n *\n * @stable\n */\nexport interface Subscription {\n readonly subscriptionId: string;\n readonly subject: string;\n events(): AsyncIterable<ServerEventFrame>;\n /**\n * Close the subscription on the server. Idempotent.\n */\n unsubscribe(): Promise<void>;\n metadata(): SubscriptionMetadata;\n}\n\ninterface SubscriptionInternal extends Subscription {\n __push(frame: ServerEventFrame): void;\n __pushLifecycle(frame: ServerLifecycleFrame): void;\n __pushReplayMarker(frame: ServerReplayMarkerFrame): void;\n __close(reason: 'unsubscribed' | 'transport-closed' | 'aborted', error?: Error): void;\n __subject(): string;\n __target(): SubscriptionTarget;\n __snapshotEventId(): string | undefined;\n __lastEventId(): string | undefined;\n /** IP-7: re-point this subscription at a new server subscriptionId. */\n __rebind(newSubscriptionId: string): void;\n}\n\n/**\n * @stable\n */\nexport class GraphorinClient {\n readonly #options: GraphorinClientOptions;\n readonly #pending: Map<ClientMessageId, PendingRpc> = new Map();\n readonly #subscriptions: Map<string, SubscriptionInternal> = new Map();\n /** IP-3: the synthetic single subscription an SSE connection carries. */\n #sseSubscription: SubscriptionInternal | undefined;\n #transport: Transport | undefined;\n #idCounter = 0;\n #closed = false;\n #connectingPromise: Promise<void> | undefined;\n #abortController: AbortController = new AbortController();\n\n constructor(options: GraphorinClientOptions) {\n if (typeof options.baseUrl !== 'string' || options.baseUrl.length === 0) {\n throw new TypeError('GraphorinClient: baseUrl must be a non-empty string.');\n }\n this.#options = options;\n }\n\n /**\n * Open the underlying transport. Resolves once the server has\n * accepted the handshake (`'open'`); rejects with a typed\n * {@link GraphorinClientError} otherwise.\n *\n * Calling `connect()` while already connected is a no-op; calling\n * it during another `connect()` returns the same promise.\n */\n async connect(): Promise<void> {\n if (this.#closed) {\n throw new ClientAbortedError('GraphorinClient was disconnected; create a new instance.');\n }\n if (this.#transport !== undefined) return;\n if (this.#connectingPromise !== undefined) return this.#connectingPromise;\n this.#connectingPromise = (async () => {\n const preference = this.#options.transport ?? 'auto';\n try {\n if (preference === 'ws' || preference === 'auto') {\n try {\n this.#transport = await this.#openWs();\n await this.#initializeRpc();\n return;\n } catch (err) {\n if (preference === 'ws') throw err;\n }\n }\n this.#transport = await this.#openSse();\n } finally {\n this.#connectingPromise = undefined;\n }\n })();\n return this.#connectingPromise;\n }\n\n async #initializeRpc(): Promise<void> {\n if (this.#transport?.kind !== 'ws') return;\n await this.#sendRpc('initialize', {\n clientInfo: { name: 'graphorin-client', version: pkg.version },\n });\n }\n\n /** Send a `ping` RPC and resolve when the server replies with `pong`. */\n async ping(): Promise<void> {\n await this.#sendRpc('ping');\n }\n\n /**\n * Subscribe to a server-side event stream. Resolves with a\n * {@link Subscription} once the server confirms with the matching\n * `subscribed` frame; rejects when the server returns an\n * `error` instead.\n */\n async subscribe(\n target: SubscriptionTarget,\n opts?: { readonly sinceEventId?: string },\n ): Promise<Subscription> {\n if (this.#transport === undefined) throw new ClientNotConnectedError();\n if (this.#transport.kind === 'sse') {\n // IP-3: the SSE connection IS one implicit subscription to the\n // bound session's events. Frames arrive with a server-generated\n // subscriptionId the client cannot know upfront, so routing\n // falls through to this synthetic subscription (see\n // #handleFrame). Other subjects still require the WS transport.\n const subject = subjectFor(target);\n const boundSubject = `session:${this.#options.sessionId ?? ''}/events`;\n if (subject !== boundSubject) {\n throw new TransportFailedError(\n `subscribe('${subject}') requires the WebSocket transport - the SSE fallback carries only the bound session stream ('${boundSubject}').`,\n );\n }\n // periphery-03: a CLOSED bound subscription (a prior terminal\n // reconnect failure) must not be handed back - recreate it so a\n // recovered client delivers events again.\n if (this.#sseSubscription === undefined || this.#sseSubscription.metadata().closed) {\n this.#sseSubscription = this.#createSubscription({\n subscriptionId: '__sse__',\n subject,\n target,\n snapshotEventId: undefined,\n });\n this.#subscriptions.set('__sse__', this.#sseSubscription);\n }\n return this.#sseSubscription;\n }\n const subject = subjectFor(target);\n // IP-7: a resubscribe passes the SUBSCRIPTION's own cursor - the\n // fresh transport's lastEventId is always undefined, so the old\n // code never consulted the server replay buffer.\n const lastEventId = opts?.sinceEventId ?? this.#transport.lastEventId;\n const params: { subject: string; sinceEventId?: string } = {\n subject,\n };\n if (lastEventId !== undefined) params.sinceEventId = lastEventId;\n const reply = await this.#sendRpc('subscription.subscribe', params);\n const result = reply as { subscriptionId?: unknown; snapshotEventId?: unknown };\n const subscriptionId =\n typeof result.subscriptionId === 'string' && result.subscriptionId.length > 0\n ? result.subscriptionId\n : undefined;\n if (subscriptionId === undefined) {\n throw new ProtocolViolationError('Server subscribe reply missing subscriptionId.');\n }\n const snapshotEventId =\n typeof result.snapshotEventId === 'string' ? result.snapshotEventId : undefined;\n const sub = this.#createSubscription({\n subscriptionId,\n subject,\n target,\n snapshotEventId,\n });\n this.#subscriptions.set(subscriptionId, sub);\n return sub;\n }\n\n /**\n * Cancel a server-side run. Sends the `run.cancel` RPC and\n * resolves with the server's `result` payload (typically\n * `{ cancelled: true, partialStateAvailable: true }`).\n */\n async cancel(\n runId: string,\n opts: {\n readonly drain?: boolean;\n readonly reason?: string;\n readonly onPendingApprovals?: 'deny' | 'preserve';\n } = {},\n ): Promise<unknown> {\n if (typeof runId !== 'string' || runId.length === 0) {\n throw new TypeError('cancel: runId must be a non-empty string.');\n }\n const params: {\n runId: string;\n drain?: boolean;\n reason?: string;\n onPendingApprovals?: 'deny' | 'preserve';\n } = { runId };\n if (opts.drain !== undefined) params.drain = opts.drain;\n if (opts.reason !== undefined) params.reason = opts.reason;\n if (opts.onPendingApprovals !== undefined) {\n params.onPendingApprovals = opts.onPendingApprovals;\n }\n return this.#sendRpc('run.cancel', params);\n }\n\n /**\n * Resume a paused (HITL) run. The WebSocket protocol intentionally\n * does NOT carry a `resume` control message - resumes are durable\n * + idempotent + body-carrying, which maps onto the REST endpoint\n * `POST /v1/runs/:runId/resume`. NOTE (IP-14): the server endpoint\n * currently answers **501** - server-side durable resume is not\n * implemented yet. Library-mode callers resume directly:\n * `agent.run(result.state, { directive })`.\n */\n async resume(\n runId: string,\n directive?: unknown,\n opts: { readonly idempotencyKey?: string } = {},\n ): Promise<unknown> {\n if (typeof runId !== 'string' || runId.length === 0) {\n throw new TypeError('resume: runId must be a non-empty string.');\n }\n const fetchImpl = this.#options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new TransportFailedError(\n 'No fetch implementation available; pass `fetch` via the client options.',\n );\n }\n const url = joinUrl(this.#options.baseUrl, `/v1/runs/${encodeURIComponent(runId)}/resume`, {\n ws: false,\n });\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (this.#options.auth.kind === 'bearer') {\n headers.Authorization = `Bearer ${this.#options.auth.token}`;\n }\n if (opts.idempotencyKey !== undefined) headers['Idempotency-Key'] = opts.idempotencyKey;\n const res = await fetchImpl(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(directive === undefined ? {} : { directive }),\n });\n if (!res.ok) {\n throw new TransportFailedError(\n `resume(${runId}): server responded ${res.status} ${res.statusText}.`,\n { code: res.status },\n );\n }\n try {\n return await res.json();\n } catch {\n return undefined;\n }\n }\n\n /**\n * Send an MCP-compatible cancellation notification. Does not wait\n * for a server reply (notifications have no `id`).\n */\n cancelNotify(requestId: string): void {\n if (this.#transport === undefined) throw new ClientNotConnectedError();\n if (typeof requestId !== 'string' || requestId.length === 0) {\n throw new TypeError('cancelNotify: requestId must be a non-empty string.');\n }\n const frame: ClientMessage = {\n v: '1',\n jsonrpc: '2.0',\n method: 'notifications/cancelled',\n params: { requestId },\n };\n this.#transport.send(frame);\n }\n\n /**\n * Disconnect the underlying transport and abort every pending RPC\n * + subscription. Idempotent.\n */\n async disconnect(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n const transport = this.#transport;\n this.#transport = undefined;\n this.#abortController.abort(new ClientAbortedError());\n for (const pending of this.#pending.values()) {\n pending.reject(new ClientAbortedError('Client disconnected before reply.'));\n }\n this.#pending.clear();\n for (const sub of this.#subscriptions.values()) {\n sub.__close('aborted', new ClientAbortedError('Client disconnected.'));\n }\n this.#subscriptions.clear();\n if (transport !== undefined) {\n try {\n transport.close(1000, 'client-disconnect');\n } catch {\n // Best-effort close: never throw from disconnect().\n }\n }\n }\n\n /** Return the active transport kind (or `undefined` if not connected). */\n get transportKind(): TransportKind | undefined {\n return this.#transport?.kind;\n }\n\n // -----------------------------------------------------------------\n // Internal helpers\n // -----------------------------------------------------------------\n\n async #openWs(): Promise<Transport> {\n const url = this.#wsUrl();\n const transport = await openWebSocketTransport(\n {\n url,\n auth: this.#options.auth,\n ...(this.#options.WebSocket !== undefined ? { WebSocket: this.#options.WebSocket } : {}),\n ...(this.#options.clientId !== undefined ? { clientId: this.#options.clientId } : {}),\n },\n this.#listeners('ws'),\n );\n return transport;\n }\n\n async #openSse(): Promise<Transport> {\n if (this.#options.auth.kind !== 'bearer') {\n throw new TransportFailedError(\n \"SSE fallback requires the 'bearer' auth strategy. The WebSocket ticket flow does not extend to EventSource.\",\n );\n }\n const url = this.#sseUrl();\n // periphery-03: resume from where the bound session subscription\n // left off - without the cursor every reconnect replays the whole\n // server buffer into the consumer.\n const resumeCursor = this.#sseSubscription?.__lastEventId();\n const transport = await openSseTransport(\n {\n url,\n auth: this.#options.auth,\n // IP-3: the fetch-streaming transport uses the client's fetch\n // seam (the EventSource seam is gone with the rewrite).\n ...(this.#options.fetch !== undefined ? { fetch: this.#options.fetch } : {}),\n ...(this.#options.clientId !== undefined ? { clientId: this.#options.clientId } : {}),\n ...(resumeCursor !== undefined ? { lastEventId: resumeCursor } : {}),\n },\n this.#listeners('sse'),\n );\n return transport;\n }\n\n #wsUrl(): string {\n const path = this.#options.wsPath ?? '/v1/ws';\n return joinUrl(this.#options.baseUrl, path, { ws: true });\n }\n\n #sseUrl(): string {\n const template = this.#options.sseSessionPath ?? '/v1/sessions/:sessionId/events';\n // IP-3: bind the session into the path - the literal template was\n // previously sent as-is, so the SSE fallback could never connect\n // to a real session stream.\n if (template.includes(':sessionId')) {\n const sessionId = this.#options.sessionId;\n if (sessionId === undefined || sessionId.length === 0) {\n throw new TransportFailedError(\n \"The SSE fallback needs a session binding - pass `sessionId` in the client options (it fills ':sessionId' in sseSessionPath).\",\n );\n }\n return joinUrl(\n this.#options.baseUrl,\n template.replace(':sessionId', encodeURIComponent(sessionId)),\n { ws: false },\n );\n }\n return joinUrl(this.#options.baseUrl, template, { ws: false });\n }\n\n #listeners(kind: TransportKind): {\n onOpen(): void;\n onFrame(frame: ServerMessage): void;\n onError(err: Error): void;\n onClose(reason: TransportCloseReason): void;\n } {\n return {\n onOpen: () => {},\n onFrame: (frame) => this.#handleFrame(frame),\n onError: (err) => this.#handleTransportError(err, kind),\n onClose: (reason) => this.#handleTransportClose(reason, kind),\n };\n }\n\n #handleFrame(frame: ServerMessage): void {\n if (isRpcSuccess(frame)) {\n const pending = this.#pending.get(frame.id);\n if (pending !== undefined) {\n this.#pending.delete(frame.id);\n pending.resolve(frame.result);\n }\n return;\n }\n if (isRpcFailure(frame)) {\n const pending = this.#pending.get(frame.id);\n if (pending !== undefined) {\n this.#pending.delete(frame.id);\n // IP-19: surface the server's error class (rate-limited, scope-denied,\n // auth-failed, …) instead of collapsing every RPC failure to\n // 'protocol-violation'.\n pending.reject(\n new GraphorinClientError(\n kindForRpcCode(frame.error.code),\n `Server returned an error for RPC '${frame.id}': ${frame.error.message} (code=${frame.error.code}).`,\n ),\n );\n }\n return;\n }\n if (isSubscribedFrame(frame)) {\n // Subscribed frames are acknowledged by the matching RPC reply.\n return;\n }\n if (isUnsubscribedFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId);\n if (sub !== undefined) {\n sub.__close('unsubscribed');\n this.#subscriptions.delete(frame.subscriptionId);\n }\n return;\n }\n if (isPongFrame(frame)) return;\n if (isLifecycleFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId) ?? this.#sseFallback();\n if (sub !== undefined) sub.__pushLifecycle(frame);\n return;\n }\n if (isReplayMarkerFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId) ?? this.#sseFallback();\n if (sub !== undefined) sub.__pushReplayMarker(frame);\n return;\n }\n if (isEventFrame(frame)) {\n const sub = this.#subscriptions.get(frame.subscriptionId) ?? this.#sseFallback();\n if (sub !== undefined) sub.__push(frame);\n return;\n }\n if (isErrorFrame(frame)) {\n const target =\n frame.subscriptionId !== undefined\n ? this.#subscriptions.get(frame.subscriptionId)\n : undefined;\n const error = new GraphorinClientError(\n 'protocol-violation',\n `Server error frame: ${frame.message} (code=${frame.code}).`,\n );\n if (target !== undefined) {\n target.__close('aborted', error);\n if (frame.subscriptionId !== undefined) this.#subscriptions.delete(frame.subscriptionId);\n }\n }\n }\n\n /**\n * IP-3: on the SSE transport every frame belongs to the single\n * implicit session subscription regardless of the server-generated\n * subscriptionId it carries.\n */\n #sseFallback(): SubscriptionInternal | undefined {\n return this.#transport?.kind === 'sse' ? this.#sseSubscription : undefined;\n }\n\n #handleTransportError(err: Error, _kind: TransportKind): void {\n for (const pending of this.#pending.values()) {\n pending.reject(err);\n }\n this.#pending.clear();\n }\n\n async #handleTransportClose(reason: TransportCloseReason, _kind: TransportKind): Promise<void> {\n if (this.#closed) return;\n this.#transport = undefined;\n const closeError = new TransportFailedError(\n `Transport closed: ${reason.reason || reason.graphorinReason || 'unknown'} (code=${reason.code}).`,\n { code: reason.code },\n );\n // Best-effort drain of pending RPCs so callers see the failure\n // immediately rather than hanging until the reconnect window\n // closes.\n for (const pending of this.#pending.values()) {\n pending.reject(closeError);\n }\n this.#pending.clear();\n if (\n reason.graphorinReason === 'auth.required' ||\n reason.graphorinReason === 'auth.invalid' ||\n reason.graphorinReason === 'auth.revoked' ||\n reason.graphorinReason === 'protocol.violation'\n ) {\n // Fatal: stop trying to reconnect and surface the failure.\n for (const sub of this.#subscriptions.values()) {\n sub.__close('aborted', closeError);\n }\n this.#subscriptions.clear();\n return;\n }\n await this.#reconnect();\n }\n\n async #reconnect(): Promise<void> {\n if (this.#closed) return;\n let attempt = 0;\n while (!this.#closed) {\n attempt += 1;\n const delay = computeBackoffMs(attempt, this.#options.reconnect);\n if (delay === null) {\n const exhausted = new TransportFailedError(\n `Reconnect attempts exhausted after ${attempt - 1} retries.`,\n );\n for (const sub of this.#subscriptions.values()) {\n sub.__close('aborted', exhausted);\n }\n this.#subscriptions.clear();\n return;\n }\n try {\n await sleep(delay, this.#abortController.signal);\n await this.connect();\n // Best-effort resubscribe with the recorded lastEventId so\n // the server replays missed events from the buffer.\n const transport = this.#transport;\n if (transport === undefined) continue;\n // periphery-03: on the SSE fallback the connection IS the\n // subscription - the reconnect already carried the resume\n // cursor as `Last-Event-ID`, and frames keep flowing into the\n // same bound subscription object. The RPC resubscribe below\n // would call `transport.send(...)`, which the read-only SSE\n // transport rejects; pre-fix that error CLOSED the\n // subscription and the client silently stopped delivering\n // events for the life of the object.\n if (transport.kind === 'sse') {\n // W-108: transport 'auto' can reconnect a WS-FIRST client\n // over SSE when the WS handshake flakes. SSE carries exactly\n // ONE bound-session subject ('__sse__'); a surviving WS\n // subscription (agent/workflow subject) is unreachable over\n // it - no frame will ever route to it and its consumer would\n // block in `for await` forever. Close them with a typed\n // error instead of a silent hang (and instead of an endless\n // WS retry, which would wedge against servers with WS\n // disabled): the application decides whether to reconnect\n // with `transport: 'ws'` or read the bound session subject.\n const fellBack = new TransportFailedError(\n \"Reconnect fell back to SSE; WebSocket subscriptions cannot be resumed over SSE - resubscribe on the bound session subject or force transport: 'ws'.\",\n );\n for (const [id, sub] of [...this.#subscriptions]) {\n if (id === '__sse__') continue;\n this.#subscriptions.delete(id);\n sub.__close('aborted', fellBack);\n }\n return;\n }\n for (const [oldId, sub] of [...this.#subscriptions]) {\n this.#subscriptions.delete(oldId);\n try {\n // IP-7: re-establish the server-side subscription with the\n // SUBSCRIPTION's own replay cursor, then re-point the SAME\n // object at the new server id - the consumer's in-flight\n // `for await` survives and the replayed events arrive in\n // the iterator it is already reading. The old code created\n // a NEW subscription and closed this one: the consumer's\n // loop ended `{done: true}` while events piled up unread\n // in an orphan.\n const cursor = sub.__lastEventId();\n const reply = await this.#sendRpc('subscription.subscribe', {\n subject: sub.__subject(),\n ...(cursor !== undefined ? { sinceEventId: cursor } : {}),\n });\n const result = reply as { subscriptionId?: unknown };\n const newId =\n typeof result.subscriptionId === 'string' && result.subscriptionId.length > 0\n ? result.subscriptionId\n : undefined;\n if (newId === undefined) {\n throw new ProtocolViolationError('Server resubscribe reply missing subscriptionId.');\n }\n sub.__rebind(newId);\n this.#subscriptions.set(newId, sub);\n } catch (err) {\n sub.__close('aborted', err instanceof Error ? err : new Error(String(err)));\n }\n }\n return;\n } catch (err) {\n if (this.#closed) return;\n // Loop and retry.\n if (err instanceof ClientAbortedError) return;\n }\n }\n }\n\n #createSubscription(args: {\n subscriptionId: string;\n readonly subject: string;\n readonly target: SubscriptionTarget;\n readonly snapshotEventId: string | undefined;\n }): SubscriptionInternal {\n const queue: ServerEventFrame[] = [];\n const lifecycle: ServerLifecycleFrame[] = [];\n const replayMarkers: ServerReplayMarkerFrame[] = [];\n const waiters: Array<{\n resolve: (value: IteratorResult<ServerEventFrame>) => void;\n reject: (err: Error) => void;\n }> = [];\n let closed = false;\n let closeError: Error | undefined;\n let lastEventId: string | undefined = args.snapshotEventId;\n\n const queueLimit = this.#options.subscriptionQueueLimit ?? 10_000;\n const push = (frame: ServerEventFrame): void => {\n // W-152: frames arriving after close (server-side unsubscribe is\n // async) must not keep growing a queue nobody will drain.\n if (closed) return;\n lastEventId = frame.eventId;\n const waiter = waiters.shift();\n if (waiter !== undefined) {\n waiter.resolve({ value: frame, done: false });\n return;\n }\n if (queueLimit > 0 && queue.length >= queueLimit) {\n // W-152: deterministic policy over silent loss - mirror the\n // server's queue-overflow close (4006 flow.throttled) with a\n // typed client error the pending/next for-await observes.\n close(\n 'aborted',\n new GraphorinClientError(\n 'flow-overflow',\n `Subscription '${args.subscriptionId}' buffered ${queue.length} events with no consumer progress (subscriptionQueueLimit=${queueLimit}). Consume the events() iterator faster, raise the limit, or set 0 to disable the cap.`,\n ),\n );\n return;\n }\n queue.push(frame);\n };\n\n const close = (reason: 'unsubscribed' | 'transport-closed' | 'aborted', err?: Error): void => {\n if (closed) return;\n closed = true;\n if (err !== undefined) closeError = err;\n else if (reason === 'aborted') closeError = new ClientAbortedError();\n while (waiters.length > 0) {\n const waiter = waiters.shift();\n if (waiter === undefined) continue;\n // IP-19(c): a waiter blocked in next() must observe the SAME outcome a\n // fresh next() would after close - reject with closeError when the\n // stream tore down abnormally, rather than silently resolving done:true\n // and swallowing the transport/abort failure.\n if (closeError !== undefined) {\n waiter.reject(closeError);\n } else {\n waiter.resolve({ value: undefined as unknown as ServerEventFrame, done: true });\n }\n }\n };\n\n const sub: SubscriptionInternal = {\n get subscriptionId(): string {\n return args.subscriptionId;\n },\n subject: args.subject,\n events: () => ({\n [Symbol.asyncIterator]: () => ({\n next: (): Promise<IteratorResult<ServerEventFrame>> => {\n const buffered = queue.shift();\n if (buffered !== undefined) {\n return Promise.resolve({ value: buffered, done: false });\n }\n if (closed) {\n if (closeError !== undefined) return Promise.reject(closeError);\n return Promise.resolve({\n value: undefined as unknown as ServerEventFrame,\n done: true,\n });\n }\n return new Promise<IteratorResult<ServerEventFrame>>((resolve, reject) => {\n waiters.push({ resolve, reject });\n });\n },\n return: (): Promise<IteratorResult<ServerEventFrame>> => {\n close('unsubscribed');\n return Promise.resolve({ value: undefined as unknown as ServerEventFrame, done: true });\n },\n }),\n }),\n unsubscribe: async () => {\n if (closed) return;\n try {\n await this.#sendRpc('subscription.unsubscribe', {\n subscriptionId: args.subscriptionId,\n });\n } catch (err) {\n if (!(err instanceof ClientAbortedError)) throw err;\n }\n close('unsubscribed');\n this.#subscriptions.delete(args.subscriptionId);\n },\n metadata: () => ({\n id: args.subscriptionId,\n subject: args.subject,\n target: args.target,\n snapshotEventId: args.snapshotEventId,\n lastEventId,\n closed,\n queuedEvents: queue.length,\n }),\n __push: push,\n __pushLifecycle: (frame) => {\n lifecycle.push(frame);\n if (\n frame.status === 'completed' ||\n frame.status === 'aborted' ||\n frame.status === 'failed'\n ) {\n close(frame.status === 'completed' ? 'unsubscribed' : 'aborted');\n this.#subscriptions.delete(args.subscriptionId);\n }\n },\n __pushReplayMarker: (frame) => {\n replayMarkers.push(frame);\n },\n __close: close,\n __subject: () => args.subject,\n __target: () => args.target,\n __snapshotEventId: () => args.snapshotEventId,\n __lastEventId: () => lastEventId,\n __rebind: (newSubscriptionId: string) => {\n args.subscriptionId = newSubscriptionId;\n },\n };\n return sub;\n }\n\n async #sendRpc(method: ClientMessage['method'], params?: object): Promise<unknown> {\n if (this.#transport === undefined) throw new ClientNotConnectedError();\n const id = this.#nextId();\n const frame = buildRpcFrame(method, id, params);\n const timeoutMs = this.#options.rpcTimeoutMs;\n return new Promise((resolve, reject) => {\n // IP-19: arm an optional reply timer. Wrapping resolve/reject so that\n // EVERY settle path (matching reply, disconnect, transport error) clears\n // the timer - otherwise a stray timer could reject an already-settled id.\n let timer: ReturnType<typeof setTimeout> | undefined;\n const clear = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n const pending: PendingRpc = {\n resolve: (value) => {\n clear();\n resolve(value);\n },\n reject: (reason) => {\n clear();\n reject(reason);\n },\n };\n this.#pending.set(id, pending);\n if (timeoutMs !== undefined && timeoutMs > 0) {\n timer = setTimeout(() => {\n // Fire only if this exact request is still outstanding.\n if (this.#pending.get(id) === pending) {\n this.#pending.delete(id);\n reject(\n new TransportFailedError(\n `RPC '${method}' (id=${id}) timed out after ${timeoutMs}ms with no server reply.`,\n ),\n );\n }\n }, timeoutMs);\n }\n try {\n // Asserting non-null because we just verified above.\n (this.#transport as Transport).send(frame);\n } catch (err) {\n clear();\n this.#pending.delete(id);\n reject(err);\n }\n });\n }\n\n #nextId(): string {\n this.#idCounter += 1;\n const prefix = this.#options.clientId ?? 'graphorin';\n return `${prefix}-${this.#idCounter.toString(36)}`;\n }\n}\n\ninterface PendingRpc {\n readonly resolve: (value: unknown) => void;\n readonly reject: (reason: Error) => void;\n}\n\nfunction subjectFor(target: SubscriptionTarget): string {\n switch (target.target) {\n case 'session':\n assertNonEmpty(target.id, 'session.id');\n return `session:${target.id}/events`;\n case 'agent':\n assertNonEmpty(target.id, 'agent.id');\n assertNonEmpty(target.runId, 'agent.runId');\n return `agent:${target.id}/runs/${target.runId}/events`;\n case 'run':\n assertNonEmpty(target.runId, 'run.runId');\n if (target.sessionId !== undefined) {\n assertNonEmpty(target.sessionId, 'run.sessionId');\n return `session:${target.sessionId}/runs/${target.runId}/events`;\n }\n // periphery-09: the server subject grammar has NO bare `run:`\n // form - the old `run:<id>/events` fallback always failed\n // server-side with an opaque unknown-subject error. Fail fast\n // with an actionable message instead.\n throw new TypeError(\n \"subscribe({ target: 'run' }) requires `sessionId` - the server subject grammar has no bare run form. \" +\n \"Pass { kind: 'run', runId, sessionId } or use the 'agent' target with the agent id.\",\n );\n case 'workflow':\n assertNonEmpty(target.id, 'workflow.id');\n return `workflow:${target.id}/events`;\n }\n}\n\nfunction assertNonEmpty(value: string, fieldName: string): void {\n if (typeof value !== 'string' || value.length === 0) {\n throw new TypeError(`${fieldName} must be a non-empty string.`);\n }\n}\n\nfunction joinUrl(baseUrl: string, path: string, opts: { readonly ws: boolean }): string {\n let normalized = baseUrl;\n if (opts.ws) {\n if (normalized.startsWith('http://')) normalized = normalized.replace(/^http:\\/\\//, 'ws://');\n else if (normalized.startsWith('https://'))\n normalized = normalized.replace(/^https:\\/\\//, 'wss://');\n } else {\n if (normalized.startsWith('ws://')) normalized = normalized.replace(/^ws:\\/\\//, 'http://');\n else if (normalized.startsWith('wss://'))\n normalized = normalized.replace(/^wss:\\/\\//, 'https://');\n }\n if (normalized.endsWith('/')) normalized = normalized.slice(0, -1);\n const tail = path.startsWith('/') ? path : `/${path}`;\n return `${normalized}${tail}`;\n}\n\nfunction buildRpcFrame(\n method: ClientMessage['method'],\n id: ClientMessageId,\n params?: object,\n): ClientMessage {\n switch (method) {\n case 'initialize':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'initialize',\n params: (params ?? {\n clientInfo: { name: 'graphorin-client', version: pkg.version },\n }) as never,\n };\n case 'subscription.subscribe':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'subscription.subscribe',\n params: (params ?? { subject: '' }) as never,\n };\n case 'subscription.unsubscribe':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'subscription.unsubscribe',\n params: (params ?? { subscriptionId: '' }) as never,\n };\n case 'run.cancel':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'run.cancel',\n params: (params ?? { runId: '' }) as never,\n };\n case 'ping':\n return {\n v: '1',\n jsonrpc: '2.0',\n id,\n method: 'ping',\n ...(params !== undefined ? { params: params as never } : {}),\n };\n case 'notifications/cancelled':\n throw new TypeError(\n \"buildRpcFrame: 'notifications/cancelled' is a notification, not an RPC.\",\n );\n }\n // Exhaustiveness check; should be unreachable.\n throw new TypeError(`Unknown RPC method '${method}'.`);\n}\n"],"mappings":"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"}
|
package/dist/package.js
CHANGED
package/dist/package.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/client\",\n \"version\": \"0.
|
|
1
|
+
{"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/client\",\n \"version\": \"0.7.0\",\n \"description\": \"Reference TypeScript client for the Graphorin standalone server. Wraps the WebSocket subprotocol `graphorin.protocol.v1` (with the optional Server-Sent Events fallback for proxy-restricted environments) behind an ergonomic `GraphorinClient` class: `connect()`, `subscribe({ target, id })` returning an async-iterable subscription, `cancel(runId, opts)`, `resume(runId, directive)`, `ping()`, `disconnect()`. Handles the browser ticket flow (single-use ticket attached as a `ticket.<value>` `Sec-WebSocket-Protocol` token), exponential-backoff reconnect with `lastEventId` resume against the server replay buffer, and Zod-validated frame parsing on both directions. Browser-friendly: zero Node-only dependencies; the only runtime dependencies are `@graphorin/protocol` and `zod`. Created and maintained by Oleksiy Stepurenko.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/client\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/client\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"client\",\n \"websocket\",\n \"sse\",\n \"browser-friendly\",\n \"ticket-flow\",\n \"reconnect\"\n ],\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"sideEffects\": false,\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./client\": {\n \"types\": \"./dist/graphorin-client.d.ts\",\n \"default\": \"./dist/graphorin-client.js\"\n },\n \"./transport\": {\n \"types\": \"./dist/transport/index.d.ts\",\n \"default\": \"./dist/transport/index.js\"\n },\n \"./reconnect\": {\n \"types\": \"./dist/reconnect.d.ts\",\n \"default\": \"./dist/reconnect.js\"\n },\n \"./errors\": {\n \"types\": \"./dist/errors.d.ts\",\n \"default\": \"./dist/errors.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/protocol\": \"workspace:*\",\n \"zod\": \"^3.25.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/security\": \"workspace:*\",\n \"@graphorin/server\": \"workspace:*\",\n \"@graphorin/store-sqlite\": \"workspace:*\",\n \"@types/ws\": \"^8.18.1\",\n \"ws\": \"^8.20.1\"\n }\n}\n"],"mappings":";cAEa"}
|
package/dist/reconnect.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//#region src/reconnect.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Pure-functional reconnect-backoff helper. Encapsulated in its own
|
|
4
|
-
* module so the
|
|
4
|
+
* module so the `GraphorinClient` stays free of timing
|
|
5
5
|
* heuristics - and so tests can drive the policy with a deterministic
|
|
6
6
|
* RNG.
|
|
7
7
|
*
|
|
@@ -43,8 +43,8 @@ interface BackoffPolicy {
|
|
|
43
43
|
declare function computeBackoffMs(attempt: number, policy?: BackoffPolicy): number | null;
|
|
44
44
|
/**
|
|
45
45
|
* Resolve when the requested number of milliseconds elapsed, or
|
|
46
|
-
* reject (with a
|
|
47
|
-
* supplied
|
|
46
|
+
* reject (with a `DOMException`-style abort error) when the
|
|
47
|
+
* supplied `AbortSignal` fires first.
|
|
48
48
|
*
|
|
49
49
|
* @stable
|
|
50
50
|
*/
|
package/dist/reconnect.js
CHANGED
|
@@ -18,8 +18,8 @@ function computeBackoffMs(attempt, policy = {}) {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Resolve when the requested number of milliseconds elapsed, or
|
|
21
|
-
* reject (with a
|
|
22
|
-
* supplied
|
|
21
|
+
* reject (with a `DOMException`-style abort error) when the
|
|
22
|
+
* supplied `AbortSignal` fires first.
|
|
23
23
|
*
|
|
24
24
|
* @stable
|
|
25
25
|
*/
|
package/dist/reconnect.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconnect.js","names":[],"sources":["../src/reconnect.ts"],"sourcesContent":["/**\n * Pure-functional reconnect-backoff helper. Encapsulated in its own\n * module so the
|
|
1
|
+
{"version":3,"file":"reconnect.js","names":[],"sources":["../src/reconnect.ts"],"sourcesContent":["/**\n * Pure-functional reconnect-backoff helper. Encapsulated in its own\n * module so the `GraphorinClient` stays free of timing\n * heuristics - and so tests can drive the policy with a deterministic\n * RNG.\n *\n * Algorithm: exponential backoff with full-jitter\n * (`delay = random(0, min(maxMs, baseMs * 2^attempt))`). The\n * implementation matches the AWS Architecture Blog \"exponential\n * backoff and jitter\" reference but is otherwise an original\n * formulation.\n *\n * @packageDocumentation\n */\n\n/**\n * Stable shape consumed by {@link computeBackoffMs}.\n *\n * @stable\n */\nexport interface BackoffPolicy {\n /** Initial slot in milliseconds. Default `500`. */\n readonly baseMs?: number;\n /** Cap on every individual sleep. Default `30_000`. */\n readonly maxMs?: number;\n /**\n * Hard cap on the number of attempts. The client surfaces a\n * `TransportFailedError` once exceeded. Default `Infinity`.\n */\n readonly maxAttempts?: number;\n /**\n * Optional injection seam used by tests; defaults to `Math.random`.\n */\n readonly random?: () => number;\n}\n\n/**\n * Compute the number of milliseconds to sleep before the\n * `attempt`-th reconnect (1-indexed). Returns `null` when the policy\n * has been exhausted (`attempt > maxAttempts`).\n *\n * @stable\n */\nexport function computeBackoffMs(attempt: number, policy: BackoffPolicy = {}): number | null {\n if (!Number.isFinite(attempt) || attempt < 1) {\n throw new RangeError('computeBackoffMs: attempt must be a positive integer.');\n }\n const baseMs = policy.baseMs ?? 500;\n const maxMs = policy.maxMs ?? 30_000;\n const maxAttempts = policy.maxAttempts ?? Number.POSITIVE_INFINITY;\n const random = policy.random ?? Math.random;\n if (attempt > maxAttempts) return null;\n // Exponential growth with full jitter, clamped to maxMs.\n const exp = Math.min(maxMs, baseMs * 2 ** Math.min(attempt - 1, 30));\n return Math.floor(random() * exp);\n}\n\n/**\n * Resolve when the requested number of milliseconds elapsed, or\n * reject (with a `DOMException`-style abort error) when the\n * supplied `AbortSignal` fires first.\n *\n * @stable\n */\nexport function sleep(durationMs: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(abortError(signal.reason));\n }\n return new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n cleanup();\n resolve();\n }, durationMs);\n const onAbort = (): void => {\n cleanup();\n reject(abortError(signal?.reason));\n };\n const cleanup = (): void => {\n clearTimeout(timeout);\n signal?.removeEventListener('abort', onAbort);\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nfunction abortError(reason: unknown): Error {\n if (reason instanceof Error) return reason;\n const err = new Error('Aborted.');\n err.name = 'AbortError';\n return err;\n}\n"],"mappings":";;;;;;;;AA2CA,SAAgB,iBAAiB,SAAiB,SAAwB,EAAE,EAAiB;AAC3F,KAAI,CAAC,OAAO,SAAS,QAAQ,IAAI,UAAU,EACzC,OAAM,IAAI,WAAW,wDAAwD;CAE/E,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,QAAQ,OAAO,SAAS;CAC9B,MAAM,cAAc,OAAO,eAAe,OAAO;CACjD,MAAM,SAAS,OAAO,UAAU,KAAK;AACrC,KAAI,UAAU,YAAa,QAAO;CAElC,MAAM,MAAM,KAAK,IAAI,OAAO,SAAS,KAAK,KAAK,IAAI,UAAU,GAAG,GAAG,CAAC;AACpE,QAAO,KAAK,MAAM,QAAQ,GAAG,IAAI;;;;;;;;;AAUnC,SAAgB,MAAM,YAAoB,QAAqC;AAC7E,KAAI,QAAQ,QACV,QAAO,QAAQ,OAAO,WAAW,OAAO,OAAO,CAAC;AAElD,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,UAAU,iBAAiB;AAC/B,YAAS;AACT,YAAS;KACR,WAAW;EACd,MAAM,gBAAsB;AAC1B,YAAS;AACT,UAAO,WAAW,QAAQ,OAAO,CAAC;;EAEpC,MAAM,gBAAsB;AAC1B,gBAAa,QAAQ;AACrB,WAAQ,oBAAoB,SAAS,QAAQ;;AAE/C,UAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;GAC1D;;AAGJ,SAAS,WAAW,QAAwB;AAC1C,KAAI,kBAAkB,MAAO,QAAO;CACpC,MAAM,sBAAM,IAAI,MAAM,WAAW;AACjC,KAAI,OAAO;AACX,QAAO"}
|
|
@@ -88,7 +88,7 @@ interface Transport {
|
|
|
88
88
|
* Send a client → server frame. Throws when the transport is not
|
|
89
89
|
* in the open state, or when the underlying back-end does not
|
|
90
90
|
* support send (the SSE transport throws every send via
|
|
91
|
-
*
|
|
91
|
+
* `TransportFailedError` - clients
|
|
92
92
|
* should fall back to REST for control-plane operations on SSE).
|
|
93
93
|
*/
|
|
94
94
|
send(frame: ClientMessage): void;
|