@graphorin/client 0.5.0 → 0.6.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 +29 -0
- package/README.md +5 -5
- package/dist/graphorin-client.d.ts +4 -4
- package/dist/graphorin-client.d.ts.map +1 -1
- package/dist/graphorin-client.js +17 -12
- package/dist/graphorin-client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/reconnect.d.ts +1 -1
- package/dist/reconnect.js.map +1 -1
- package/dist/transport/sse.js +5 -4
- package/dist/transport/sse.js.map +1 -1
- package/dist/transport/types.d.ts +9 -1
- package/dist/transport/types.d.ts.map +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @graphorin/client
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#135](https://github.com/o-stepper/graphorin/pull/135) [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3) Thanks [@o-stepper](https://github.com/o-stepper)! - Server / client / periphery correctness (audit 2026-07-04 Wave B, cluster B6).
|
|
8
|
+
|
|
9
|
+
- periphery-01: `POST /v1/workflows/:id/resume` actually resumes - it background-iterates `workflow.resume(threadId, {resume})` and emits every event on the `workflow:<id>/runs/<runId>/events` subject (mirroring execute); the fictional unmounted SSE URL is gone and the run completes. `POST /v1/workflows/:id/fork` returns an honest 501 instead of a 202 that forked nothing.
|
|
10
|
+
- periphery-02: `standalone-server.md` now states plainly that `graphorin start` serves health/metrics/tokens/tickets/WS-SSE only and that the domain routes mount only when adapters are composed programmatically via `createServer({...})`.
|
|
11
|
+
- periphery-03: the client SSE fallback survives reconnects - `#reconnect` no longer attempts the RPC resubscribe on the read-only SSE transport (which threw and permanently killed the subscription); the reconnect carries the subscription's cursor as a `Last-Event-ID` header so the server replays only missed events; a closed bound subscription is recreated on the next `subscribe()`.
|
|
12
|
+
- periphery-08: the idempotency middleware tracks in-flight keyed executions - a concurrent duplicate gets `409 idempotency-in-flight` + `Retry-After` instead of double-executing (per draft-ietf-httpapi-idempotency-key-header).
|
|
13
|
+
- periphery-09: `subscribe({ target: 'run' })` without `sessionId` throws a clear client-side error instead of building a `run:` subject the server grammar can never accept.
|
|
14
|
+
- periphery-10: session-stream subjects gate on `sessions:read:<sessionId>` (read-only streams, sessionId resource slot) instead of `agents:invoke:<sessionId>`, aligning with the SSE route's requirement.
|
|
15
|
+
- periphery-11: interval triggers under `catchupPolicy: 'none'` advance to the next FUTURE boundary after downtime instead of firing immediately on restart; `recordActivity()` no longer arms idle timers on a stopped scheduler (P-14).
|
|
16
|
+
- periphery-05: the TransformersJS embedder throws for an unknown model with no `dim` hint (the PS-11 fix ported from the Ollama embedder) instead of assuming 768 - a wrong assumed width baked a wrong-width id + vec0 table and the id changed after the first embed; a width drift against a bound dim now throws too.
|
|
17
|
+
- P-05: the WS upgrade bearer verification passes the client IP so the per-IP failure lockout engages for upgrade attempts (previously a lockout-free brute-force surface).
|
|
18
|
+
- Docs: the fictional "disconnect policy" section replaced with the real reconnect-and-replay behaviour; the WS ticket endpoint path corrected to `/v1/session/ws-ticket`.
|
|
19
|
+
|
|
20
|
+
- Updated dependencies []:
|
|
21
|
+
- @graphorin/protocol@0.6.0
|
|
22
|
+
|
|
23
|
+
## 0.5.0
|
|
24
|
+
|
|
25
|
+
First version published to the npm registry (with Sigstore build
|
|
26
|
+
provenance). The 0.2.0, 0.3.0, and 0.4.0 versions were internal lockstep
|
|
27
|
+
milestones and were never published. All `@graphorin/*` packages release
|
|
28
|
+
lockstep at the same version; the full release notes for 0.2.0-0.5.0 live
|
|
29
|
+
in the repository-level
|
|
30
|
+
[CHANGELOG](https://github.com/o-stepper/graphorin/blob/main/CHANGELOG.md).
|
|
31
|
+
|
|
3
32
|
## 0.1.0
|
|
4
33
|
|
|
5
34
|
### Minor 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.6.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>
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
|---|---|
|
|
19
19
|
| **WebSocket transport** | Honours the `graphorin.protocol.v1` subprotocol; supports both bearer-token (Node SDK) and ticket-flow (browser) authentication. |
|
|
20
20
|
| **SSE fallback** | Read-only `EventSource` transport for environments that block WebSocket upgrades. Control-plane operations (`subscribe`, `cancel`, `resume`) fall back to REST. |
|
|
21
|
-
| **Async-iterable subscriptions** | `for await (const event of sub.events())`
|
|
21
|
+
| **Async-iterable subscriptions** | `for await (const event of sub.events())` - typed `AgentEvent` / `WorkflowEvent` payload via `@graphorin/protocol`. |
|
|
22
22
|
| **Reconnect** | Exponential backoff with full jitter; resubscribes with the recorded `lastEventId` so the server replays buffered events. |
|
|
23
23
|
| **Bundle hygiene** | Browser-friendly. Zero Node-only dependencies; runtime depends only on `@graphorin/protocol` and `zod`. |
|
|
24
24
|
|
|
25
25
|
## Direct dependencies
|
|
26
26
|
|
|
27
|
-
- [`@graphorin/protocol`](../protocol/README.md)
|
|
28
|
-
- [`zod`](https://zod.dev) (`^3.25.0`)
|
|
27
|
+
- [`@graphorin/protocol`](../protocol/README.md) - single source of truth for the wire format.
|
|
28
|
+
- [`zod`](https://zod.dev) (`^3.25.0`) - schema validation re-exported transitively through `@graphorin/protocol`.
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
@@ -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.6.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
|
@@ -47,7 +47,7 @@ interface GraphorinClientOptions {
|
|
|
47
47
|
/**
|
|
48
48
|
* Session bound to the SSE fallback (IP-3): substituted into the
|
|
49
49
|
* `:sessionId` slot of `sseSessionPath`. Required to connect over
|
|
50
|
-
* SSE
|
|
50
|
+
* SSE - the old client sent the literal template and could never
|
|
51
51
|
* receive an event.
|
|
52
52
|
*/
|
|
53
53
|
readonly sessionId?: string;
|
|
@@ -81,7 +81,7 @@ interface GraphorinClientOptions {
|
|
|
81
81
|
* IP-19: per-RPC reply timeout in milliseconds. When set (and > 0), an RPC
|
|
82
82
|
* that receives no matching reply within this window rejects with a
|
|
83
83
|
* {@link TransportFailedError} instead of hanging forever on a
|
|
84
|
-
* non-responsive server. Default: unset
|
|
84
|
+
* non-responsive server. Default: unset - no timeout, so a legitimately
|
|
85
85
|
* slow server reply is never aborted (opt-in).
|
|
86
86
|
*/
|
|
87
87
|
readonly rpcTimeoutMs?: number;
|
|
@@ -152,10 +152,10 @@ declare class GraphorinClient {
|
|
|
152
152
|
}): Promise<unknown>;
|
|
153
153
|
/**
|
|
154
154
|
* Resume a paused (HITL) run. The WebSocket protocol intentionally
|
|
155
|
-
* does NOT carry a `resume` control message
|
|
155
|
+
* does NOT carry a `resume` control message - resumes are durable
|
|
156
156
|
* + idempotent + body-carrying, which maps onto the REST endpoint
|
|
157
157
|
* `POST /v1/runs/:runId/resume`. NOTE (IP-14): the server endpoint
|
|
158
|
-
* currently answers **501**
|
|
158
|
+
* currently answers **501** - server-side durable resume is not
|
|
159
159
|
* implemented yet. Library-mode callers resume directly:
|
|
160
160
|
* `agent.run(result.state, { directive })`.
|
|
161
161
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphorin-client.d.ts","names":[],"sources":["../src/graphorin-client.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;KAsEY,kBAAA;;;;;;;;;;;;;;;;;;;;;KAoBA,mBAAA,GAAsB;;;;;;UAOjB,sBAAA;;;;;;;;;;;;;;iBAcA;uBACM;;;;;;;;;;uBAUA;;8BAEO;;gCAEE;;0BAEN;;;;;;;;;;;;;;;;;UAkBT,oBAAA;;;mBAGE;;;;;;;;;;UAWF,YAAA;;;YAGL,cAAc;;;;iBAIT;cACH;;;;;cAmBD,eAAA;;uBAYU;;;;;;;;;aAeJ;;UAkCH;;;;;;;oBAWJ;;MAEP,QAAQ;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"graphorin-client.d.ts","names":[],"sources":["../src/graphorin-client.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;KAsEY,kBAAA;;;;;;;;;;;;;;;;;;;;;KAoBA,mBAAA,GAAsB;;;;;;UAOjB,sBAAA;;;;;;;;;;;;;;iBAcA;uBACM;;;;;;;;;;uBAUA;;8BAEO;;gCAEE;;0BAEN;;;;;;;;;;;;;;;;;UAkBT,oBAAA;;;mBAGE;;;;;;;;;;UAWF,YAAA;;;YAGL,cAAc;;;;iBAIT;cACH;;;;;cAmBD,eAAA;;uBAYU;;;;;;;;;aAeJ;;UAkCH;;;;;;;oBAWJ;;MAEP,QAAQ;;;;;;;;;;MAuER;;;;;;;;;;;;MA+BA;;;;;;;;;;gBA4DiB;;uBAwBC"}
|
package/dist/graphorin-client.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isErrorFrame, isEventFrame, isLifecycleFrame, isPongFrame, isReplayMark
|
|
|
6
6
|
|
|
7
7
|
//#region src/graphorin-client.ts
|
|
8
8
|
/**
|
|
9
|
-
* `GraphorinClient`
|
|
9
|
+
* `GraphorinClient` - ergonomic façade over the
|
|
10
10
|
* {@link Transport} contract. Handles:
|
|
11
11
|
*
|
|
12
12
|
* - WS handshake (`openWebSocketTransport`) with optional ticket flow.
|
|
@@ -78,7 +78,7 @@ var GraphorinClient = class {
|
|
|
78
78
|
if (this.#transport?.kind !== "ws") return;
|
|
79
79
|
await this.#sendRpc("initialize", { clientInfo: {
|
|
80
80
|
name: "graphorin-client",
|
|
81
|
-
version: "0.
|
|
81
|
+
version: "0.6.0"
|
|
82
82
|
} });
|
|
83
83
|
}
|
|
84
84
|
/** Send a `ping` RPC and resolve when the server replies with `pong`. */
|
|
@@ -96,8 +96,8 @@ var GraphorinClient = class {
|
|
|
96
96
|
if (this.#transport.kind === "sse") {
|
|
97
97
|
const subject$1 = subjectFor(target);
|
|
98
98
|
const boundSubject = `session:${this.#options.sessionId ?? ""}/events`;
|
|
99
|
-
if (subject$1 !== boundSubject) throw new TransportFailedError(`subscribe('${subject$1}') requires the WebSocket transport
|
|
100
|
-
if (this.#sseSubscription === void 0) {
|
|
99
|
+
if (subject$1 !== boundSubject) throw new TransportFailedError(`subscribe('${subject$1}') requires the WebSocket transport - the SSE fallback carries only the bound session stream ('${boundSubject}').`);
|
|
100
|
+
if (this.#sseSubscription === void 0 || this.#sseSubscription.metadata().closed) {
|
|
101
101
|
this.#sseSubscription = this.#createSubscription({
|
|
102
102
|
subscriptionId: "__sse__",
|
|
103
103
|
subject: subject$1,
|
|
@@ -140,10 +140,10 @@ var GraphorinClient = class {
|
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
142
|
* Resume a paused (HITL) run. The WebSocket protocol intentionally
|
|
143
|
-
* does NOT carry a `resume` control message
|
|
143
|
+
* does NOT carry a `resume` control message - resumes are durable
|
|
144
144
|
* + idempotent + body-carrying, which maps onto the REST endpoint
|
|
145
145
|
* `POST /v1/runs/:runId/resume`. NOTE (IP-14): the server endpoint
|
|
146
|
-
* currently answers **501**
|
|
146
|
+
* currently answers **501** - server-side durable resume is not
|
|
147
147
|
* implemented yet. Library-mode callers resume directly:
|
|
148
148
|
* `agent.run(result.state, { directive })`.
|
|
149
149
|
*/
|
|
@@ -214,11 +214,14 @@ var GraphorinClient = class {
|
|
|
214
214
|
}
|
|
215
215
|
async #openSse() {
|
|
216
216
|
if (this.#options.auth.kind !== "bearer") throw new TransportFailedError("SSE fallback requires the 'bearer' auth strategy. The WebSocket ticket flow does not extend to EventSource.");
|
|
217
|
+
const url = this.#sseUrl();
|
|
218
|
+
const resumeCursor = this.#sseSubscription?.__lastEventId();
|
|
217
219
|
return await openSseTransport({
|
|
218
|
-
url
|
|
220
|
+
url,
|
|
219
221
|
auth: this.#options.auth,
|
|
220
222
|
...this.#options.fetch !== void 0 ? { fetch: this.#options.fetch } : {},
|
|
221
|
-
...this.#options.clientId !== void 0 ? { clientId: this.#options.clientId } : {}
|
|
223
|
+
...this.#options.clientId !== void 0 ? { clientId: this.#options.clientId } : {},
|
|
224
|
+
...resumeCursor !== void 0 ? { lastEventId: resumeCursor } : {}
|
|
222
225
|
}, this.#listeners("sse"));
|
|
223
226
|
}
|
|
224
227
|
#wsUrl() {
|
|
@@ -229,7 +232,7 @@ var GraphorinClient = class {
|
|
|
229
232
|
const template = this.#options.sseSessionPath ?? "/v1/sessions/:sessionId/events";
|
|
230
233
|
if (template.includes(":sessionId")) {
|
|
231
234
|
const sessionId = this.#options.sessionId;
|
|
232
|
-
if (sessionId === void 0 || sessionId.length === 0) throw new TransportFailedError("The SSE fallback needs a session binding
|
|
235
|
+
if (sessionId === void 0 || sessionId.length === 0) throw new TransportFailedError("The SSE fallback needs a session binding - pass `sessionId` in the client options (it fills ':sessionId' in sseSessionPath).");
|
|
233
236
|
return joinUrl(this.#options.baseUrl, template.replace(":sessionId", encodeURIComponent(sessionId)), { ws: false });
|
|
234
237
|
}
|
|
235
238
|
return joinUrl(this.#options.baseUrl, template, { ws: false });
|
|
@@ -333,7 +336,9 @@ var GraphorinClient = class {
|
|
|
333
336
|
try {
|
|
334
337
|
await sleep(delay, this.#abortController.signal);
|
|
335
338
|
await this.connect();
|
|
336
|
-
|
|
339
|
+
const transport = this.#transport;
|
|
340
|
+
if (transport === void 0) continue;
|
|
341
|
+
if (transport.kind === "sse") return;
|
|
337
342
|
for (const [oldId, sub] of [...this.#subscriptions]) {
|
|
338
343
|
this.#subscriptions.delete(oldId);
|
|
339
344
|
try {
|
|
@@ -524,7 +529,7 @@ function subjectFor(target) {
|
|
|
524
529
|
assertNonEmpty(target.sessionId, "run.sessionId");
|
|
525
530
|
return `session:${target.sessionId}/runs/${target.runId}/events`;
|
|
526
531
|
}
|
|
527
|
-
|
|
532
|
+
throw new TypeError("subscribe({ target: 'run' }) requires `sessionId` - the server subject grammar has no bare run form. Pass { kind: 'run', runId, sessionId } or use the 'agent' target with the agent id.");
|
|
528
533
|
case "workflow":
|
|
529
534
|
assertNonEmpty(target.id, "workflow.id");
|
|
530
535
|
return `workflow:${target.id}/events`;
|
|
@@ -553,7 +558,7 @@ function buildRpcFrame(method, id, params) {
|
|
|
553
558
|
method: "initialize",
|
|
554
559
|
params: params ?? { clientInfo: {
|
|
555
560
|
name: "graphorin-client",
|
|
556
|
-
version: "0.
|
|
561
|
+
version: "0.6.0"
|
|
557
562
|
} }
|
|
558
563
|
};
|
|
559
564
|
case "subscription.subscribe": return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphorin-client.js","names":["#options","#pending","#subscriptions","#closed","#transport","#connectingPromise","#openWs","#initializeRpc","#openSse","#sendRpc","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":["/**\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: '0.5.0' },\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 if (this.#sseSubscription === undefined) {\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 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 },\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 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 return `run:${target.runId}/events`;\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 ?? { clientInfo: { name: 'graphorin-client', version: '0.5.0' } }) 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","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":["/**\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: '0.6.0' },\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 ?? { clientInfo: { name: 'graphorin-client', version: '0.6.0' } }) 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/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { GraphorinClient, GraphorinClientOptions, Subscription, SubscriptionMeta
|
|
|
9
9
|
//#region src/index.d.ts
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* `@graphorin/client`
|
|
12
|
+
* `@graphorin/client` - reference TypeScript client for the
|
|
13
13
|
* Graphorin standalone server.
|
|
14
14
|
*
|
|
15
15
|
* Wraps the WebSocket subprotocol `graphorin.protocol.v1` (with an
|
|
@@ -28,7 +28,7 @@ import { GraphorinClient, GraphorinClientOptions, Subscription, SubscriptionMeta
|
|
|
28
28
|
* @packageDocumentation
|
|
29
29
|
*/
|
|
30
30
|
/** Canonical version constant. Mirrors the `package.json` version. */
|
|
31
|
-
declare const VERSION = "0.
|
|
31
|
+
declare const VERSION = "0.6.0";
|
|
32
32
|
//#endregion
|
|
33
33
|
export { AuthFailedError, type BackoffPolicy, ClientAbortedError, ClientNotConnectedError, GraphorinClient, GraphorinClientError, GraphorinClientErrorKind, type GraphorinClientOptions, InvalidServerFrameError, ProtocolViolationError, SubprotocolMismatchError, type Subscription, type SubscriptionMetadata, SubscriptionNotFoundError, type SubscriptionTarget, type Transport, type TransportAuth, type TransportCloseReason, TransportFailedError, type TransportKind, type TransportListeners, type TransportOptions, type TransportPreference, VERSION, computeBackoffMs, kindForRpcCode, openSseTransport, openWebSocketTransport, sleep };
|
|
34
34
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { GraphorinClient } from "./graphorin-client.js";
|
|
|
6
6
|
|
|
7
7
|
//#region src/index.ts
|
|
8
8
|
/**
|
|
9
|
-
* `@graphorin/client`
|
|
9
|
+
* `@graphorin/client` - reference TypeScript client for the
|
|
10
10
|
* Graphorin standalone server.
|
|
11
11
|
*
|
|
12
12
|
* Wraps the WebSocket subprotocol `graphorin.protocol.v1` (with an
|
|
@@ -25,7 +25,7 @@ import { GraphorinClient } from "./graphorin-client.js";
|
|
|
25
25
|
* @packageDocumentation
|
|
26
26
|
*/
|
|
27
27
|
/** Canonical version constant. Mirrors the `package.json` version. */
|
|
28
|
-
const VERSION = "0.
|
|
28
|
+
const VERSION = "0.6.0";
|
|
29
29
|
|
|
30
30
|
//#endregion
|
|
31
31
|
export { AuthFailedError, ClientAbortedError, ClientNotConnectedError, GraphorinClient, GraphorinClientError, InvalidServerFrameError, ProtocolViolationError, SubprotocolMismatchError, SubscriptionNotFoundError, TransportFailedError, VERSION, computeBackoffMs, kindForRpcCode, openSseTransport, openWebSocketTransport, sleep };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@graphorin/client`
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@graphorin/client` - reference TypeScript client for the\n * Graphorin standalone server.\n *\n * Wraps the WebSocket subprotocol `graphorin.protocol.v1` (with an\n * optional Server-Sent Events fallback for proxy-restricted\n * environments) behind an ergonomic {@link GraphorinClient} class:\n * `connect()`, `subscribe({ target, id })` returning an\n * async-iterable subscription, `cancel(runId, opts)`,\n * `disconnect()`. Handles the browser ticket flow, exponential-\n * backoff reconnect with `lastEventId` resume against the server\n * replay buffer, and Zod-validated frame parsing on both\n * directions.\n *\n * Browser-friendly: zero Node-only dependencies; the only runtime\n * dependencies are `@graphorin/protocol` and `zod`.\n *\n * @packageDocumentation\n */\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.6.0';\n\nexport * from './errors.js';\nexport {\n GraphorinClient,\n type GraphorinClientOptions,\n type Subscription,\n type SubscriptionMetadata,\n type SubscriptionTarget,\n type TransportPreference,\n} from './graphorin-client.js';\nexport {\n type BackoffPolicy,\n computeBackoffMs,\n sleep,\n} from './reconnect.js';\nexport {\n openSseTransport,\n openWebSocketTransport,\n type Transport,\n type TransportAuth,\n type TransportCloseReason,\n type TransportKind,\n type TransportListeners,\n type TransportOptions,\n} from './transport/index.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,UAAU"}
|
package/dist/reconnect.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Pure-functional reconnect-backoff helper. Encapsulated in its own
|
|
4
4
|
* module so the {@link GraphorinClient} stays free of timing
|
|
5
|
-
* heuristics
|
|
5
|
+
* heuristics - and so tests can drive the policy with a deterministic
|
|
6
6
|
* RNG.
|
|
7
7
|
*
|
|
8
8
|
* Algorithm: exponential backoff with full-jitter
|
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 {@link GraphorinClient} stays free of timing\n * heuristics
|
|
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 {@link 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 {@link DOMException}-style abort error) when the\n * supplied {@link 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"}
|
package/dist/transport/sse.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ServerMessageSchema } from "@graphorin/protocol";
|
|
|
3
3
|
|
|
4
4
|
//#region src/transport/sse.ts
|
|
5
5
|
/**
|
|
6
|
-
* Server-Sent-Events transport
|
|
6
|
+
* Server-Sent-Events transport - the read-only fallback for clients
|
|
7
7
|
* stuck behind proxies that block WebSocket upgrades. The transport
|
|
8
8
|
* is unidirectional (server → client only); control-plane operations
|
|
9
9
|
* (subscribe / abort / resume) must go through REST endpoints
|
|
@@ -11,10 +11,10 @@ import { ServerMessageSchema } from "@graphorin/protocol";
|
|
|
11
11
|
*
|
|
12
12
|
* IP-3: implemented on **fetch streaming**, not `EventSource`. The
|
|
13
13
|
* server writes *named* events (`event: <frame.type>`), which an
|
|
14
|
-
* `EventSource` delivers only to listeners registered per type
|
|
14
|
+
* `EventSource` delivers only to listeners registered per type - an
|
|
15
15
|
* unbounded catalogue the old `message`-only listener never saw.
|
|
16
16
|
* Every frame's full JSON rides in `data:`, so a direct SSE parse
|
|
17
|
-
* that ignores the event name delivers everything
|
|
17
|
+
* that ignores the event name delivers everything - and `fetch`
|
|
18
18
|
* sends the `Authorization` header natively (no polyfill seam).
|
|
19
19
|
*
|
|
20
20
|
* @packageDocumentation
|
|
@@ -37,7 +37,8 @@ async function openSseTransport(options, listeners) {
|
|
|
37
37
|
method: "GET",
|
|
38
38
|
headers: {
|
|
39
39
|
Accept: "text/event-stream",
|
|
40
|
-
Authorization: `Bearer ${options.auth.token}
|
|
40
|
+
Authorization: `Bearer ${options.auth.token}`,
|
|
41
|
+
...options.lastEventId !== void 0 && options.lastEventId.length > 0 ? { "Last-Event-ID": options.lastEventId } : {}
|
|
41
42
|
},
|
|
42
43
|
signal: controller.signal
|
|
43
44
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","names":["resp: Response","lastEventId: string | undefined","dataLines: string[]","pendingId: string | undefined","payload: unknown"],"sources":["../../src/transport/sse.ts"],"sourcesContent":["/**\n * Server-Sent-Events transport
|
|
1
|
+
{"version":3,"file":"sse.js","names":["resp: Response","lastEventId: string | undefined","dataLines: string[]","pendingId: string | undefined","payload: unknown"],"sources":["../../src/transport/sse.ts"],"sourcesContent":["/**\n * Server-Sent-Events transport - the read-only fallback for clients\n * stuck behind proxies that block WebSocket upgrades. The transport\n * is unidirectional (server → client only); control-plane operations\n * (subscribe / abort / resume) must go through REST endpoints\n * exposed by `@graphorin/server`.\n *\n * IP-3: implemented on **fetch streaming**, not `EventSource`. The\n * server writes *named* events (`event: <frame.type>`), which an\n * `EventSource` delivers only to listeners registered per type - an\n * unbounded catalogue the old `message`-only listener never saw.\n * Every frame's full JSON rides in `data:`, so a direct SSE parse\n * that ignores the event name delivers everything - and `fetch`\n * sends the `Authorization` header natively (no polyfill seam).\n *\n * @packageDocumentation\n */\n\nimport { ServerMessageSchema } from '@graphorin/protocol';\n\nimport { InvalidServerFrameError, TransportFailedError } from '../errors.js';\nimport type { Transport, TransportListeners, TransportOptions } from './types.js';\n\n/**\n * Open an SSE transport. Resolves once the server answers with a\n * streaming response; rejects with a typed\n * {@link TransportFailedError} on construction / connection failure.\n *\n * @stable\n */\nexport async function openSseTransport(\n options: TransportOptions,\n listeners: TransportListeners,\n): Promise<Transport> {\n if (options.auth.kind !== 'bearer') {\n throw new TransportFailedError(\n `SSE transport supports only the 'bearer' auth strategy; got '${options.auth.kind}'.`,\n );\n }\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new TransportFailedError('No fetch implementation available for the SSE transport.');\n }\n const controller = new AbortController();\n let resp: Response;\n try {\n resp = await fetchImpl(options.url, {\n method: 'GET',\n headers: {\n Accept: 'text/event-stream',\n Authorization: `Bearer ${options.auth.token}`,\n // periphery-03: resume from the replay buffer instead of\n // re-receiving the entire buffered history on every reconnect.\n ...(options.lastEventId !== undefined && options.lastEventId.length > 0\n ? { 'Last-Event-ID': options.lastEventId }\n : {}),\n },\n signal: controller.signal,\n });\n } catch (cause) {\n throw new TransportFailedError(\n `SSE connection to '${options.url}' failed.`,\n cause instanceof Error ? { cause } : {},\n );\n }\n if (!resp.ok || resp.body === null) {\n throw new TransportFailedError(\n `SSE endpoint '${options.url}' answered ${resp.status}${resp.body === null ? ' with no body' : ''}.`,\n );\n }\n\n let lastEventId: string | undefined;\n let closeFired = false;\n const close = (reason: { code: number; reason: string; wasClean: boolean }): void => {\n if (closeFired) return;\n closeFired = true;\n listeners.onClose(reason);\n };\n\n // Consume the stream in the background: split on newlines, parse\n // `id:` / `data:` fields, dispatch on blank lines. The `event:`\n // name is intentionally ignored - the full frame JSON is in `data:`.\n void (async () => {\n const decoder = new TextDecoder();\n let buffer = '';\n let dataLines: string[] = [];\n let pendingId: string | undefined;\n\n const dispatch = (): void => {\n if (dataLines.length === 0) {\n pendingId = undefined;\n return;\n }\n const raw = dataLines.join('\\n');\n dataLines = [];\n const id = pendingId;\n pendingId = undefined;\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n // Keep-alive / non-JSON data is not a frame.\n return;\n }\n const parsed = ServerMessageSchema.safeParse(payload);\n if (!parsed.success) {\n listeners.onError(\n new InvalidServerFrameError(\n 'Server SSE frame failed schema validation.',\n parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n ),\n );\n return;\n }\n const frame = parsed.data;\n const eventId = (frame as { eventId?: string }).eventId ?? id;\n if (typeof eventId === 'string' && eventId.length > 0) lastEventId = eventId;\n listeners.onFrame(frame);\n };\n\n try {\n for await (const chunk of resp.body as unknown as AsyncIterable<Uint8Array>) {\n buffer += decoder.decode(chunk, { stream: true });\n for (;;) {\n const nl = buffer.indexOf('\\n');\n if (nl === -1) break;\n const line = buffer.slice(0, nl).replace(/\\r$/, '');\n buffer = buffer.slice(nl + 1);\n if (line.length === 0) {\n dispatch();\n continue;\n }\n if (line.startsWith(':')) continue; // comment / keep-alive\n if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).replace(/^ /, ''));\n } else if (line.startsWith('id:')) {\n pendingId = line.slice(3).trim();\n }\n // `event:` / `retry:` fields are intentionally ignored.\n }\n }\n close({ code: 1000, reason: 'stream ended', wasClean: true });\n } catch (err) {\n if (controller.signal.aborted) {\n close({ code: 1000, reason: 'closed by client', wasClean: true });\n return;\n }\n close({\n code: 1006,\n reason: err instanceof Error ? err.message : String(err),\n wasClean: false,\n });\n }\n })();\n\n listeners.onOpen();\n const transport: Transport = {\n kind: 'sse',\n url: options.url,\n send: () => {\n throw new TransportFailedError(\n 'The SSE transport is read-only; control-plane calls go through REST.',\n );\n },\n close: () => {\n controller.abort();\n close({ code: 1000, reason: 'closed by client', wasClean: true });\n },\n get lastEventId(): string | undefined {\n return lastEventId;\n },\n };\n return transport;\n}\n"],"mappings":"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"}
|
|
@@ -21,6 +21,14 @@ interface TransportOptions {
|
|
|
21
21
|
readonly EventSource?: typeof EventSource;
|
|
22
22
|
/** Per-connection identifier surfaced on diagnostics + reconnects. */
|
|
23
23
|
readonly clientId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Resume cursor sent as the `Last-Event-ID` header on (re)connect
|
|
26
|
+
* (periphery-03). The server replays only events AFTER it from the
|
|
27
|
+
* buffer - without it every SSE reconnect replays the entire
|
|
28
|
+
* buffered history. Consumed by the SSE transport; ignored by WS
|
|
29
|
+
* (whose resubscribe carries the cursor in the RPC).
|
|
30
|
+
*/
|
|
31
|
+
readonly lastEventId?: string;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* Authentication strategy passed to a transport. The bearer-token
|
|
@@ -80,7 +88,7 @@ interface Transport {
|
|
|
80
88
|
* Send a client → server frame. Throws when the transport is not
|
|
81
89
|
* in the open state, or when the underlying back-end does not
|
|
82
90
|
* support send (the SSE transport throws every send via
|
|
83
|
-
* {@link import('../errors.js').TransportFailedError}
|
|
91
|
+
* {@link import('../errors.js').TransportFailedError} - clients
|
|
84
92
|
* should fall back to REST for control-plane operations on SSE).
|
|
85
93
|
*/
|
|
86
94
|
send(frame: ClientMessage): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/transport/types.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/transport/types.ts"],"sourcesContent":[],"mappings":";;;;;AAoDA;AAWA;;;AAIkB,KAnDN,aAAA,GAmDM,IAAA,GAAA,KAAA;;AASlB;AAeA;;;UApEiB,gBAAA;;iBAEA;0BACS;8BACI;gCACE;;;;;;;;;;;;;;;;;;;;;;;KAwBpB,aAAA;;;;;iCAEkD;;;;;;;;;UAS7C,kBAAA;;iBAEA;iBACA;kBACC;;;;;;;;UASD,oBAAA;;;;;;;;;;;;;;UAeA,SAAA;iBACA;;;;;;;;;cASH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphorin/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Oleksiy Stepurenko",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"zod": "^3.25.0",
|
|
67
|
-
"@graphorin/protocol": "0.
|
|
67
|
+
"@graphorin/protocol": "0.6.0"
|
|
68
68
|
},
|
|
69
69
|
"publishConfig": {
|
|
70
70
|
"access": "public",
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@types/ws": "^8.18.1",
|
|
75
75
|
"ws": "^8.20.1",
|
|
76
|
-
"@graphorin/security": "0.
|
|
77
|
-
"@graphorin/server": "0.
|
|
78
|
-
"@graphorin/store-sqlite": "0.
|
|
76
|
+
"@graphorin/security": "0.6.0",
|
|
77
|
+
"@graphorin/server": "0.6.0",
|
|
78
|
+
"@graphorin/store-sqlite": "0.6.0"
|
|
79
79
|
},
|
|
80
80
|
"scripts": {
|
|
81
81
|
"build": "tsdown",
|