iterate 0.3.0 → 0.4.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/README.md +164 -82
- package/dist/{next/api.d.ts → api.d.ts} +197 -33
- package/dist/{next/app-server.d.ts → app-server.d.ts} +7 -0
- package/dist/{next/app-server.mjs → app-server.mjs} +17 -15
- package/dist/app-server.mjs.map +1 -0
- package/dist/{next/app-session.mjs → app-session.mjs} +12 -15
- package/dist/app-session.mjs.map +1 -0
- package/dist/{next/app.mjs → app.mjs} +53 -14
- package/dist/app.mjs.map +1 -0
- package/dist/{next/client → client}/live-state.d.ts +11 -11
- package/dist/client/oauth.d.ts +17 -0
- package/dist/{next/client → client}/react.d.ts +10 -42
- package/dist/{next/client → client}/socket.d.ts +1 -0
- package/dist/client.mjs +156 -4
- package/dist/client.mjs.map +1 -0
- package/dist/{next/expression.d.ts → expression.d.ts} +11 -69
- package/dist/{next/expression.mjs → expression.mjs} +11 -109
- package/dist/expression.mjs.map +1 -0
- package/dist/lib-BWr-5mFO.mjs +36 -0
- package/dist/lib-BWr-5mFO.mjs.map +1 -0
- package/dist/{next/lib.d.ts → lib.d.ts} +16 -2
- package/dist/{next/lib.mjs → lib.mjs} +32 -3
- package/dist/lib.mjs.map +1 -0
- package/dist/node.d.ts +15 -3
- package/dist/node.mjs +36 -174
- package/dist/node.mjs.map +1 -1
- package/dist/{next/oauth-scopes.mjs → oauth-scopes.mjs} +1 -1
- package/dist/oauth-scopes.mjs.map +1 -0
- package/dist/{next/oauth.mjs → oauth.mjs} +14 -2
- package/dist/oauth.mjs.map +1 -0
- package/dist/principal.d.ts +8 -0
- package/dist/principal.mjs +8 -0
- package/dist/principal.mjs.map +1 -0
- package/dist/project-ingress.d.ts +58 -0
- package/dist/project-ingress.mjs +104 -0
- package/dist/project-ingress.mjs.map +1 -0
- package/dist/{next/react.mjs → react.mjs} +12 -12
- package/dist/react.mjs.map +1 -0
- package/dist/sdk/auth.d.ts +25 -0
- package/dist/sdk/index.d.ts +155 -0
- package/dist/sdk/record-pipelined-steps.d.ts +19 -0
- package/dist/sdk.mjs +245 -2
- package/dist/sdk.mjs.map +1 -0
- package/dist/{next/stream → stream}/processor.d.ts +28 -23
- package/dist/{next/stream → stream}/processor.mjs +48 -25
- package/dist/stream/processor.mjs.map +1 -0
- package/dist/{next/stream → stream}/run.d.ts +9 -6
- package/dist/{next/stream → stream}/run.mjs +13 -8
- package/dist/stream/run.mjs.map +1 -0
- package/dist/stream/test-support.d.ts +45 -0
- package/dist/stream/test-support.mjs +196 -0
- package/dist/stream/test-support.mjs.map +1 -0
- package/package.json +65 -219
- package/THIRD_PARTY_NOTICES.md +0 -55
- package/bin/iterate.js +0 -94
- package/dist/api-url-B6404M82.mjs +0 -17
- package/dist/api-url-B6404M82.mjs.map +0 -1
- package/dist/app-ref-BipL0feU.mjs +0 -35
- package/dist/app-ref-BipL0feU.mjs.map +0 -1
- package/dist/app-ref-C1CrgXqX.mjs +0 -7
- package/dist/app-ref-C1CrgXqX.mjs.map +0 -1
- package/dist/app-ref-DYai_om1.mjs +0 -7
- package/dist/app-ref-DYai_om1.mjs.map +0 -1
- package/dist/cli-D0c-pDL_.mjs +0 -1010
- package/dist/cli-D0c-pDL_.mjs.map +0 -1
- package/dist/client.d.ts +0 -3
- package/dist/cloudflare-BTm90gQ4.mjs +0 -951
- package/dist/cloudflare-BTm90gQ4.mjs.map +0 -1
- package/dist/contract-s4FW4eES.mjs +0 -309
- package/dist/contract-s4FW4eES.mjs.map +0 -1
- package/dist/document-review/index.d.ts +0 -5
- package/dist/document-review/types.d.ts +0 -107
- package/dist/document-review.mjs +0 -7015
- package/dist/document-review.mjs.map +0 -1
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs +0 -205
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +0 -1
- package/dist/idempotency-DleloJNt.mjs +0 -28
- package/dist/idempotency-DleloJNt.mjs.map +0 -1
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -8
- package/dist/index.mjs.map +0 -1
- package/dist/itx/api-url.d.ts +0 -6
- package/dist/itx/itx-node-client.d.ts +0 -65
- package/dist/itx/itx-session.d.ts +0 -215
- package/dist/itx/owned-rpc-session.d.ts +0 -14
- package/dist/itx/query-client.d.ts +0 -10
- package/dist/itx-api.generated.d.ts +0 -6195
- package/dist/itx-session-sjud8GiT.mjs +0 -534
- package/dist/itx-session-sjud8GiT.mjs.map +0 -1
- package/dist/live-state-BJNqOwFw.mjs +0 -299
- package/dist/live-state-BJNqOwFw.mjs.map +0 -1
- package/dist/next/app-server.mjs.map +0 -1
- package/dist/next/app-session.mjs.map +0 -1
- package/dist/next/app.mjs.map +0 -1
- package/dist/next/client/oauth.d.ts +0 -12
- package/dist/next/client.mjs +0 -156
- package/dist/next/client.mjs.map +0 -1
- package/dist/next/expression.mjs.map +0 -1
- package/dist/next/lib.mjs.map +0 -1
- package/dist/next/oauth-scopes.mjs.map +0 -1
- package/dist/next/oauth.mjs.map +0 -1
- package/dist/next/principal.d.ts +0 -64
- package/dist/next/principal.mjs +0 -98
- package/dist/next/principal.mjs.map +0 -1
- package/dist/next/project-ingress.d.ts +0 -37
- package/dist/next/project-ingress.mjs +0 -75
- package/dist/next/project-ingress.mjs.map +0 -1
- package/dist/next/react.mjs.map +0 -1
- package/dist/next/sdk/auth.d.ts +0 -5
- package/dist/next/sdk/index.d.ts +0 -112
- package/dist/next/sdk.mjs +0 -139
- package/dist/next/sdk.mjs.map +0 -1
- package/dist/next/stream/processor.mjs.map +0 -1
- package/dist/next/stream/run.mjs.map +0 -1
- package/dist/next-node.d.ts +0 -15
- package/dist/next-node.mjs +0 -51
- package/dist/next-node.mjs.map +0 -1
- package/dist/processor-host-capabilities-BMFH3KTM.mjs +0 -56
- package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +0 -1
- package/dist/processors/cloudflare.d.ts +0 -3
- package/dist/processors/durable-object-processor-durability.d.ts +0 -79
- package/dist/processors/event-consumption-metrics.d.ts +0 -82
- package/dist/processors/idempotency.d.ts +0 -13
- package/dist/processors/index.d.ts +0 -12
- package/dist/processors/processor-contracts.d.ts +0 -342
- package/dist/processors/processor-facet.d.ts +0 -186
- package/dist/processors/processor-host-capabilities.d.ts +0 -60
- package/dist/processors/prompt-sections.d.ts +0 -17
- package/dist/processors/rpc-types.d.ts +0 -515
- package/dist/processors/schemas.d.ts +0 -102
- package/dist/processors/stream-handle.d.ts +0 -45
- package/dist/processors/stream-processor-keepalive.d.ts +0 -95
- package/dist/processors/stream-processor-registry.d.ts +0 -233
- package/dist/processors/stream-processor-runner.d.ts +0 -289
- package/dist/processors/stream-processor.d.ts +0 -339
- package/dist/processors/stream-runtime-metrics.d.ts +0 -107
- package/dist/processors/testing.d.ts +0 -302
- package/dist/processors-BoNyeBfQ.mjs +0 -10
- package/dist/processors-BoNyeBfQ.mjs.map +0 -1
- package/dist/processors-cloudflare.mjs +0 -3
- package/dist/processors-testing.mjs +0 -435
- package/dist/processors-testing.mjs.map +0 -1
- package/dist/processors.mjs +0 -52
- package/dist/processors.mjs.map +0 -1
- package/dist/protocol-DnK_f2m6.mjs +0 -251
- package/dist/protocol-DnK_f2m6.mjs.map +0 -1
- package/dist/sdk/capnweb/index.d.ts +0 -2
- package/dist/sdk/capnweb/live-state/compact.d.ts +0 -5
- package/dist/sdk/capnweb/live-state/diff.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/engine.d.ts +0 -44
- package/dist/sdk/capnweb/live-state/index.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/protocol.d.ts +0 -87
- package/dist/sdk/capnweb/live-state/retain.d.ts +0 -23
- package/dist/sdk/capnweb/live-state/store.d.ts +0 -20
- package/dist/sdk/capnweb/live-state/types.d.ts +0 -11
- package/dist/sdk/capnweb/react.d.ts +0 -45
- package/dist/sdk/capnweb/react.mjs +0 -316
- package/dist/sdk/capnweb/react.mjs.map +0 -1
- package/dist/sdk/capnweb.mjs +0 -4
- package/dist/sdk/itx/react.d.ts +0 -191
- package/dist/sdk/itx/react.mjs +0 -383
- package/dist/sdk/itx/react.mjs.map +0 -1
- package/dist/sdk-DMB-IM11.mjs +0 -933
- package/dist/sdk-DMB-IM11.mjs.map +0 -1
- package/dist/sdk.d.ts +0 -339
- package/dist/serve-itx.d.ts +0 -46
- package/dist/starter-apps/flake-dashboard/app-ref.d.ts +0 -31
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs +0 -1055
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/contract.d.ts +0 -4839
- package/dist/starter-apps/flake-dashboard/contract.mjs +0 -2
- package/dist/starter-apps/flake-dashboard/index.d.ts +0 -17
- package/dist/starter-apps/flake-dashboard/index.mjs +0 -56
- package/dist/starter-apps/flake-dashboard/index.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/worker.d.ts +0 -4607
- package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +0 -8914
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs +0 -17987
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/contract.d.ts +0 -9193
- package/dist/starter-apps/github-ai-linter/index.d.ts +0 -10
- package/dist/starter-apps/github-ai-linter/index.mjs +0 -36
- package/dist/starter-apps/github-ai-linter/index.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/prompt.d.ts +0 -13
- package/dist/starter-apps/github-ai-linter/review-bot.d.ts +0 -808
- package/dist/starter-apps/github-ai-linter/rules.d.ts +0 -34
- package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.mjs +0 -947
- package/dist/starter-apps/github-ai-linter/worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/app-ref.d.ts +0 -27
- package/dist/starter-apps/guestbook/client.d.ts +0 -7
- package/dist/starter-apps/guestbook/client.mjs +0 -59
- package/dist/starter-apps/guestbook/configured-worker.mjs +0 -205
- package/dist/starter-apps/guestbook/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/index.d.ts +0 -9
- package/dist/starter-apps/guestbook/index.mjs +0 -31
- package/dist/starter-apps/guestbook/index.mjs.map +0 -1
- package/dist/starter-apps/guestbook/processor.d.ts +0 -2267
- package/dist/starter-apps/guestbook/worker.d.ts +0 -26
- package/dist/starter-apps/guestbook/worker.mjs +0 -191
- package/dist/starter-apps/guestbook/worker.mjs.map +0 -1
- package/dist/starter-apps/media/configured-worker.mjs +0 -577
- package/dist/starter-apps/media/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/media/index.mjs +0 -36
- package/dist/starter-apps/media/index.mjs.map +0 -1
- package/dist/starter-apps/media/ref.mjs +0 -20
- package/dist/starter-apps/media/ref.mjs.map +0 -1
- package/dist/starter-apps/media/worker.mjs +0 -579
- package/dist/starter-apps/media/worker.mjs.map +0 -1
- package/dist/starter-apps/notes/configured-worker.mjs +0 -6134
- package/dist/starter-apps/notes/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/notes/index.mjs +0 -23
- package/dist/starter-apps/notes/index.mjs.map +0 -1
- package/dist/starter-apps/notes/ref.mjs +0 -21
- package/dist/starter-apps/notes/ref.mjs.map +0 -1
- package/dist/starter-apps/notes/worker.mjs +0 -427
- package/dist/starter-apps/notes/worker.mjs.map +0 -1
- package/dist/starter-apps/todo/client.mjs +0 -59
- package/dist/starter-apps/todo/configured-worker.mjs +0 -2864
- package/dist/starter-apps/todo/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/todo/index.d.ts +0 -8
- package/dist/starter-apps/todo/index.mjs +0 -29
- package/dist/starter-apps/todo/index.mjs.map +0 -1
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs +0 -2082
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +0 -1
- package/dist/usingCtx-mZx5nsAW.mjs +0 -11800
- package/dist/usingCtx-mZx5nsAW.mjs.map +0 -1
- package/dist/worker-ref-DZxPDmb_.mjs +0 -390
- package/dist/worker-ref-DZxPDmb_.mjs.map +0 -1
- package/dist/worker.d.mts +0 -33
- package/dist/worker.mjs +0 -18
- package/dist/worker.mjs.map +0 -1
- package/menubar/Iterate.entitlements +0 -12
- package/menubar/Iterate.swift +0 -914
- package/menubar/IterateIcon.swift +0 -145
- package/menubar/README.md +0 -28
- package/menubar/build-menubar-app.sh +0 -59
- /package/dist/{next/api.mjs → api.mjs} +0 -0
- /package/dist/{next/app-session.d.ts → app-session.d.ts} +0 -0
- /package/dist/{next/app.d.ts → app.d.ts} +0 -0
- /package/dist/{next/oauth-scopes.d.ts → oauth-scopes.d.ts} +0 -0
|
@@ -1,534 +0,0 @@
|
|
|
1
|
-
import { t as apiWebSocketUrl } from "./api-url-B6404M82.mjs";
|
|
2
|
-
import { newWebSocketRpcSession } from "@iterate-com/capnweb";
|
|
3
|
-
//#region src/itx/itx-session.ts
|
|
4
|
-
/**
|
|
5
|
-
* itx-session — the framework-free half of the itx client: ONE WebSocket per
|
|
6
|
-
* process (browser tab, TUI, phone), connected to an OS deployment's `/api`,
|
|
7
|
-
* `authenticate()`d into a **Session** (the catalog that vends project itxs via
|
|
8
|
-
* `session.projects.get(slug)`), and kept alive through transport gaps.
|
|
9
|
-
*
|
|
10
|
-
* React never appears in this module. The hooks in ../sdk/itx/react.ts are a thin
|
|
11
|
-
* binding over the exact surface exported here (`subscribeSession` +
|
|
12
|
-
* `currentSnapshot` feed `useSyncExternalStore`; everything else is shared
|
|
13
|
-
* verbatim), so a non-React consumer — a node script, a future runtime —
|
|
14
|
-
* gets the same one-socket semantics by importing `iterate/client`.
|
|
15
|
-
*
|
|
16
|
-
* WHERE THE CONNECTION TARGET COMES FROM
|
|
17
|
-
* • In a browser, nothing to configure: the keeper connects
|
|
18
|
-
* `window.location`'s `/api` and authenticates with the session cookie
|
|
19
|
-
* riding the WebSocket handshake.
|
|
20
|
-
* • Anywhere else (the chat TUI, tests, scripts that want the KEEPER rather
|
|
21
|
-
* than the one-shot node dial), call {@link configureIterateSession} with a
|
|
22
|
-
* base URL and credentials. Calling it again for the same deployment
|
|
23
|
-
* refreshes the credential source without disturbing the socket; changing
|
|
24
|
-
* deployments deliberately replaces the socket. The runtime's global
|
|
25
|
-
* WebSocket carries the dial — node ≥ 22, bun, and React Native all satisfy
|
|
26
|
-
* capnweb's WebSocket needs.
|
|
27
|
-
*
|
|
28
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
29
|
-
* THE SESSION MODEL — one socket, generations, invisible reconnect
|
|
30
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
31
|
-
*
|
|
32
|
-
* • ONE WebSocket for the whole process, kept in module state (so in a
|
|
33
|
-
* browser it persists across client-side navigation). One connection attempt is a
|
|
34
|
-
* GENERATION ({@link Generation}): its WebSocket and its connecting
|
|
35
|
-
* promise. The session is the AWAITED `authenticate()` result — one settled
|
|
36
|
-
* stub identity shared by the snapshot, imperative awaiters, and the
|
|
37
|
-
* project-stub cache (resolving with the raw pipelined RpcPromise would
|
|
38
|
-
* fork identities: native promises assimilate thenables). The connection timeout
|
|
39
|
-
* spans the whole handshake (TCP/TLS/upgrade AND authenticate), and a REAL
|
|
40
|
-
* auth rejection over a working socket is terminal — it surfaces from the
|
|
41
|
-
* connecting promise instead of looping.
|
|
42
|
-
*
|
|
43
|
-
* • RECONNECT IS INVISIBLE. Readers see an immutable {@link Snapshot};
|
|
44
|
-
* `snapshot.session` holds the LAST live session and is kept across a
|
|
45
|
-
* transport gap. Before the FIRST session, awaiters share the STABLE
|
|
46
|
-
* {@link firstConnect} promise, which survives failed connection attempts (paced
|
|
47
|
-
* reconnects happen behind it; an individual attempt's rejection never reaches a suspended
|
|
48
|
-
* React tree) and rejects only on a TERMINAL failure. Kept-across-the-gap
|
|
49
|
-
* applies to TRANSPORT gaps only: a terminal auth rejection on a reconnect is
|
|
50
|
-
* an AUTHORITY loss — the halted snapshot drops the (already dead) session
|
|
51
|
-
* so the real error surfaces instead of zombie stubs.
|
|
52
|
-
*
|
|
53
|
-
* • PROJECT STUBS ARE SESSION-OWNED. `session.projects.get` allocates a
|
|
54
|
-
* capnweb import-table entry, so deriving stubs ad hoc (or inside React
|
|
55
|
-
* renders that may be discarded) would leak them. The module
|
|
56
|
-
* {@link projectStubCaches} WeakMap keyed by the session stub caches one
|
|
57
|
-
* real stub per (session, slug); a retired generation disposes them. The
|
|
58
|
-
* stub stays the REAL capnweb stub (a lazy wrapper that awaited the session
|
|
59
|
-
* per call would break pipelining — capnweb fork v0.8.0), and its identity
|
|
60
|
-
* changes exactly once per successful reconnect.
|
|
61
|
-
*
|
|
62
|
-
* • TRANSPORT HEALTH IS SOCKET-OWNED and GENERATION-GUARDED. A half-open
|
|
63
|
-
* socket (laptop sleep, network switch — no `close` event) is detected by
|
|
64
|
-
* one verifier per generation ({@link verifyTransport}: periodic +
|
|
65
|
-
* visibility/online probes, two-strike). Consumers REPORT suspicion
|
|
66
|
-
* ({@link reportTransportSuspicion}); they never close the shared socket
|
|
67
|
-
* themselves. Only two failed probes against the SAME generation retire it,
|
|
68
|
-
* and {@link reconnectIfCurrent} is a compare-and-swap on generation OBJECT
|
|
69
|
-
* IDENTITY — a late verdict against a superseded generation can never close
|
|
70
|
-
* its healthy successor. {@link reconnectIterateSession} is the separate,
|
|
71
|
-
* deliberate *semantic* reset (new claims after create/unlock).
|
|
72
|
-
*/
|
|
73
|
-
let explicitConfig;
|
|
74
|
-
/**
|
|
75
|
-
* Point the keeper at a deployment explicitly — the non-browser entry into the
|
|
76
|
-
* one-socket model (the chat TUI, React Native, keeper-based scripts). Repeating
|
|
77
|
-
* the same target updates its credential source without disturbing the live
|
|
78
|
-
* socket. A different target retires the old deployment immediately and connects
|
|
79
|
-
* the new one, so authority can never cross deployments. In a browser this is
|
|
80
|
-
* optional (the default is `window.location`'s `/api` with cookie auth).
|
|
81
|
-
*/
|
|
82
|
-
function configureIterateSession(config) {
|
|
83
|
-
const targetChanged = explicitConfig !== void 0 && apiWebSocketUrl(explicitConfig.baseUrl).href !== apiWebSocketUrl(config.baseUrl).href;
|
|
84
|
-
explicitConfig = config;
|
|
85
|
-
if (!targetChanged || current === void 0) return;
|
|
86
|
-
const generation = current;
|
|
87
|
-
current = void 0;
|
|
88
|
-
retireGeneration(generation);
|
|
89
|
-
const retiredSession = snapshot?.session;
|
|
90
|
-
snapshot = void 0;
|
|
91
|
-
if (retiredSession !== void 0) disposeSession(retiredSession);
|
|
92
|
-
consecutiveConnectionFailures = 0;
|
|
93
|
-
if (firstConnect === void 0) {
|
|
94
|
-
firstConnect = Promise.withResolvers();
|
|
95
|
-
firstConnect.promise.catch(() => {});
|
|
96
|
-
}
|
|
97
|
-
startConnectionAttempt();
|
|
98
|
-
}
|
|
99
|
-
function resolveConnectionTarget() {
|
|
100
|
-
return {
|
|
101
|
-
url: apiWebSocketUrl(explicitConfig?.baseUrl ?? (typeof window === "undefined" ? missingConnectionTarget() : window.location.href)),
|
|
102
|
-
credentials: explicitConfig?.credentials || { type: "from-server-cookie" }
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function missingConnectionTarget() {
|
|
106
|
-
throw new Error("itx has no connection target: in a browser the session connects to the page's /api (never during SSR — render itx consumers under an `ssr: false` route or <ClientOnly>); anywhere else call configureIterateSession({ baseUrl, credentials }) before connecting.");
|
|
107
|
-
}
|
|
108
|
-
const CONNECTION_TIMEOUT_MS = 15e3;
|
|
109
|
-
const RECONNECT_BACKOFF_MIN_MS = 250;
|
|
110
|
-
const RECONNECT_BACKOFF_MAX_MS = 1e4;
|
|
111
|
-
/**
|
|
112
|
-
* How often a live transport (the socket verifier) and a mounted subscription
|
|
113
|
-
* (the watchdog) prove they are not silently dead. ONE shared cadence: the two
|
|
114
|
-
* checks deliberately use the same value, so a change here moves both.
|
|
115
|
-
*/
|
|
116
|
-
const LIVENESS_INTERVAL_MS = 45e3;
|
|
117
|
-
/** A probe slower than this counts as a strike (two strikes ⇒ half-open). */
|
|
118
|
-
const LIVENESS_TIMEOUT_MS = 1e4;
|
|
119
|
-
let current;
|
|
120
|
-
let snapshot;
|
|
121
|
-
let generationCounter = 0;
|
|
122
|
-
/** Consecutive connects that died before establishing a session — the backoff input. */
|
|
123
|
-
let consecutiveConnectionFailures = 0;
|
|
124
|
-
/**
|
|
125
|
-
* The promise first-load callers share until the FIRST session exists. One
|
|
126
|
-
* stable promise across connection attempt retries: a connection attempt that closes before opening rejects
|
|
127
|
-
* its own `connecting` promise (imperative awaiters fail fast) but must NOT
|
|
128
|
-
* reject a suspended React tree — React replays a suspended (never-committed)
|
|
129
|
-
* component against the thenable it first used, so a rejected first promise
|
|
130
|
-
* would surface in the error boundary even though a paced reconnect is already
|
|
131
|
-
* underway. It rejects only on a TERMINAL failure (authenticate answered with
|
|
132
|
-
* a real application error over a working socket).
|
|
133
|
-
*/
|
|
134
|
-
let firstConnect;
|
|
135
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
136
|
-
const subscribeSession = (onChange) => {
|
|
137
|
-
listeners.add(onChange);
|
|
138
|
-
return () => void listeners.delete(onChange);
|
|
139
|
-
};
|
|
140
|
-
/**
|
|
141
|
-
* One real project stub per (session, slug), cached in module state so a
|
|
142
|
-
* caller's lifecycle (React renders that may be discarded, repeated
|
|
143
|
-
* imperative calls) can't leak undisposed capnweb import entries (see module
|
|
144
|
-
* header). Keyed by the session stub identity, so it is implicitly scoped to a
|
|
145
|
-
* generation and torn down with it. `slug` may be a slug or a `prj_…` id —
|
|
146
|
-
* `session.projects.get` accepts either.
|
|
147
|
-
*/
|
|
148
|
-
const projectStubCaches = /* @__PURE__ */ new WeakMap();
|
|
149
|
-
function projectStubFor(session, slug) {
|
|
150
|
-
let cache = projectStubCaches.get(session);
|
|
151
|
-
if (cache === void 0) {
|
|
152
|
-
cache = /* @__PURE__ */ new Map();
|
|
153
|
-
projectStubCaches.set(session, cache);
|
|
154
|
-
}
|
|
155
|
-
let stub = cache.get(slug);
|
|
156
|
-
if (stub === void 0) {
|
|
157
|
-
stub = session.projects.get(slug);
|
|
158
|
-
cache.set(slug, stub);
|
|
159
|
-
}
|
|
160
|
-
return stub;
|
|
161
|
-
}
|
|
162
|
-
/** getSnapshot for useSyncExternalStore: stable between transitions; connects when idle. */
|
|
163
|
-
function currentSnapshot() {
|
|
164
|
-
if (current === void 0) startConnectionAttempt();
|
|
165
|
-
return snapshot;
|
|
166
|
-
}
|
|
167
|
-
const serverSnapshot = () => missingConnectionTarget();
|
|
168
|
-
/**
|
|
169
|
-
* Ensure a live-or-connecting session and return its connecting promise. The
|
|
170
|
-
* imperative sibling of the React `useIterateSession()`: for handlers,
|
|
171
|
-
* `mutationFn`s, scripts, and lazy closures. Same one socket the hooks use.
|
|
172
|
-
* After a TERMINAL auth rejection this keeps returning that failure (the
|
|
173
|
-
* halted generation — see Generation.failed) until {@link reconnectIterateSession}.
|
|
174
|
-
*/
|
|
175
|
-
function connectIterateSession() {
|
|
176
|
-
return (current ?? startConnectionAttempt()).connecting;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* The project itx for a slug (or `prj_…` id), imperatively. Pipelines through
|
|
180
|
-
* the returned promise — `(await connectItx(slug)).streams.get(path)`. Returns
|
|
181
|
-
* the session-owned cached stub, re-derived automatically after a reconnect.
|
|
182
|
-
*/
|
|
183
|
-
function connectItx(slug) {
|
|
184
|
-
return connectIterateSession().then((session) => projectStubFor(session, slug));
|
|
185
|
-
}
|
|
186
|
-
function startConnectionAttempt() {
|
|
187
|
-
const target = resolveConnectionTarget();
|
|
188
|
-
const id = ++generationCounter;
|
|
189
|
-
const { promise, resolve, reject } = Promise.withResolvers();
|
|
190
|
-
promise.catch(() => {});
|
|
191
|
-
const generation = {
|
|
192
|
-
ws: void 0,
|
|
193
|
-
rpcRoot: void 0,
|
|
194
|
-
connecting: promise,
|
|
195
|
-
rejectConnecting: reject,
|
|
196
|
-
ping: void 0,
|
|
197
|
-
liveness: void 0,
|
|
198
|
-
verifying: false,
|
|
199
|
-
failed: false
|
|
200
|
-
};
|
|
201
|
-
current = generation;
|
|
202
|
-
const priorSession = snapshot?.session;
|
|
203
|
-
if (priorSession === void 0 && firstConnect === void 0) {
|
|
204
|
-
firstConnect = Promise.withResolvers();
|
|
205
|
-
firstConnect.promise.catch(() => {});
|
|
206
|
-
}
|
|
207
|
-
setSnapshot({
|
|
208
|
-
generation: id,
|
|
209
|
-
session: priorSession,
|
|
210
|
-
connecting: firstConnect?.promise ?? promise
|
|
211
|
-
});
|
|
212
|
-
/** Authentication established a session: publish it and retire the predecessor. */
|
|
213
|
-
const publish = (root, ping) => {
|
|
214
|
-
consecutiveConnectionFailures = 0;
|
|
215
|
-
generation.ping = ping;
|
|
216
|
-
generation.liveness = setInterval(() => {
|
|
217
|
-
if (current === generation) verifyTransport(generation);
|
|
218
|
-
}, LIVENESS_INTERVAL_MS);
|
|
219
|
-
const retiring = snapshot?.session;
|
|
220
|
-
setSnapshot({
|
|
221
|
-
generation: id,
|
|
222
|
-
session: root,
|
|
223
|
-
connecting: promise
|
|
224
|
-
});
|
|
225
|
-
resolve(root);
|
|
226
|
-
firstConnect?.resolve(root);
|
|
227
|
-
firstConnect = void 0;
|
|
228
|
-
if (retiring !== void 0) disposeSession(retiring);
|
|
229
|
-
};
|
|
230
|
-
const beginWebSocketConnection = () => {
|
|
231
|
-
if (current !== generation) {
|
|
232
|
-
reject(/* @__PURE__ */ new Error("itx WebSocket closed before connecting"));
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const ws = new WebSocket(target.url.href);
|
|
236
|
-
generation.ws = ws;
|
|
237
|
-
let established = false;
|
|
238
|
-
const timeout = setTimeout(() => {
|
|
239
|
-
if (current !== generation || established) return;
|
|
240
|
-
current = void 0;
|
|
241
|
-
consecutiveConnectionFailures += 1;
|
|
242
|
-
reject(/* @__PURE__ */ new Error("itx WebSocket closed before connecting"));
|
|
243
|
-
retireGeneration(generation);
|
|
244
|
-
startConnectionAttempt();
|
|
245
|
-
}, CONNECTION_TIMEOUT_MS);
|
|
246
|
-
ws.addEventListener("open", () => {
|
|
247
|
-
if (current !== generation) {
|
|
248
|
-
clearTimeout(timeout);
|
|
249
|
-
ws.close();
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const unauthenticated = newWebSocketRpcSession(ws);
|
|
253
|
-
generation.rpcRoot = unauthenticated;
|
|
254
|
-
(async () => {
|
|
255
|
-
let root;
|
|
256
|
-
try {
|
|
257
|
-
const credentialProvider = target.credentials;
|
|
258
|
-
if (typeof credentialProvider === "function") {
|
|
259
|
-
const authenticate = async (forceRefresh) => await unauthenticated.authenticate(await credentialProvider({ forceRefresh }));
|
|
260
|
-
try {
|
|
261
|
-
root = await authenticate(false);
|
|
262
|
-
} catch (error) {
|
|
263
|
-
if (!(error instanceof Error) || !/auth|token|unauthorized|401/i.test(error.message)) throw error;
|
|
264
|
-
root = await authenticate(true);
|
|
265
|
-
}
|
|
266
|
-
} else root = await unauthenticated.authenticate(credentialProvider);
|
|
267
|
-
} catch (error) {
|
|
268
|
-
clearTimeout(timeout);
|
|
269
|
-
if (current !== generation) return;
|
|
270
|
-
if (isItxTransportError(error)) {
|
|
271
|
-
ws.close();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const terminal = error instanceof Error ? error : new Error(String(error));
|
|
275
|
-
generation.failed = true;
|
|
276
|
-
reject(terminal);
|
|
277
|
-
firstConnect?.reject(terminal);
|
|
278
|
-
firstConnect = void 0;
|
|
279
|
-
const zombieSession = snapshot?.session;
|
|
280
|
-
setSnapshot({
|
|
281
|
-
generation: id,
|
|
282
|
-
session: void 0,
|
|
283
|
-
connecting: promise
|
|
284
|
-
});
|
|
285
|
-
if (zombieSession !== void 0) disposeSession(zombieSession);
|
|
286
|
-
retireGeneration(generation);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
clearTimeout(timeout);
|
|
290
|
-
if (current !== generation) {
|
|
291
|
-
root[Symbol.dispose]?.();
|
|
292
|
-
ws.close();
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
established = true;
|
|
296
|
-
publish(root, async () => {
|
|
297
|
-
await root.__describe();
|
|
298
|
-
});
|
|
299
|
-
})();
|
|
300
|
-
});
|
|
301
|
-
ws.addEventListener("close", () => {
|
|
302
|
-
clearTimeout(timeout);
|
|
303
|
-
const wasCurrent = current === generation && !generation.failed;
|
|
304
|
-
disposeGeneration(generation);
|
|
305
|
-
if (wasCurrent) {
|
|
306
|
-
if (!established) consecutiveConnectionFailures += 1;
|
|
307
|
-
current = void 0;
|
|
308
|
-
startConnectionAttempt();
|
|
309
|
-
}
|
|
310
|
-
reject(/* @__PURE__ */ new Error("itx WebSocket closed before connecting"));
|
|
311
|
-
});
|
|
312
|
-
};
|
|
313
|
-
const delay = consecutiveConnectionFailures <= 1 ? 0 : Math.min(RECONNECT_BACKOFF_MAX_MS, RECONNECT_BACKOFF_MIN_MS * 2 ** Math.min(consecutiveConnectionFailures - 2, 6));
|
|
314
|
-
if (delay === 0) beginWebSocketConnection();
|
|
315
|
-
else setTimeout(beginWebSocketConnection, delay);
|
|
316
|
-
return generation;
|
|
317
|
-
}
|
|
318
|
-
function setSnapshot(next) {
|
|
319
|
-
snapshot = next;
|
|
320
|
-
for (const listener of listeners) listener();
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Release a generation's TRANSPORT resources — its liveness timer. Naturally
|
|
324
|
-
* idempotent, and it does NOT close the socket or dispose the session stub: the
|
|
325
|
-
* session is retired separately, by its successor's publish (see the open
|
|
326
|
-
* handler), so invisible reconnect never hands out a disposed stub. Safe to
|
|
327
|
-
* call from the `close` handler without re-entering it.
|
|
328
|
-
*/
|
|
329
|
-
function disposeGeneration(generation) {
|
|
330
|
-
if (generation.liveness !== void 0) clearInterval(generation.liveness);
|
|
331
|
-
generation.liveness = void 0;
|
|
332
|
-
}
|
|
333
|
-
/** Dispose a retired session's project-stub cache and the session stub itself. */
|
|
334
|
-
function disposeSession(session) {
|
|
335
|
-
const cache = projectStubCaches.get(session);
|
|
336
|
-
if (cache) for (const stub of cache.values()) stub[Symbol.dispose]?.();
|
|
337
|
-
session[Symbol.dispose]?.();
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* FORCED retirement (a reconnect, not an observed close): release resources,
|
|
341
|
-
* abort capnweb through its bootstrap stub, then ask the socket to close.
|
|
342
|
-
* Disposing the bootstrap synchronously rejects every pending and future call;
|
|
343
|
-
* relying on WebSocket.close() alone is insufficient because a half-open socket
|
|
344
|
-
* may never finish its close handshake or fire `close`. Callers set
|
|
345
|
-
* `current = undefined` FIRST, so any close event won't auto-reconnect (reconnecting
|
|
346
|
-
* is the caller's job).
|
|
347
|
-
*/
|
|
348
|
-
function retireGeneration(generation) {
|
|
349
|
-
disposeGeneration(generation);
|
|
350
|
-
generation.rejectConnecting(/* @__PURE__ */ new Error("itx WebSocket closed before connecting"));
|
|
351
|
-
generation.rpcRoot?.[Symbol.dispose]?.();
|
|
352
|
-
generation.rpcRoot = void 0;
|
|
353
|
-
generation.ws?.close();
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Report that the shared transport may be half-open (a call hung). Consumers —
|
|
357
|
-
* the subscription watchdog, any long-lived reader — call this instead of
|
|
358
|
-
* closing the socket themselves — {@link verifyTransport} probes and, only on
|
|
359
|
-
* two strikes against the SAME generation, retires it. A stale report is a
|
|
360
|
-
* no-op.
|
|
361
|
-
*/
|
|
362
|
-
function reportTransportSuspicion() {
|
|
363
|
-
const generation = current;
|
|
364
|
-
if (generation?.ping === void 0) return;
|
|
365
|
-
verifyTransport(generation);
|
|
366
|
-
}
|
|
367
|
-
async function verifyTransport(generation) {
|
|
368
|
-
if (generation.verifying || current !== generation || generation.ping === void 0) return;
|
|
369
|
-
generation.verifying = true;
|
|
370
|
-
try {
|
|
371
|
-
const probeOnce = () => Promise.race([generation.ping().then(() => "alive", (error) => isItxTransportError(error) ? "strike" : "alive"), new Promise((resolve) => setTimeout(() => resolve("strike"), LIVENESS_TIMEOUT_MS))]);
|
|
372
|
-
let verdict = await probeOnce();
|
|
373
|
-
if (verdict === "strike" && current === generation) verdict = await probeOnce();
|
|
374
|
-
if (verdict === "strike") reconnectIfCurrent(generation);
|
|
375
|
-
} finally {
|
|
376
|
-
generation.verifying = false;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Retire + reconnect the transport, but ONLY if `generation` still owns the slot —
|
|
381
|
-
* the compare-and-swap that stops a late verdict (a probe that timed out on a
|
|
382
|
-
* corpse) from closing the healthy successor another path already connected.
|
|
383
|
-
*/
|
|
384
|
-
function reconnectIfCurrent(generation) {
|
|
385
|
-
if (current !== generation) return;
|
|
386
|
-
current = void 0;
|
|
387
|
-
retireGeneration(generation);
|
|
388
|
-
startConnectionAttempt();
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* The SEMANTIC reset (not a transport reconnect): drop the live socket and connection attempt
|
|
392
|
-
* a fresh one so the next reads run under the caller's CURRENT claims. Call
|
|
393
|
-
* after creating a project or unlocking admin — the live socket carries the
|
|
394
|
-
* connect-time principal. React callers that need already-cached data refreshed
|
|
395
|
-
* should also `invalidateQueries({ queryKey: ["itx"] })`.
|
|
396
|
-
*/
|
|
397
|
-
function reconnectIterateSession() {
|
|
398
|
-
const generation = current;
|
|
399
|
-
if (generation !== void 0) {
|
|
400
|
-
current = void 0;
|
|
401
|
-
retireGeneration(generation);
|
|
402
|
-
}
|
|
403
|
-
consecutiveConnectionFailures = 0;
|
|
404
|
-
startConnectionAttempt();
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Revive only a generation parked by a terminal authentication failure. This
|
|
408
|
-
* is the imperative retry boundary for query/mutation clients: calling it
|
|
409
|
-
* before a repeated operation lets refreshed credentials dial again without
|
|
410
|
-
* disturbing a healthy or merely reconnecting session.
|
|
411
|
-
*/
|
|
412
|
-
function retryFailedIterateSession() {
|
|
413
|
-
if (current?.failed) reconnectIterateSession();
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Release the current transport and authority without reconnecting. Intended
|
|
417
|
-
* for process-local lifecycle boundaries such as signing out of a native app;
|
|
418
|
-
* mounted consumers should be removed in the same transition. The explicit
|
|
419
|
-
* deployment configuration remains, so a later connect can dial it again.
|
|
420
|
-
*/
|
|
421
|
-
function disconnectIterateSession() {
|
|
422
|
-
const generation = current;
|
|
423
|
-
current = void 0;
|
|
424
|
-
if (generation !== void 0) retireGeneration(generation);
|
|
425
|
-
const retiredSession = snapshot?.session;
|
|
426
|
-
snapshot = void 0;
|
|
427
|
-
if (retiredSession !== void 0) disposeSession(retiredSession);
|
|
428
|
-
firstConnect?.reject(/* @__PURE__ */ new Error("itx session disconnected"));
|
|
429
|
-
firstConnect = void 0;
|
|
430
|
-
consecutiveConnectionFailures = 0;
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* The four — and only four — transport-close rejections a caller may treat as
|
|
434
|
-
* "the socket died, retry on a fresh one": our own connection-closed rejection, capnweb's
|
|
435
|
-
* two WebSocket aborts (`Peer closed WebSocket: <code> <reason>` and
|
|
436
|
-
* `WebSocket connection failed.`), and capnweb's exact local-shutdown error
|
|
437
|
-
* from our forced bootstrap disposal. Deliberately NARROW: an
|
|
438
|
-
* application/auth/validation error that merely mentions "WebSocket" must never
|
|
439
|
-
* be mistaken for a transport failure and retried. This is the one discriminant
|
|
440
|
-
* shared by the query retry, the subscribe retry, and the liveness verifier.
|
|
441
|
-
*/
|
|
442
|
-
function isItxTransportError(error) {
|
|
443
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
444
|
-
return message.includes("itx WebSocket closed before connecting") || message.includes("Peer closed WebSocket") || message.includes("WebSocket connection failed") || message.includes("RPC session was shut down by disposing the main stub");
|
|
445
|
-
}
|
|
446
|
-
if (typeof document !== "undefined") {
|
|
447
|
-
document.addEventListener("visibilitychange", () => {
|
|
448
|
-
if (document.visibilityState === "visible") reportTransportSuspicion();
|
|
449
|
-
});
|
|
450
|
-
window.addEventListener("online", () => reportTransportSuspicion());
|
|
451
|
-
}
|
|
452
|
-
const PING_TIMED_OUT = Symbol("itx-ping-timed-out");
|
|
453
|
-
/**
|
|
454
|
-
* Release a push connection completely: `close()` closes the server side
|
|
455
|
-
* (already-dead is fine — the rejection is swallowed), then
|
|
456
|
-
* `[Symbol.dispose]` frees the caller-owned stub. One helper because
|
|
457
|
-
* forgetting either half leaks (see {@link ItxRecoverableConnectionHandle}).
|
|
458
|
-
*/
|
|
459
|
-
function releaseItxConnection(handle) {
|
|
460
|
-
Promise.resolve().then(() => handle.close()).catch(() => {}).finally(() => handle[Symbol.dispose]?.());
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Poll a push connection's `ping()` until it stops answering `true`, then
|
|
464
|
-
* recover. Server pushes fail SILENTLY: a dead Durable Object or half-open TCP
|
|
465
|
-
* stops delivering with no client-visible signal, so a consumer can show
|
|
466
|
-
* "live" forever while stale. The watchdog checks on an interval and — because
|
|
467
|
-
* those are exactly when sockets die — when a browser tab becomes visible or
|
|
468
|
-
* comes online (in runtimes without those signals, the interval carries it).
|
|
469
|
-
*
|
|
470
|
-
* `dead` → ping answered `false` or REJECTED: the socket works but the
|
|
471
|
-
* server-side connection is gone (DO restart / dropped
|
|
472
|
-
* callback). Recovery opens it again on the same socket.
|
|
473
|
-
* `timed-out` → ping never answered: the shared WebSocket is half-open. The
|
|
474
|
-
* watchdog REPORTS the suspicion to the socket-owned verifier
|
|
475
|
-
* ({@link reportTransportSuspicion}) — it never closes the socket
|
|
476
|
-
* itself — and the holder opens again once the generation
|
|
477
|
-
* reconnects.
|
|
478
|
-
*/
|
|
479
|
-
function watchItxConnection(ping, onDead) {
|
|
480
|
-
let stopped = false;
|
|
481
|
-
let checking = false;
|
|
482
|
-
const report = (reason) => {
|
|
483
|
-
if (stopped) return;
|
|
484
|
-
stop();
|
|
485
|
-
if (reason === "timed-out") reportTransportSuspicion();
|
|
486
|
-
onDead();
|
|
487
|
-
};
|
|
488
|
-
const pingOnce = () => {
|
|
489
|
-
const timeout = new Promise((resolve) => setTimeout(() => resolve(PING_TIMED_OUT), LIVENESS_TIMEOUT_MS));
|
|
490
|
-
return Promise.race([Promise.resolve(ping()), timeout]);
|
|
491
|
-
};
|
|
492
|
-
const check = async () => {
|
|
493
|
-
if (stopped || checking) return;
|
|
494
|
-
checking = true;
|
|
495
|
-
try {
|
|
496
|
-
let alive;
|
|
497
|
-
try {
|
|
498
|
-
alive = await pingOnce();
|
|
499
|
-
if (alive === PING_TIMED_OUT && !stopped) alive = await pingOnce();
|
|
500
|
-
} catch {
|
|
501
|
-
report("dead");
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
if (alive === PING_TIMED_OUT) report("timed-out");
|
|
505
|
-
else if (alive !== true) report("dead");
|
|
506
|
-
} finally {
|
|
507
|
-
checking = false;
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
const inBrowser = typeof document !== "undefined";
|
|
511
|
-
const onVisible = () => {
|
|
512
|
-
if (document.visibilityState === "visible") check();
|
|
513
|
-
};
|
|
514
|
-
const onOnline = () => void check();
|
|
515
|
-
const interval = setInterval(() => void check(), LIVENESS_INTERVAL_MS);
|
|
516
|
-
if (inBrowser) {
|
|
517
|
-
document.addEventListener("visibilitychange", onVisible);
|
|
518
|
-
window.addEventListener("online", onOnline);
|
|
519
|
-
}
|
|
520
|
-
const stop = () => {
|
|
521
|
-
if (stopped) return;
|
|
522
|
-
stopped = true;
|
|
523
|
-
clearInterval(interval);
|
|
524
|
-
if (inBrowser) {
|
|
525
|
-
document.removeEventListener("visibilitychange", onVisible);
|
|
526
|
-
window.removeEventListener("online", onOnline);
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
return stop;
|
|
530
|
-
}
|
|
531
|
-
//#endregion
|
|
532
|
-
export { disconnectIterateSession as a, reconnectIterateSession as c, retryFailedIterateSession as d, serverSnapshot as f, currentSnapshot as i, releaseItxConnection as l, watchItxConnection as m, connectIterateSession as n, isItxTransportError as o, subscribeSession as p, connectItx as r, projectStubFor as s, configureIterateSession as t, reportTransportSuspicion as u };
|
|
533
|
-
|
|
534
|
-
//# sourceMappingURL=itx-session-sjud8GiT.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"itx-session-sjud8GiT.mjs","names":[],"sources":["../src/itx/itx-session.ts"],"sourcesContent":["/**\n * itx-session — the framework-free half of the itx client: ONE WebSocket per\n * process (browser tab, TUI, phone), connected to an OS deployment's `/api`,\n * `authenticate()`d into a **Session** (the catalog that vends project itxs via\n * `session.projects.get(slug)`), and kept alive through transport gaps.\n *\n * React never appears in this module. The hooks in ../sdk/itx/react.ts are a thin\n * binding over the exact surface exported here (`subscribeSession` +\n * `currentSnapshot` feed `useSyncExternalStore`; everything else is shared\n * verbatim), so a non-React consumer — a node script, a future runtime —\n * gets the same one-socket semantics by importing `iterate/client`.\n *\n * WHERE THE CONNECTION TARGET COMES FROM\n * • In a browser, nothing to configure: the keeper connects\n * `window.location`'s `/api` and authenticates with the session cookie\n * riding the WebSocket handshake.\n * • Anywhere else (the chat TUI, tests, scripts that want the KEEPER rather\n * than the one-shot node dial), call {@link configureIterateSession} with a\n * base URL and credentials. Calling it again for the same deployment\n * refreshes the credential source without disturbing the socket; changing\n * deployments deliberately replaces the socket. The runtime's global\n * WebSocket carries the dial — node ≥ 22, bun, and React Native all satisfy\n * capnweb's WebSocket needs.\n *\n * ───────────────────────────────────────────────────────────────────────────\n * THE SESSION MODEL — one socket, generations, invisible reconnect\n * ───────────────────────────────────────────────────────────────────────────\n *\n * • ONE WebSocket for the whole process, kept in module state (so in a\n * browser it persists across client-side navigation). One connection attempt is a\n * GENERATION ({@link Generation}): its WebSocket and its connecting\n * promise. The session is the AWAITED `authenticate()` result — one settled\n * stub identity shared by the snapshot, imperative awaiters, and the\n * project-stub cache (resolving with the raw pipelined RpcPromise would\n * fork identities: native promises assimilate thenables). The connection timeout\n * spans the whole handshake (TCP/TLS/upgrade AND authenticate), and a REAL\n * auth rejection over a working socket is terminal — it surfaces from the\n * connecting promise instead of looping.\n *\n * • RECONNECT IS INVISIBLE. Readers see an immutable {@link Snapshot};\n * `snapshot.session` holds the LAST live session and is kept across a\n * transport gap. Before the FIRST session, awaiters share the STABLE\n * {@link firstConnect} promise, which survives failed connection attempts (paced\n * reconnects happen behind it; an individual attempt's rejection never reaches a suspended\n * React tree) and rejects only on a TERMINAL failure. Kept-across-the-gap\n * applies to TRANSPORT gaps only: a terminal auth rejection on a reconnect is\n * an AUTHORITY loss — the halted snapshot drops the (already dead) session\n * so the real error surfaces instead of zombie stubs.\n *\n * • PROJECT STUBS ARE SESSION-OWNED. `session.projects.get` allocates a\n * capnweb import-table entry, so deriving stubs ad hoc (or inside React\n * renders that may be discarded) would leak them. The module\n * {@link projectStubCaches} WeakMap keyed by the session stub caches one\n * real stub per (session, slug); a retired generation disposes them. The\n * stub stays the REAL capnweb stub (a lazy wrapper that awaited the session\n * per call would break pipelining — capnweb fork v0.8.0), and its identity\n * changes exactly once per successful reconnect.\n *\n * • TRANSPORT HEALTH IS SOCKET-OWNED and GENERATION-GUARDED. A half-open\n * socket (laptop sleep, network switch — no `close` event) is detected by\n * one verifier per generation ({@link verifyTransport}: periodic +\n * visibility/online probes, two-strike). Consumers REPORT suspicion\n * ({@link reportTransportSuspicion}); they never close the shared socket\n * themselves. Only two failed probes against the SAME generation retire it,\n * and {@link reconnectIfCurrent} is a compare-and-swap on generation OBJECT\n * IDENTITY — a late verdict against a superseded generation can never close\n * its healthy successor. {@link reconnectIterateSession} is the separate,\n * deliberate *semantic* reset (new claims after create/unlock).\n */\nimport { newWebSocketRpcSession, type RpcStub } from \"@iterate-com/capnweb\";\nimport type {\n ItxAuthCredentials,\n Project,\n Session,\n UnauthenticatedOs,\n} from \"../itx-api.generated.ts\";\nimport { apiWebSocketUrl } from \"./api-url.ts\";\n\n// A handle is a capnweb `RpcStub` — a chainable proxy over the contract\n// interface. `authenticate()` / `projects.get()` return an `RpcPromise` (a stub\n// that is *also* awaitable); an RpcPromise is assignable to the plain RpcStub,\n// so the pipelined calls narrow to these without a cast. `RpcStub<Session>` (not\n// the awaitable RpcPromise) keeps `Promise<SessionStub>` from nesting.\n\n/** The Session catalog (what `authenticate()` returns): vends project itxs. */\nexport type SessionStub = RpcStub<Session>;\n/** A project capability handle — `session.projects.get(slug)`. */\nexport type ProjectStub = RpcStub<Project>;\nexport type { ProjectStub as Itx };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The connection target: browser defaults, or an explicit configuration.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type IterateSessionConfig = {\n /** OS deployment base URL, e.g. `https://os.iterate.com`; `/api` is appended. */\n baseUrl: string;\n /**\n * How `authenticate()` identifies the caller. A provider is resolved for\n * every dial, so rotating credentials stay fresh across transport reconnects.\n * If authentication rejects with an auth-shaped error, providers get one\n * forced-refresh attempt before the failure becomes terminal. Default: the\n * browser session cookie.\n */\n credentials?:\n | ItxAuthCredentials\n | ((options: { forceRefresh: boolean }) => ItxAuthCredentials | Promise<ItxAuthCredentials>);\n};\n\nlet explicitConfig: IterateSessionConfig | undefined;\n\n/**\n * Point the keeper at a deployment explicitly — the non-browser entry into the\n * one-socket model (the chat TUI, React Native, keeper-based scripts). Repeating\n * the same target updates its credential source without disturbing the live\n * socket. A different target retires the old deployment immediately and connects\n * the new one, so authority can never cross deployments. In a browser this is\n * optional (the default is `window.location`'s `/api` with cookie auth).\n */\nexport function configureIterateSession(config: IterateSessionConfig): void {\n const targetChanged =\n explicitConfig !== undefined &&\n apiWebSocketUrl(explicitConfig.baseUrl).href !== apiWebSocketUrl(config.baseUrl).href;\n explicitConfig = config;\n if (!targetChanged || current === undefined) return;\n\n const generation = current;\n current = undefined;\n retireGeneration(generation);\n const retiredSession = snapshot?.session;\n snapshot = undefined;\n if (retiredSession !== undefined) disposeSession(retiredSession);\n consecutiveConnectionFailures = 0;\n // Connect now so mounted hooks see one coherent target transition rather than\n // retaining the previous deployment until their next read.\n if (firstConnect === undefined) {\n firstConnect = Promise.withResolvers<SessionStub>();\n void firstConnect.promise.catch(() => {});\n }\n startConnectionAttempt();\n}\n\nfunction resolveConnectionTarget(): {\n url: URL;\n credentials: NonNullable<IterateSessionConfig[\"credentials\"]>;\n} {\n const base =\n explicitConfig?.baseUrl ??\n (typeof window === \"undefined\" ? missingConnectionTarget() : window.location.href);\n return {\n url: apiWebSocketUrl(base),\n credentials: explicitConfig?.credentials || { type: \"from-server-cookie\" },\n };\n}\n\nfunction missingConnectionTarget(): never {\n throw new Error(\n \"itx has no connection target: in a browser the session connects to the page's /api \" +\n \"(never during SSR — render itx consumers under an `ssr: false` route or <ClientOnly>); \" +\n \"anywhere else call configureIterateSession({ baseUrl, credentials }) before connecting.\",\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The one socket: a single live WebSocket per process, in module state.\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst CONNECTION_TIMEOUT_MS = 15_000;\n// Pace reconnects after a WebSocket closes before authentication. Without it, a\n// fast-REFUSING endpoint (offline after mobile resume, dev-server restart)\n// loops connect → instant close → reconnect with zero delay, unbounded. The wait\n// lives inside the connecting promise, so `use()` semantics are unchanged.\nconst RECONNECT_BACKOFF_MIN_MS = 250;\nconst RECONNECT_BACKOFF_MAX_MS = 10_000;\n/**\n * How often a live transport (the socket verifier) and a mounted subscription\n * (the watchdog) prove they are not silently dead. ONE shared cadence: the two\n * checks deliberately use the same value, so a change here moves both.\n */\nconst LIVENESS_INTERVAL_MS = 45_000;\n/** A probe slower than this counts as a strike (two strikes ⇒ half-open). */\nconst LIVENESS_TIMEOUT_MS = 10_000;\n\n/**\n * One connection attempt = one WebSocket lifetime. OBJECT IDENTITY is the\n * compare-and-swap token that makes every reconnect verdict identity-safe\n * (`current === generation` — only the CURRENT generation may be retired or\n * published). `connecting` settles with the session once `authenticate()`\n * returns over the open socket; `ping` doubles as the \"session established\"\n * marker.\n */\ntype Generation = {\n ws: WebSocket | undefined;\n /**\n * The capnweb bootstrap stub owns the whole RPC session. Disposing it aborts\n * every pending import immediately, which is stronger than WebSocket.close()\n * on a half-open socket (that close handshake may never produce an event).\n */\n rpcRoot: RpcStub<UnauthenticatedOs> | undefined;\n connecting: Promise<SessionStub>;\n /** Settles an imperative waiter if forced retirement happens before publish. */\n rejectConnecting: (reason?: unknown) => void;\n /** One cheap authenticated round trip proving the transport is alive. */\n ping: (() => Promise<void>) | undefined;\n liveness: ReturnType<typeof setInterval> | undefined;\n /** Single-flight latch for {@link verifyTransport}, per generation. */\n verifying: boolean;\n /**\n * HALTED after a terminal auth rejection: the generation keeps owning the\n * slot (so a render can never trigger another connection attempt — every re-render of an\n * always-mounted hook reads {@link currentSnapshot}, which connects when the\n * slot is empty, and the failure's own setState re-renders: an unbounded\n * socket storm otherwise) and its close handler must not reconnect. Only an\n * explicit {@link reconnectIterateSession} (or a page load) revives.\n */\n failed: boolean;\n};\n\n/**\n * The immutable value readers see (React reads it via `useSyncExternalStore` +\n * `use()`). Replaced wholesale on every transition, always BEFORE listeners are\n * notified, so a concurrent render can never tear. `session` is the last live\n * session and survives transport gaps — that is what makes reconnect\n * invisible. `generation` is a monotonic number whose only job is being the\n * reconnect dep for the React layer's reconnect-aware effect (the CAS is\n * generation object identity, not this number). `connecting` is what\n * first-load callers await/suspend on: before the FIRST session it is the\n * stable {@link firstConnect} promise (survives closed-before-open retries\n * without rejecting a suspended tree); afterwards `session` is always defined.\n */\ntype Snapshot = {\n generation: number;\n session: SessionStub | undefined;\n connecting: Promise<SessionStub>;\n};\n\nlet current: Generation | undefined;\nlet snapshot: Snapshot | undefined;\nlet generationCounter = 0;\n/** Consecutive connects that died before establishing a session — the backoff input. */\nlet consecutiveConnectionFailures = 0;\n/**\n * The promise first-load callers share until the FIRST session exists. One\n * stable promise across connection attempt retries: a connection attempt that closes before opening rejects\n * its own `connecting` promise (imperative awaiters fail fast) but must NOT\n * reject a suspended React tree — React replays a suspended (never-committed)\n * component against the thenable it first used, so a rejected first promise\n * would surface in the error boundary even though a paced reconnect is already\n * underway. It rejects only on a TERMINAL failure (authenticate answered with\n * a real application error over a working socket).\n */\nlet firstConnect: PromiseWithResolvers<SessionStub> | undefined;\n\nconst listeners = new Set<() => void>();\nexport const subscribeSession = (onChange: () => void) => {\n listeners.add(onChange);\n return () => void listeners.delete(onChange);\n};\n\n/**\n * One real project stub per (session, slug), cached in module state so a\n * caller's lifecycle (React renders that may be discarded, repeated\n * imperative calls) can't leak undisposed capnweb import entries (see module\n * header). Keyed by the session stub identity, so it is implicitly scoped to a\n * generation and torn down with it. `slug` may be a slug or a `prj_…` id —\n * `session.projects.get` accepts either.\n */\nconst projectStubCaches = new WeakMap<SessionStub, Map<string, ProjectStub>>();\n\nexport function projectStubFor(session: SessionStub, slug: string): ProjectStub {\n let cache = projectStubCaches.get(session);\n if (cache === undefined) {\n cache = new Map();\n projectStubCaches.set(session, cache);\n }\n let stub = cache.get(slug);\n if (stub === undefined) {\n stub = session.projects.get(slug);\n cache.set(slug, stub);\n }\n return stub;\n}\n\n/** getSnapshot for useSyncExternalStore: stable between transitions; connects when idle. */\nexport function currentSnapshot(): Snapshot {\n // `startConnectionAttempt()` sets `current` synchronously, so this fires at most once per idle\n // window (first load, or after a retry exhausted its own reconnect) — never a\n // per-render loop.\n if (current === undefined) startConnectionAttempt();\n return snapshot!;\n}\n\nexport const serverSnapshot = (): never => missingConnectionTarget();\n\n/**\n * Ensure a live-or-connecting session and return its connecting promise. The\n * imperative sibling of the React `useIterateSession()`: for handlers,\n * `mutationFn`s, scripts, and lazy closures. Same one socket the hooks use.\n * After a TERMINAL auth rejection this keeps returning that failure (the\n * halted generation — see Generation.failed) until {@link reconnectIterateSession}.\n */\nexport function connectIterateSession(): Promise<SessionStub> {\n // No runtime guard here: `startConnectionAttempt()` resolves its target first thing, so an\n // unconfigured non-browser call throws the no-target error synchronously.\n return (current ?? startConnectionAttempt()).connecting;\n}\n\n/**\n * The project itx for a slug (or `prj_…` id), imperatively. Pipelines through\n * the returned promise — `(await connectItx(slug)).streams.get(path)`. Returns\n * the session-owned cached stub, re-derived automatically after a reconnect.\n */\nexport function connectItx(slug: string): Promise<ProjectStub> {\n return connectIterateSession().then((session) => projectStubFor(session, slug));\n}\n\nfunction startConnectionAttempt(): Generation {\n const target = resolveConnectionTarget();\n\n const id = ++generationCounter;\n const { promise, resolve, reject } = Promise.withResolvers<SessionStub>();\n // Keep an internal handler so a connection attempt that rejects with no live awaiter never\n // surfaces as an unhandledrejection — real `connectIterateSession()` awaiters still observe it.\n void promise.catch(() => {});\n const generation: Generation = {\n ws: undefined,\n rpcRoot: undefined,\n connecting: promise,\n rejectConnecting: reject,\n ping: undefined,\n liveness: undefined,\n verifying: false,\n failed: false,\n };\n current = generation;\n // Keep showing the last session while the new generation connects (invisible\n // reconnect). Before the FIRST session, awaiters must share the stable\n // first-connect promise (see {@link firstConnect}) — never one attempt's promise\n // whose closed-before-open rejection React would replay into an error boundary.\n const priorSession = snapshot?.session;\n if (priorSession === undefined && firstConnect === undefined) {\n firstConnect = Promise.withResolvers<SessionStub>();\n void firstConnect.promise.catch(() => {});\n }\n setSnapshot({\n generation: id,\n session: priorSession,\n connecting: firstConnect?.promise ?? promise,\n });\n\n /** Authentication established a session: publish it and retire the predecessor. */\n const publish = (root: SessionStub, ping: () => Promise<void>) => {\n consecutiveConnectionFailures = 0;\n generation.ping = ping;\n generation.liveness = setInterval(() => {\n if (current === generation) void verifyTransport(generation);\n }, LIVENESS_INTERVAL_MS);\n // Retire the PREVIOUS published session — exactly once, now that its\n // successor is live. It was kept alive through the reconnect gap so\n // useIterateSession()/useItx() never handed out a disposed stub; dispose its\n // project-stub cache + the stub itself only here.\n //\n // Safe even though this runs the same turn as setSnapshot (before React\n // commits the re-render whose subscription cleanups unsubscribe): a\n // successor only connects after `current` was cleared, and every path clears\n // it AFTER closing the prior socket (the close handler, reconnectIfCurrent,\n // reconnectIterateSession). So the prior transport is ALWAYS already closed\n // here — its subscriptions are already dead (capnweb rejects on close) and\n // their unsubscribe cleanups are catch-wrapped. This releases already-dead\n // local refs, never a live subscription.\n const retiring = snapshot?.session;\n setSnapshot({ generation: id, session: root, connecting: promise });\n resolve(root);\n firstConnect?.resolve(root);\n firstConnect = undefined;\n if (retiring !== undefined) disposeSession(retiring);\n };\n\n const beginWebSocketConnection = () => {\n // Superseded while waiting out the backoff: this attempt no longer owns the\n // slot — settle and let the current attempt continue.\n if (current !== generation) {\n reject(new Error(\"itx WebSocket closed before connecting\"));\n return;\n }\n const ws = new WebSocket(target.url.href);\n generation.ws = ws;\n let established = false;\n // The timeout spans TCP/TLS/upgrade AND the authenticate\n // round trip — so a server that accepts the socket but never answers\n // authenticate call, so an unanswered authenticate becomes a paced reconnect.\n const timeout = setTimeout(() => {\n // Do not delegate progress to `close`: a half-open socket can accept the\n // close request without ever firing a close event. Settle this attempt,\n // abort its RPC session, and start a new connection attempt here.\n if (current !== generation || established) return;\n current = undefined;\n consecutiveConnectionFailures += 1;\n reject(new Error(\"itx WebSocket closed before connecting\"));\n retireGeneration(generation);\n startConnectionAttempt();\n }, CONNECTION_TIMEOUT_MS);\n\n ws.addEventListener(\"open\", () => {\n // Generation CAS: a superseded generation's late `open` (its successor was\n // already connected) must never publish over the live one. Close and bail.\n if (current !== generation) {\n clearTimeout(timeout);\n ws.close();\n return;\n }\n // ONE awaited authenticate: its result is THE session identity — the same\n // settled stub for the snapshot, the imperative promise, and the\n // project-stub cache. (Resolving with the raw pipelined RpcPromise would\n // fork identities: a native promise ASSIMILATES a thenable, so imperative\n // awaiters would receive the pulled resolution while the snapshot held the\n // RpcPromise — two WeakMap keys, doubled project stubs, and a\n // disposeSession that misses half of them.) In a browser the default\n // credentials ride the session cookie on the handshake; configured\n // consumers authenticate with their explicit credentials.\n const unauthenticated = newWebSocketRpcSession<UnauthenticatedOs>(ws);\n generation.rpcRoot = unauthenticated;\n void (async () => {\n let root: SessionStub;\n try {\n // The runtime value IS a session stub (same callable surface); the\n // cast bridges capnweb's Awaited-type nesting (`Stubify<Session>`\n // re-wraps every member in promise types) back to the nominal handle.\n // This is the module's ONE cast, at the identity boundary.\n const credentialProvider = target.credentials;\n if (typeof credentialProvider === \"function\") {\n const authenticate = async (forceRefresh: boolean) =>\n (await unauthenticated.authenticate(\n await credentialProvider({ forceRefresh }),\n )) as unknown as SessionStub;\n try {\n root = await authenticate(false);\n } catch (error) {\n if (\n !(error instanceof Error) ||\n !/auth|token|unauthorized|401/i.test(error.message)\n ) {\n throw error;\n }\n root = await authenticate(true);\n }\n } else {\n root = (await unauthenticated.authenticate(\n credentialProvider,\n )) as unknown as SessionStub;\n }\n } catch (error) {\n clearTimeout(timeout);\n if (current !== generation) return;\n if (isItxTransportError(error)) {\n // The socket died mid-handshake; the close handler owns recovery.\n ws.close();\n return;\n }\n // The server ANSWERED with a real application error (bad principal,\n // rejected handshake): this connection attempt is terminally failed. Surface it to\n // imperative awaiters AND the suspended tree — retrying would loop.\n // Reject BEFORE closing: the close handler's generic connection-closed\n // rejection must never mask the real error. Then HALT: the failed\n // generation keeps owning the slot (see Generation.failed) so a\n // render can't storm fresh connects; publish the halted snapshot so\n // effects observe one final generation and settle in \"error\".\n //\n // The halted snapshot carries NO session. Keeping the last session is\n // the invisible-reconnect move for TRANSPORT gaps — but this is an\n // AUTHORITY loss (claims revoked / signed out elsewhere): the prior\n // session's socket is already closed and no reconnect will revive it, so\n // handing it out would wedge hooks on zombie stubs whose calls fail\n // with transport-shaped errors while the real auth error never\n // surfaces. Dropping it makes useIterateSession() re-throw the\n // terminal error to the boundary; the prior session's refs are\n // released here (they were dead already).\n const terminal = error instanceof Error ? error : new Error(String(error));\n generation.failed = true;\n reject(terminal);\n firstConnect?.reject(terminal);\n firstConnect = undefined;\n const zombieSession = snapshot?.session;\n setSnapshot({ generation: id, session: undefined, connecting: promise });\n if (zombieSession !== undefined) disposeSession(zombieSession);\n retireGeneration(generation);\n return;\n }\n clearTimeout(timeout);\n if (current !== generation) {\n // Superseded while authenticating: never publish; release the stub.\n (root as Partial<Disposable>)[Symbol.dispose]?.();\n ws.close();\n return;\n }\n established = true;\n publish(root, async () => {\n // Any round trip proves the transport. Probe the already-authorized\n // Session capability: re-running authenticate would needlessly\n // re-verify the cookie/token (and can cross into the auth worker)\n // every 45 seconds per tab merely to test socket health.\n await root.__describe();\n });\n })();\n });\n\n ws.addEventListener(\"close\", () => {\n clearTimeout(timeout);\n // A HALTED generation stays the owner: its socket closing must not\n // reconnect (that would restart the terminal-auth loop it exists to stop).\n const wasCurrent = current === generation && !generation.failed;\n // The socket is already closed — release resources WITHOUT re-closing it\n // (that would re-enter this handler); disposeGeneration is idempotent.\n disposeGeneration(generation);\n if (wasCurrent) {\n // A connection attempt that never established a session counts toward backoff; a\n // post-establish death is a transient to recover from immediately.\n if (!established) consecutiveConnectionFailures += 1;\n current = undefined;\n // With a live session in hand this is the INVISIBLE reconnect\n // (the snapshot keeps showing it); with none it keeps first-load\n // callers on the stable first-connect promise — a paced retry, never\n // a wedge or an error boundary.\n startConnectionAttempt();\n }\n // Once a connection attempt has resolved this is a no-op; a connection attempt that closed BEFORE\n // establishing rejects so imperative `connectIterateSession()` awaiters fail fast.\n reject(new Error(\"itx WebSocket closed before connecting\"));\n });\n };\n\n // The FIRST retry is immediate (a one-off blip should recover instantly);\n // pacing kicks in from the second consecutive failure — that's the storm.\n const delay =\n consecutiveConnectionFailures <= 1\n ? 0\n : Math.min(\n RECONNECT_BACKOFF_MAX_MS,\n RECONNECT_BACKOFF_MIN_MS * 2 ** Math.min(consecutiveConnectionFailures - 2, 6),\n );\n if (delay === 0) beginWebSocketConnection();\n else setTimeout(beginWebSocketConnection, delay);\n return generation;\n}\n\nfunction setSnapshot(next: Snapshot): void {\n // Replace the immutable snapshot, then wake readers — always in this order so\n // a concurrent render can't observe listeners firing against a stale value.\n snapshot = next;\n for (const listener of listeners) listener();\n}\n\n/**\n * Release a generation's TRANSPORT resources — its liveness timer. Naturally\n * idempotent, and it does NOT close the socket or dispose the session stub: the\n * session is retired separately, by its successor's publish (see the open\n * handler), so invisible reconnect never hands out a disposed stub. Safe to\n * call from the `close` handler without re-entering it.\n */\nfunction disposeGeneration(generation: Generation): void {\n if (generation.liveness !== undefined) clearInterval(generation.liveness);\n generation.liveness = undefined;\n}\n\n/** Dispose a retired session's project-stub cache and the session stub itself. */\nfunction disposeSession(session: SessionStub): void {\n const cache = projectStubCaches.get(session);\n if (cache) for (const stub of cache.values()) (stub as Partial<Disposable>)[Symbol.dispose]?.();\n (session as Partial<Disposable>)[Symbol.dispose]?.();\n}\n\n/**\n * FORCED retirement (a reconnect, not an observed close): release resources,\n * abort capnweb through its bootstrap stub, then ask the socket to close.\n * Disposing the bootstrap synchronously rejects every pending and future call;\n * relying on WebSocket.close() alone is insufficient because a half-open socket\n * may never finish its close handshake or fire `close`. Callers set\n * `current = undefined` FIRST, so any close event won't auto-reconnect (reconnecting\n * is the caller's job).\n */\nfunction retireGeneration(generation: Generation): void {\n disposeGeneration(generation);\n generation.rejectConnecting(new Error(\"itx WebSocket closed before connecting\"));\n generation.rpcRoot?.[Symbol.dispose]?.();\n generation.rpcRoot = undefined;\n generation.ws?.close();\n}\n\n/**\n * Report that the shared transport may be half-open (a call hung). Consumers —\n * the subscription watchdog, any long-lived reader — call this instead of\n * closing the socket themselves — {@link verifyTransport} probes and, only on\n * two strikes against the SAME generation, retires it. A stale report is a\n * no-op.\n */\nexport function reportTransportSuspicion(): void {\n const generation = current;\n // An attempt still awaiting authentication owns its connection timeout; nothing to verify.\n if (generation?.ping === undefined) return;\n void verifyTransport(generation);\n}\n\nasync function verifyTransport(generation: Generation): Promise<void> {\n // Single-flight PER GENERATION, not process-wide: a probe still racing on a\n // retired generation (its ping hung and never settled) must not block\n // verifying the fresh successor.\n if (generation.verifying || current !== generation || generation.ping === undefined) return;\n generation.verifying = true;\n try {\n // A probe is a STRIKE only when the transport itself failed — a timeout, or\n // a rejection classified as a transport close. Any other rejection means the\n // socket ANSWERED (an application/auth error came back over it), so the\n // transport is alive. Rejections are caught here, never left unhandled.\n const probeOnce = (): Promise<\"alive\" | \"strike\"> =>\n Promise.race([\n generation.ping!().then(\n () => \"alive\" as const,\n (error: unknown) =>\n (isItxTransportError(error) ? \"strike\" : \"alive\") as \"alive\" | \"strike\",\n ),\n new Promise<\"strike\">((resolve) =>\n setTimeout(() => resolve(\"strike\"), LIVENESS_TIMEOUT_MS),\n ),\n ]);\n // Two-strike: one slow answer is a busy server, not a dead socket.\n let verdict = await probeOnce();\n if (verdict === \"strike\" && current === generation) verdict = await probeOnce();\n if (verdict === \"strike\") reconnectIfCurrent(generation);\n } finally {\n generation.verifying = false;\n }\n}\n\n/**\n * Retire + reconnect the transport, but ONLY if `generation` still owns the slot —\n * the compare-and-swap that stops a late verdict (a probe that timed out on a\n * corpse) from closing the healthy successor another path already connected.\n */\nfunction reconnectIfCurrent(generation: Generation): void {\n if (current !== generation) return;\n current = undefined; // FIRST: the close retireGeneration triggers must not auto-reconnect\n retireGeneration(generation);\n startConnectionAttempt();\n}\n\n/**\n * The SEMANTIC reset (not a transport reconnect): drop the live socket and connection attempt\n * a fresh one so the next reads run under the caller's CURRENT claims. Call\n * after creating a project or unlocking admin — the live socket carries the\n * connect-time principal. React callers that need already-cached data refreshed\n * should also `invalidateQueries({ queryKey: [\"itx\"] })`.\n */\nexport function reconnectIterateSession(): void {\n const generation = current;\n if (generation !== undefined) {\n current = undefined; // FIRST: the close retireGeneration triggers must not auto-reconnect\n retireGeneration(generation);\n }\n // A deliberate reset connects NOW: clear any backoff inherited from earlier\n // closed-before-open failures so the new-claims socket doesn't wait out a\n // transient storm that's already irrelevant.\n consecutiveConnectionFailures = 0;\n startConnectionAttempt();\n}\n\n/**\n * Revive only a generation parked by a terminal authentication failure. This\n * is the imperative retry boundary for query/mutation clients: calling it\n * before a repeated operation lets refreshed credentials dial again without\n * disturbing a healthy or merely reconnecting session.\n */\nexport function retryFailedIterateSession(): void {\n if (current?.failed) reconnectIterateSession();\n}\n\n/**\n * Release the current transport and authority without reconnecting. Intended\n * for process-local lifecycle boundaries such as signing out of a native app;\n * mounted consumers should be removed in the same transition. The explicit\n * deployment configuration remains, so a later connect can dial it again.\n */\nexport function disconnectIterateSession(): void {\n const generation = current;\n current = undefined;\n if (generation !== undefined) retireGeneration(generation);\n const retiredSession = snapshot?.session;\n snapshot = undefined;\n if (retiredSession !== undefined) disposeSession(retiredSession);\n firstConnect?.reject(new Error(\"itx session disconnected\"));\n firstConnect = undefined;\n consecutiveConnectionFailures = 0;\n}\n\n/**\n * The four — and only four — transport-close rejections a caller may treat as\n * \"the socket died, retry on a fresh one\": our own connection-closed rejection, capnweb's\n * two WebSocket aborts (`Peer closed WebSocket: <code> <reason>` and\n * `WebSocket connection failed.`), and capnweb's exact local-shutdown error\n * from our forced bootstrap disposal. Deliberately NARROW: an\n * application/auth/validation error that merely mentions \"WebSocket\" must never\n * be mistaken for a transport failure and retried. This is the one discriminant\n * shared by the query retry, the subscribe retry, and the liveness verifier.\n */\nexport function isItxTransportError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return (\n message.includes(\"itx WebSocket closed before connecting\") ||\n message.includes(\"Peer closed WebSocket\") ||\n message.includes(\"WebSocket connection failed\") ||\n message.includes(\"RPC session was shut down by disposing the main stub\")\n );\n}\n\n// Baseline half-open recovery for the whole browser tab: on the moments\n// transports die (waking a tab, coming back online), verify the current socket.\n// Connection watchdogs recover their own callbacks faster; this\n// covers a query-only page whose socket nobody else probes. Non-browser\n// runtimes have no equivalent signal — their timers carry the whole story.\nif (typeof document !== \"undefined\") {\n // Becoming visible → verify (a tab going hidden shouldn't trigger a probe).\n document.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"visible\") reportTransportSuspicion();\n });\n // Connectivity returning → verify UNCONDITIONALLY: a backgrounded tab must\n // recover a half-open socket when the network comes back, not wait for focus.\n window.addEventListener(\"online\", () => reportTransportSuspicion());\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Push-connection liveness: the handle shape + the watchdog that polls it.\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst PING_TIMED_OUT = Symbol(\"itx-ping-timed-out\");\n\n/**\n * The common lifecycle used by reconnecting stream and live-state clients.\n * Concrete APIs adapt their own handle (`StreamConnectionHandle.close()` or\n * `LiveStateSubscriptionHandle.unsubscribe()`) to `close()`. Real handles are\n * capnweb stubs and therefore Disposable: `close()` closes the server-side\n * resource, while `[Symbol.dispose]` releases the caller-owned stub —\n * on the process-long shared socket, skipping the dispose leaks one\n * import-table entry per subscribe cycle, so holders always do both.\n */\nexport type ItxRecoverableConnectionHandle = {\n ping(): boolean | Promise<boolean>;\n close(): unknown;\n [Symbol.dispose]?(): void;\n};\n\n/**\n * Release a push connection completely: `close()` closes the server side\n * (already-dead is fine — the rejection is swallowed), then\n * `[Symbol.dispose]` frees the caller-owned stub. One helper because\n * forgetting either half leaks (see {@link ItxRecoverableConnectionHandle}).\n */\nexport function releaseItxConnection(handle: ItxRecoverableConnectionHandle): void {\n void Promise.resolve()\n .then(() => handle.close())\n .catch(() => {})\n .finally(() => handle[Symbol.dispose]?.());\n}\n\n/**\n * Poll a push connection's `ping()` until it stops answering `true`, then\n * recover. Server pushes fail SILENTLY: a dead Durable Object or half-open TCP\n * stops delivering with no client-visible signal, so a consumer can show\n * \"live\" forever while stale. The watchdog checks on an interval and — because\n * those are exactly when sockets die — when a browser tab becomes visible or\n * comes online (in runtimes without those signals, the interval carries it).\n *\n * `dead` → ping answered `false` or REJECTED: the socket works but the\n * server-side connection is gone (DO restart / dropped\n * callback). Recovery opens it again on the same socket.\n * `timed-out` → ping never answered: the shared WebSocket is half-open. The\n * watchdog REPORTS the suspicion to the socket-owned verifier\n * ({@link reportTransportSuspicion}) — it never closes the socket\n * itself — and the holder opens again once the generation\n * reconnects.\n */\nexport function watchItxConnection(\n ping: () => boolean | Promise<boolean>,\n onDead: () => void,\n): () => void {\n let stopped = false;\n let checking = false;\n\n // The reason stays internal: on \"timed-out\" the watchdog itself reports the\n // transport suspicion; either way the caller's only move is to open again.\n const report = (reason: \"dead\" | \"timed-out\") => {\n if (stopped) return;\n stop();\n if (reason === \"timed-out\") reportTransportSuspicion();\n onDead();\n };\n\n const pingOnce = () => {\n const timeout = new Promise<typeof PING_TIMED_OUT>((resolve) =>\n setTimeout(() => resolve(PING_TIMED_OUT), LIVENESS_TIMEOUT_MS),\n );\n return Promise.race([Promise.resolve(ping()), timeout]);\n };\n\n const check = async () => {\n if (stopped || checking) return;\n checking = true;\n try {\n let alive: boolean | typeof PING_TIMED_OUT;\n try {\n alive = await pingOnce();\n // One slow answer is a busy/cold DO, not a dead socket — two-strike.\n if (alive === PING_TIMED_OUT && !stopped) alive = await pingOnce();\n } catch {\n report(\"dead\");\n return;\n }\n if (alive === PING_TIMED_OUT) report(\"timed-out\");\n else if (alive !== true) report(\"dead\");\n } finally {\n checking = false;\n }\n };\n\n // Browser wake signals (guarded: node/TUI runs interval-only). Becoming\n // visible → check (going hidden shouldn't). Connectivity returning → check\n // UNCONDITIONALLY, so a backgrounded tab holding a live subscription recovers\n // a half-open socket when the network comes back, not on next focus.\n const inBrowser = typeof document !== \"undefined\";\n const onVisible = () => {\n if (document.visibilityState === \"visible\") void check();\n };\n const onOnline = () => void check();\n const interval = setInterval(() => void check(), LIVENESS_INTERVAL_MS);\n if (inBrowser) {\n document.addEventListener(\"visibilitychange\", onVisible);\n window.addEventListener(\"online\", onOnline);\n }\n\n const stop = () => {\n if (stopped) return;\n stopped = true;\n clearInterval(interval);\n if (inBrowser) {\n document.removeEventListener(\"visibilitychange\", onVisible);\n window.removeEventListener(\"online\", onOnline);\n }\n };\n return stop;\n}\n"],"mappings":"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"}
|