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,205 +0,0 @@
|
|
|
1
|
-
import { F as isStreamIdMismatchError, M as StreamIdMismatchError, d as STREAM_PROCESSOR_REVIVED_EVENT_TYPE, r as ProcessorKeepalive } from "./stream-processor-keepalive-DAQTP6m3.mjs";
|
|
2
|
-
//#region src/processors/durable-object-processor-durability.ts
|
|
3
|
-
/** The two-cursor progress record ({@link ProcessorProgress}). */
|
|
4
|
-
const processorProgressKey = (name) => `stream-processor:${name}:progress`;
|
|
5
|
-
/** The per-runner keepalive record ({@link KeepaliveRecord}). */
|
|
6
|
-
const processorKeepaliveKey = (name) => `stream-processor:${name}:keepalive`;
|
|
7
|
-
function isStreamKeepaliveRecord(value) {
|
|
8
|
-
if (typeof value !== "object" || value === null) return false;
|
|
9
|
-
const candidate = value;
|
|
10
|
-
return Number.isInteger(candidate.revivals) && (candidate.revivals ?? -1) >= 0 && typeof candidate.lastRevivalAt === "number" && Number.isFinite(candidate.lastRevivalAt) && typeof candidate.version === "string" && candidate.version.trim().length > 0 && (candidate.armedAtMs === null || typeof candidate.armedAtMs === "number" && Number.isFinite(candidate.armedAtMs)) && typeof candidate.streamId === "string" && candidate.streamId.trim().length > 0;
|
|
11
|
-
}
|
|
12
|
-
function sameKeepaliveAttempt(stored, streamId, attempt) {
|
|
13
|
-
return stored.streamId === streamId && stored.revivals === attempt.revivals && stored.lastRevivalAt === attempt.lastRevivalAt && stored.version === attempt.version && stored.armedAtMs === attempt.armedAtMs;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* The runner's durable progress store over DO KV (`storage.kv` — synchronous,
|
|
17
|
-
* single-threaded isolate, so read-check-write is atomic without awaits).
|
|
18
|
-
*/
|
|
19
|
-
function durableObjectProgressStore(args) {
|
|
20
|
-
const { storage, name } = args;
|
|
21
|
-
const progressKey = processorProgressKey(name);
|
|
22
|
-
const cachedProgress = (progress) => {
|
|
23
|
-
const reductionCache = args.reductionCache;
|
|
24
|
-
if (!reductionCache || reductionCache.shouldCacheReduction(progress.reduction.state)) return progress;
|
|
25
|
-
return {
|
|
26
|
-
...progress,
|
|
27
|
-
reduction: {
|
|
28
|
-
...progress.reduction,
|
|
29
|
-
reducedThroughOffset: 0,
|
|
30
|
-
state: reductionCache.initialState()
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
return {
|
|
35
|
-
read: () => storage.kv.get(progressKey),
|
|
36
|
-
commit: (progress, opts) => {
|
|
37
|
-
const persisted = storage.kv.get(progressKey);
|
|
38
|
-
if (persisted?.streamId !== opts.expectedStreamId) throw new Error(`stream processor "${name}" progress commit fenced: expected stream ID ${String(opts.expectedStreamId)}, persisted ${String(persisted?.streamId)} — the stream lifetime changed after this continuation began`);
|
|
39
|
-
const persistedRevision = persisted?.processing.cursorRevision ?? 0;
|
|
40
|
-
if (opts.expectedCursorRevision !== persistedRevision) throw new Error(`stream processor "${name}" progress commit fenced: expected cursorRevision ${opts.expectedCursorRevision}, persisted ${persistedRevision} — a cursor rewind landed after this continuation began`);
|
|
41
|
-
if (persisted !== void 0 && progress.processing.acknowledgedThroughOffset < persisted.processing.acknowledgedThroughOffset && progress.processing.cursorRevision <= persistedRevision) throw new Error(`stream processor "${name}" progress commit fenced: acknowledgedThroughOffset would move backward (${persisted.processing.acknowledgedThroughOffset} -> ${progress.processing.acknowledgedThroughOffset}) without a cursorRevision bump — a stale incarnation is rolling the cursor back`);
|
|
42
|
-
storage.kv.put(progressKey, cachedProgress(progress));
|
|
43
|
-
},
|
|
44
|
-
replaceForStream: (progress, opts) => {
|
|
45
|
-
const persisted = storage.kv.get(progressKey);
|
|
46
|
-
if (persisted?.streamId !== opts.expectedStreamId || persisted.processing.cursorRevision !== opts.expectedCursorRevision) throw new Error(`stream processor "${name}" stream replacement fenced: expected ${opts.expectedStreamId}@${opts.expectedCursorRevision}, persisted ${String(persisted?.streamId)}@${persisted?.processing.cursorRevision ?? 0}`);
|
|
47
|
-
args.resetForStream?.();
|
|
48
|
-
storage.kv.delete(processorKeepaliveKey(name));
|
|
49
|
-
storage.kv.put(progressKey, cachedProgress(progress));
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* The runner's recovery adapter for a Durable Object: wraps ONE
|
|
55
|
-
* {@link ProcessorKeepalive} for THIS runner (per-runner recovery identity, so
|
|
56
|
-
* a revival names exactly which processor owed work). The keepalive machinery
|
|
57
|
-
* — mark-before-work, bounded backoff, quiet-clean reset,
|
|
58
|
-
* deploy-version reset, wedged-work detection — is reused wholesale, never
|
|
59
|
-
* reimplemented.
|
|
60
|
-
*
|
|
61
|
-
* DO-shaped seams are INJECTED, not reached for:
|
|
62
|
-
* - `armAlarm` — the hosting registry's alarm slice for this runner. A DO
|
|
63
|
-
* has ONE alarm; the registry merges every runner's desire (plus its own)
|
|
64
|
-
* and arms the earliest, exactly like the host's `setAlarmSlice`. This
|
|
65
|
-
* adapter never touches `storage.setAlarm`.
|
|
66
|
-
* - `waitUntil` — calls the hosting DO's `ctx.waitUntil`, keeping the
|
|
67
|
-
* incarnation alive while tracked work runs.
|
|
68
|
-
*
|
|
69
|
-
* Revival appends the core `stream/processor-revived` fact (ONE type for
|
|
70
|
-
* every processor — {@link STREAM_PROCESSOR_REVIVED_EVENT_TYPE}; the payload's
|
|
71
|
-
* `processorSlug` and the idempotency key carry the per-processor identity)
|
|
72
|
-
* to the stream and STOPS: the append wakes the source stream's event sender (its
|
|
73
|
-
* `woken` handlers cold-boot the stream DO if the deploy evicted it too), and
|
|
74
|
-
* wake-mode delivery reaches head and guarantees a turn: either the contract
|
|
75
|
-
* consumes the fact and receives it, or the runner supplies its eventless
|
|
76
|
-
* `processEvent(event: null, caughtUp: true)` pass. No self-driven catch-up
|
|
77
|
-
* here, unlike the host's `catchUpInternal` loop: delivery has ONE entrypoint.
|
|
78
|
-
*
|
|
79
|
-
* Construction re-issues a persisted armed desire through `armAlarm` (the
|
|
80
|
-
* host's boot-time reconcile): a platform `setAlarm` that failed after the KV
|
|
81
|
-
* record committed — or an eviction in the fire→re-arm window — would
|
|
82
|
-
* otherwise leave the only thing that revives this DO permanently lost.
|
|
83
|
-
*/
|
|
84
|
-
function durableObjectRecovery(args) {
|
|
85
|
-
const now = args.now ?? (() => Date.now());
|
|
86
|
-
const recordKey = processorKeepaliveKey(args.name);
|
|
87
|
-
const progressKey = processorProgressKey(args.name);
|
|
88
|
-
const readProgress = () => args.storage.kv.get(progressKey);
|
|
89
|
-
const discardStoredRecord = (reason) => {
|
|
90
|
-
args.storage.kv.delete(recordKey);
|
|
91
|
-
args.armAlarm(null);
|
|
92
|
-
console.warn(`stream processor "${args.name}" discarded its recovery record: ${reason}`);
|
|
93
|
-
};
|
|
94
|
-
const readStoredRecord = (context) => {
|
|
95
|
-
const value = args.storage.kv.get(recordKey);
|
|
96
|
-
if (value === void 0) return void 0;
|
|
97
|
-
if (!isStreamKeepaliveRecord(value)) {
|
|
98
|
-
discardStoredRecord(`invalid record found while ${context}`);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
};
|
|
103
|
-
const requireProgressStreamId = () => {
|
|
104
|
-
const streamId = readProgress()?.streamId;
|
|
105
|
-
if (streamId === void 0 || streamId.trim().length === 0) throw new Error(`stream processor "${args.name}" cannot arm recovery before its stream lifetime is bound`);
|
|
106
|
-
return streamId;
|
|
107
|
-
};
|
|
108
|
-
const assertProgressStreamId = (expectedStreamId) => {
|
|
109
|
-
const currentStreamId = readProgress()?.streamId;
|
|
110
|
-
if (currentStreamId !== expectedStreamId) throw new StreamIdMismatchError(`stream processor "${args.name}" recovery belongs to stream ID ${expectedStreamId}, but current progress belongs to ${String(currentStreamId)}`);
|
|
111
|
-
};
|
|
112
|
-
const readCurrentStoredRecord = (context) => {
|
|
113
|
-
const record = readStoredRecord(context);
|
|
114
|
-
if (record === void 0) return void 0;
|
|
115
|
-
const progressStreamId = readProgress()?.streamId;
|
|
116
|
-
if (progressStreamId !== record.streamId) {
|
|
117
|
-
discardStoredRecord(`record belongs to stream ID ${record.streamId}, current progress belongs to ${String(progressStreamId)}`);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
return record;
|
|
121
|
-
};
|
|
122
|
-
const appendRevived = async (streamId, record) => {
|
|
123
|
-
assertProgressStreamId(streamId);
|
|
124
|
-
await args.stream.appendIfStreamId({
|
|
125
|
-
streamId,
|
|
126
|
-
events: [{
|
|
127
|
-
type: STREAM_PROCESSOR_REVIVED_EVENT_TYPE,
|
|
128
|
-
idempotencyKey: `processor-revived:${args.name}@${record.version}:${record.revivals}:${record.lastRevivalAt}`,
|
|
129
|
-
payload: {
|
|
130
|
-
processorSlug: args.name,
|
|
131
|
-
revivals: record.revivals,
|
|
132
|
-
version: record.version
|
|
133
|
-
}
|
|
134
|
-
}]
|
|
135
|
-
});
|
|
136
|
-
};
|
|
137
|
-
let activeKeepalive;
|
|
138
|
-
const keepaliveFor = (streamId) => {
|
|
139
|
-
if (activeKeepalive?.streamId === streamId) return activeKeepalive.keepalive;
|
|
140
|
-
const readRecord = () => {
|
|
141
|
-
const record = readCurrentStoredRecord("reading it");
|
|
142
|
-
return record?.streamId === streamId ? record : void 0;
|
|
143
|
-
};
|
|
144
|
-
const keepalive = new ProcessorKeepalive({
|
|
145
|
-
now,
|
|
146
|
-
readRecord,
|
|
147
|
-
writeRecord: (record) => {
|
|
148
|
-
assertProgressStreamId(streamId);
|
|
149
|
-
const persisted = readStoredRecord("arming recovery");
|
|
150
|
-
if (persisted !== void 0 && persisted.streamId !== streamId) throw new StreamIdMismatchError(`stream processor "${args.name}" cannot arm recovery for stream ID ${streamId} over the record for ${persisted.streamId}`);
|
|
151
|
-
args.storage.kv.put(recordKey, {
|
|
152
|
-
...record,
|
|
153
|
-
streamId
|
|
154
|
-
});
|
|
155
|
-
},
|
|
156
|
-
armAlarm: (atMs) => args.armAlarm(atMs),
|
|
157
|
-
keepAlive: (work) => args.waitUntil(work),
|
|
158
|
-
revive: (record) => appendRevived(streamId, record),
|
|
159
|
-
discardFailedRevival: (error, record) => {
|
|
160
|
-
if (!isStreamIdMismatchError(error)) return false;
|
|
161
|
-
const stored = readStoredRecord("discarding a stale revival");
|
|
162
|
-
if (stored === void 0) args.armAlarm(null);
|
|
163
|
-
else if (sameKeepaliveAttempt(stored, streamId, record)) discardStoredRecord(`stream ID ${streamId} was replaced`);
|
|
164
|
-
return true;
|
|
165
|
-
},
|
|
166
|
-
appendFact: (event) => {
|
|
167
|
-
Promise.resolve().then(() => {
|
|
168
|
-
assertProgressStreamId(streamId);
|
|
169
|
-
return args.stream.appendIfStreamId({
|
|
170
|
-
streamId,
|
|
171
|
-
events: [event]
|
|
172
|
-
});
|
|
173
|
-
}).catch((error) => {
|
|
174
|
-
console.error(`stream processor "${args.name}" keepalive evidence append failed`, error);
|
|
175
|
-
});
|
|
176
|
-
},
|
|
177
|
-
version: args.version
|
|
178
|
-
});
|
|
179
|
-
activeKeepalive = {
|
|
180
|
-
streamId,
|
|
181
|
-
keepalive
|
|
182
|
-
};
|
|
183
|
-
return keepalive;
|
|
184
|
-
};
|
|
185
|
-
const recovery = {
|
|
186
|
-
keepAliveWhile: (work) => keepaliveFor(requireProgressStreamId()).track(work()),
|
|
187
|
-
handleAlarm: async () => {
|
|
188
|
-
const record = readCurrentStoredRecord("handling an alarm");
|
|
189
|
-
if (record === void 0) return;
|
|
190
|
-
await keepaliveFor(record.streamId).onAlarm();
|
|
191
|
-
},
|
|
192
|
-
resetBackoff: () => {
|
|
193
|
-
const record = readCurrentStoredRecord("resetting the backoff");
|
|
194
|
-
if (record === void 0) return;
|
|
195
|
-
keepaliveFor(record.streamId).resetBackoff();
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
const persisted = readCurrentStoredRecord("booting");
|
|
199
|
-
if (persisted !== void 0 && persisted.armedAtMs !== null) args.armAlarm(persisted.armedAtMs);
|
|
200
|
-
return recovery;
|
|
201
|
-
}
|
|
202
|
-
//#endregion
|
|
203
|
-
export { processorProgressKey as i, durableObjectRecovery as n, processorKeepaliveKey as r, durableObjectProgressStore as t };
|
|
204
|
-
|
|
205
|
-
//# sourceMappingURL=durable-object-processor-durability-CNsTjAJS.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"durable-object-processor-durability-CNsTjAJS.mjs","names":[],"sources":["../src/processors/durable-object-processor-durability.ts"],"sourcesContent":["// The Cloudflare Durable Object implementations of the StreamProcessorRunner's\n// two durability adapters (stream-processor-runner.ts):\n//\n// - `durableObjectProgressStore` — the CAS-fenced two-cursor\n// {@link ProcessorProgressStore} over the DO's synchronous KV facade.\n// - `durableObjectRecovery` — the {@link ProcessorRecovery} adapter for a\n// durable processor that owns background work: ONE ProcessorKeepalive per\n// runner, storage-backed per registered NAME, with the DO-shaped seams\n// (alarm slice, waitUntil) INJECTED so nothing here touches\n// `storage.setAlarm` or a Cloudflare ctx directly.\n//\n// This file is deliberately runtime-light so its tests run in plain-node\n// vitest over an in-memory `storage.kv` fake\n// (durable-object-processor-durability.test.ts).\n\nimport type { ProcessorStream } from \"./stream-handle.ts\";\nimport { STREAM_PROCESSOR_REVIVED_EVENT_TYPE } from \"./processor-contracts.ts\";\nimport { isStreamIdMismatchError, StreamIdMismatchError } from \"./rpc-types.ts\";\nimport { ProcessorKeepalive, type KeepaliveRecord } from \"./stream-processor-keepalive.ts\";\nimport type {\n ProcessorProgress,\n ProcessorProgressStore,\n ProcessorRecovery,\n} from \"./stream-processor-runner.ts\";\n\n// -----------------------------------------------------------------------------\n// Key layout. Per registered NAME (the subscription name, which equals the\n// contract slug — one identity), all under the `stream-processor:` prefix.\n// -----------------------------------------------------------------------------\n\n/** The two-cursor progress record ({@link ProcessorProgress}). */\nexport const processorProgressKey = (name: string) => `stream-processor:${name}:progress`;\n\n/** The per-runner keepalive record ({@link KeepaliveRecord}). */\nexport const processorKeepaliveKey = (name: string) => `stream-processor:${name}:keepalive`;\n\n/** A recovery alarm belongs to one stream lifetime, not merely one path. */\ntype StreamKeepaliveRecord = KeepaliveRecord & { streamId: string };\n\nfunction isStreamKeepaliveRecord(value: unknown): value is StreamKeepaliveRecord {\n if (typeof value !== \"object\" || value === null) return false;\n const candidate = value as Partial<StreamKeepaliveRecord>;\n return (\n Number.isInteger(candidate.revivals) &&\n (candidate.revivals ?? -1) >= 0 &&\n typeof candidate.lastRevivalAt === \"number\" &&\n Number.isFinite(candidate.lastRevivalAt) &&\n typeof candidate.version === \"string\" &&\n candidate.version.trim().length > 0 &&\n (candidate.armedAtMs === null ||\n (typeof candidate.armedAtMs === \"number\" && Number.isFinite(candidate.armedAtMs))) &&\n typeof candidate.streamId === \"string\" &&\n candidate.streamId.trim().length > 0\n );\n}\n\nfunction sameKeepaliveAttempt(\n stored: StreamKeepaliveRecord,\n streamId: string,\n attempt: KeepaliveRecord,\n): boolean {\n return (\n stored.streamId === streamId &&\n stored.revivals === attempt.revivals &&\n stored.lastRevivalAt === attempt.lastRevivalAt &&\n stored.version === attempt.version &&\n stored.armedAtMs === attempt.armedAtMs\n );\n}\n\n/**\n * The runner's durable progress store over DO KV (`storage.kv` — synchronous,\n * single-threaded isolate, so read-check-write is atomic without awaits).\n */\nexport function durableObjectProgressStore<State>(args: {\n storage: DurableObjectStorage;\n /** The registered processor name (subscription name = contract slug) —\n * keys the progress record. */\n name: string;\n /** Synchronously clear related projections when the source stream is replaced. */\n resetForStream?: () => void;\n /**\n * An optional bounded reduction cache. Processing cursors always remain\n * durable; a cold runner refolds reduce-only from initialState when the\n * predicate declines a large cache.\n */\n reductionCache?: {\n shouldCacheReduction(state: State): boolean;\n initialState(): State;\n };\n}): ProcessorProgressStore<State> {\n const { storage, name } = args;\n const progressKey = processorProgressKey(name);\n const cachedProgress = (progress: ProcessorProgress<State>): ProcessorProgress<State> => {\n const reductionCache = args.reductionCache;\n if (!reductionCache || reductionCache.shouldCacheReduction(progress.reduction.state))\n return progress;\n return {\n ...progress,\n reduction: {\n ...progress.reduction,\n reducedThroughOffset: 0,\n state: reductionCache.initialState(),\n },\n };\n };\n\n return {\n read: () => storage.kv.get<ProcessorProgress<State>>(progressKey),\n commit: (progress, opts) => {\n // CAS fence: read-check-write with NO intervening awaits — DO storage is\n // synchronous and the isolate single-threaded, so this whole block is\n // atomic. An absent record reads as revision 0.\n const persisted = storage.kv.get<ProcessorProgress<State>>(progressKey);\n if (persisted?.streamId !== opts.expectedStreamId) {\n throw new Error(\n `stream processor \"${name}\" progress commit fenced: expected stream ID ` +\n `${String(opts.expectedStreamId)}, persisted ${String(persisted?.streamId)} — ` +\n `the stream lifetime changed after this continuation began`,\n );\n }\n const persistedRevision = persisted?.processing.cursorRevision ?? 0;\n if (opts.expectedCursorRevision !== persistedRevision) {\n throw new Error(\n `stream processor \"${name}\" progress commit fenced: expected cursorRevision ` +\n `${opts.expectedCursorRevision}, persisted ${persistedRevision} — ` +\n `a cursor rewind landed after this continuation began`,\n );\n }\n // MONOTONIC fence: the revision CAS alone cannot stop a stale\n // incarnation at the SAME revision from rolling acknowledgement (and\n // state) backward past progress a newer incarnation already committed —\n // re-running effects that were durably acknowledged. Only an explicit\n // revision-bumping cursor rewind may move acked backward.\n if (\n persisted !== undefined &&\n progress.processing.acknowledgedThroughOffset <\n persisted.processing.acknowledgedThroughOffset &&\n progress.processing.cursorRevision <= persistedRevision\n ) {\n throw new Error(\n `stream processor \"${name}\" progress commit fenced: acknowledgedThroughOffset would ` +\n `move backward (${persisted.processing.acknowledgedThroughOffset} -> ` +\n `${progress.processing.acknowledgedThroughOffset}) without a cursorRevision bump — ` +\n `a stale incarnation is rolling the cursor back`,\n );\n }\n storage.kv.put(progressKey, cachedProgress(progress));\n },\n replaceForStream: (progress, opts) => {\n const persisted = storage.kv.get<ProcessorProgress<State>>(progressKey);\n if (\n persisted?.streamId !== opts.expectedStreamId ||\n persisted.processing.cursorRevision !== opts.expectedCursorRevision\n ) {\n throw new Error(\n `stream processor \"${name}\" stream replacement fenced: expected ` +\n `${opts.expectedStreamId}@${opts.expectedCursorRevision}, persisted ` +\n `${String(persisted?.streamId)}@${persisted?.processing.cursorRevision ?? 0}`,\n );\n }\n // Old-lifetime recovery desires must not append revival facts into the\n // recreated stream. A new obligation will arm a fresh record.\n args.resetForStream?.();\n storage.kv.delete(processorKeepaliveKey(name));\n storage.kv.put(progressKey, cachedProgress(progress));\n },\n };\n}\n\n/**\n * The runner's recovery adapter for a Durable Object: wraps ONE\n * {@link ProcessorKeepalive} for THIS runner (per-runner recovery identity, so\n * a revival names exactly which processor owed work). The keepalive machinery\n * — mark-before-work, bounded backoff, quiet-clean reset,\n * deploy-version reset, wedged-work detection — is reused wholesale, never\n * reimplemented.\n *\n * DO-shaped seams are INJECTED, not reached for:\n * - `armAlarm` — the hosting registry's alarm slice for this runner. A DO\n * has ONE alarm; the registry merges every runner's desire (plus its own)\n * and arms the earliest, exactly like the host's `setAlarmSlice`. This\n * adapter never touches `storage.setAlarm`.\n * - `waitUntil` — calls the hosting DO's `ctx.waitUntil`, keeping the\n * incarnation alive while tracked work runs.\n *\n * Revival appends the core `stream/processor-revived` fact (ONE type for\n * every processor — {@link STREAM_PROCESSOR_REVIVED_EVENT_TYPE}; the payload's\n * `processorSlug` and the idempotency key carry the per-processor identity)\n * to the stream and STOPS: the append wakes the source stream's event sender (its\n * `woken` handlers cold-boot the stream DO if the deploy evicted it too), and\n * wake-mode delivery reaches head and guarantees a turn: either the contract\n * consumes the fact and receives it, or the runner supplies its eventless\n * `processEvent(event: null, caughtUp: true)` pass. No self-driven catch-up\n * here, unlike the host's `catchUpInternal` loop: delivery has ONE entrypoint.\n *\n * Construction re-issues a persisted armed desire through `armAlarm` (the\n * host's boot-time reconcile): a platform `setAlarm` that failed after the KV\n * record committed — or an eviction in the fire→re-arm window — would\n * otherwise leave the only thing that revives this DO permanently lost.\n */\nexport function durableObjectRecovery(args: {\n storage: DurableObjectStorage;\n /** The registered processor name (subscription name = contract slug) —\n * keys the per-runner keepalive record and the revival fact's idempotency\n * key, and fills the revival payload's `processorSlug`. */\n name: string;\n /** The processor's home stream: revived facts and crash-loop evidence land here. */\n stream: ProcessorStream;\n /** Worker deploy version; a change resets the keepalive's crash-loop budget\n * (the antidote deploy). Pass `workerVersion(env)`. REQUIRED for the same\n * reason the host requires it: a silent default could never take the\n * version-reset code path. */\n version: string;\n /** The registry's alarm-slice seam for this runner (null = disarm). */\n armAlarm: (atMs: number | null) => void;\n /** The hosting DO's `ctx.waitUntil` — keeps the incarnation alive while\n * tracked work runs. */\n waitUntil: (work: Promise<unknown>) => void;\n /** Injected clock for the test harness; production uses Date.now. */\n now?: () => number;\n}): ProcessorRecovery {\n const now = args.now ?? (() => Date.now());\n const recordKey = processorKeepaliveKey(args.name);\n const progressKey = processorProgressKey(args.name);\n const readProgress = () => args.storage.kv.get<ProcessorProgress<unknown>>(progressKey);\n\n const discardStoredRecord = (reason: string): void => {\n args.storage.kv.delete(recordKey);\n args.armAlarm(null);\n console.warn(`stream processor \"${args.name}\" discarded its recovery record: ${reason}`);\n };\n\n const readStoredRecord = (context: string): StreamKeepaliveRecord | undefined => {\n const value = args.storage.kv.get<unknown>(recordKey);\n if (value === undefined) return undefined;\n if (!isStreamKeepaliveRecord(value)) {\n discardStoredRecord(`invalid record found while ${context}`);\n return undefined;\n }\n return value;\n };\n\n const requireProgressStreamId = (): string => {\n const streamId = readProgress()?.streamId;\n if (streamId === undefined || streamId.trim().length === 0) {\n throw new Error(\n `stream processor \"${args.name}\" cannot arm recovery before its stream lifetime is bound`,\n );\n }\n return streamId;\n };\n\n const assertProgressStreamId = (expectedStreamId: string): void => {\n const currentStreamId = readProgress()?.streamId;\n if (currentStreamId !== expectedStreamId) {\n throw new StreamIdMismatchError(\n `stream processor \"${args.name}\" recovery belongs to stream ID ${expectedStreamId}, ` +\n `but current progress belongs to ${String(currentStreamId)}`,\n );\n }\n };\n\n const readCurrentStoredRecord = (context: string): StreamKeepaliveRecord | undefined => {\n const record = readStoredRecord(context);\n if (record === undefined) return undefined;\n const progressStreamId = readProgress()?.streamId;\n if (progressStreamId !== record.streamId) {\n discardStoredRecord(\n `record belongs to stream ID ${record.streamId}, current progress belongs to ${String(\n progressStreamId,\n )}`,\n );\n return undefined;\n }\n return record;\n };\n\n const appendRevived = async (streamId: string, record: KeepaliveRecord): Promise<void> => {\n // Check both sides of the RPC boundary. The progress check stops an old\n // local continuation immediately; appendIfStreamId closes the remaining\n // race if the path is deleted and recreated while the RPC is in flight.\n assertProgressStreamId(streamId);\n await args.stream.appendIfStreamId({\n streamId,\n events: [\n {\n type: STREAM_PROCESSOR_REVIVED_EVENT_TYPE,\n idempotencyKey:\n `processor-revived:${args.name}` +\n `@${record.version}:${record.revivals}:${record.lastRevivalAt}`,\n payload: {\n // The registered name IS the contract slug (one identity).\n processorSlug: args.name,\n revivals: record.revivals,\n version: record.version,\n },\n },\n ],\n });\n };\n\n let activeKeepalive: { streamId: string; keepalive: ProcessorKeepalive } | undefined;\n\n const keepaliveFor = (streamId: string): ProcessorKeepalive => {\n if (activeKeepalive?.streamId === streamId) return activeKeepalive.keepalive;\n\n const readRecord = (): KeepaliveRecord | undefined => {\n const record = readCurrentStoredRecord(\"reading it\");\n return record?.streamId === streamId ? record : undefined;\n };\n const keepalive = new ProcessorKeepalive({\n now,\n readRecord,\n writeRecord: (record) => {\n assertProgressStreamId(streamId);\n const persisted = readStoredRecord(\"arming recovery\");\n if (persisted !== undefined && persisted.streamId !== streamId) {\n throw new StreamIdMismatchError(\n `stream processor \"${args.name}\" cannot arm recovery for stream ID ${streamId} ` +\n `over the record for ${persisted.streamId}`,\n );\n }\n args.storage.kv.put(recordKey, { ...record, streamId } satisfies StreamKeepaliveRecord);\n },\n armAlarm: (atMs) => args.armAlarm(atMs),\n keepAlive: (work) => args.waitUntil(work),\n // Append the journaled fact and stop — the wake delivery of that fact is\n // the recovery turn. Failures throw: the keepalive's breaker owns retries.\n revive: (record) => appendRevived(streamId, record),\n // A stream ID never becomes current again. Retrying its revival would\n // wake forever, so discard exactly this attempt. If a newer lifetime or\n // attempt already replaced the record, leave its alarm desire untouched.\n discardFailedRevival: (error, record) => {\n if (!isStreamIdMismatchError(error)) return false;\n const stored = readStoredRecord(\"discarding a stale revival\");\n if (stored === undefined) {\n args.armAlarm(null);\n } else if (sameKeepaliveAttempt(stored, streamId, record)) {\n discardStoredRecord(`stream ID ${streamId} was replaced`);\n }\n return true;\n },\n // Best-effort journal evidence (crash-loop warnings). The stream-ID\n // guard prevents an old lifetime's evidence from landing in its replacement.\n appendFact: (event) => {\n void Promise.resolve()\n .then(() => {\n assertProgressStreamId(streamId);\n return args.stream.appendIfStreamId({ streamId, events: [event] });\n })\n .catch((error: unknown) => {\n console.error(\n `stream processor \"${args.name}\" keepalive evidence append failed`,\n error,\n );\n });\n },\n version: args.version,\n });\n activeKeepalive = { streamId, keepalive };\n return keepalive;\n };\n\n const recovery: ProcessorRecovery = {\n // The host's wiring verbatim (stream-processor-host.ts:445): every\n // registered closure — blocking, background, and the runner's whole-batch\n // attempt — rides the keepalive, so \"the DO died owing work\" is exactly\n // \"the DO died with the alarm armed\".\n keepAliveWhile: (work) => keepaliveFor(requireProgressStreamId()).track(work()),\n handleAlarm: async () => {\n // Delegation IS the whole handler: every alarm action drives the\n // injected seams from inside onAlarm before it returns — busy_rearmed\n // re-arms at the lead, clean_disarmed disarms via armAlarm(null), the\n // revival outcomes record + arm the backoff and run the revive hook (which\n // appends the revived fact). The keepalive self-gates on its persisted\n // armed time, so a fire belonging to another slice of the shared DO\n // alarm is a no-op here — route every fire to every runner.\n const record = readCurrentStoredRecord(\"handling an alarm\");\n if (record === undefined) return;\n await keepaliveFor(record.streamId).onAlarm();\n },\n // The operator's no-deploy antidote for a 3-strikes plateau: clear the\n // crash-loop budget and pull the owed retry to the lead. Delegates\n // wholesale to the keepalive (the budget's one owner).\n resetBackoff: () => {\n const record = readCurrentStoredRecord(\"resetting the backoff\");\n if (record === undefined) return;\n keepaliveFor(record.streamId).resetBackoff();\n },\n };\n\n // Boot-time reconcile (the host's post-construction block verbatim): a\n // fresh incarnation restores — and RE-ISSUES — the persisted alarm desire,\n // so a lost platform alarm heals when the host next opens instead of never.\n const persisted = readCurrentStoredRecord(\"booting\");\n if (persisted !== undefined && persisted.armedAtMs !== null) {\n args.armAlarm(persisted.armedAtMs);\n }\n\n return recovery;\n}\n"],"mappings":"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"}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
//#region src/processors/idempotency.ts
|
|
2
|
-
const IDEMPOTENCY_CONFLICT_FRAGMENT = " already names a different event at offset ";
|
|
3
|
-
function idempotencyConflictMessage(idempotencyKey, existingOffset) {
|
|
4
|
-
return `idempotency key "${idempotencyKey}"${IDEMPOTENCY_CONFLICT_FRAGMENT}${existingOffset}`;
|
|
5
|
-
}
|
|
6
|
-
function isIdempotencyConflict(error) {
|
|
7
|
-
return (error instanceof Error ? error.message : String(error)).includes(IDEMPOTENCY_CONFLICT_FRAGMENT);
|
|
8
|
-
}
|
|
9
|
-
/** Whether a requested append names the SAME event an idempotency key already committed. */
|
|
10
|
-
function sameIdempotentEvent(existing, requested) {
|
|
11
|
-
return existing.type === requested.type && jsonValuesEqual(existing.payload, requested.payload) && jsonValuesEqual(existing.metadata, requested.metadata) && existing.ephemeral === requested.ephemeral;
|
|
12
|
-
}
|
|
13
|
-
/** Structural JSON equality (key-order-insensitive). */
|
|
14
|
-
function jsonValuesEqual(left, right) {
|
|
15
|
-
if (Object.is(left, right)) return true;
|
|
16
|
-
if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => jsonValuesEqual(value, right[index]));
|
|
17
|
-
if (typeof left === "object" && typeof right === "object" && left !== null && right !== null) {
|
|
18
|
-
const leftKeys = Object.keys(left).sort();
|
|
19
|
-
const rightKeys = Object.keys(right).sort();
|
|
20
|
-
if (leftKeys.length !== rightKeys.length) return false;
|
|
21
|
-
return leftKeys.every((key, index) => key === rightKeys[index] && jsonValuesEqual(left[key], right[key]));
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
//#endregion
|
|
26
|
-
export { sameIdempotentEvent as i, isIdempotencyConflict as n, jsonValuesEqual as r, idempotencyConflictMessage as t };
|
|
27
|
-
|
|
28
|
-
//# sourceMappingURL=idempotency-DleloJNt.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"idempotency-DleloJNt.mjs","names":[],"sources":["../src/processors/idempotency.ts"],"sourcesContent":["// The Stream DO's idempotency semantics, importable: a same-key append is a\n// dedup ONLY when its body is structurally identical — otherwise it must be\n// rejected. Lives here so the production Stream Durable Object and the\n// MemoryStream test double share ONE predicate and cannot drift.\n\ntype EventBody = {\n type: string;\n payload?: unknown;\n metadata?: unknown;\n ephemeral?: boolean | undefined;\n};\n\n// The wording is a live wire contract: the rejection crosses Workers RPC,\n// which preserves the message but not class identity, so deployed processors\n// classify the conflict by this text. Mint with the builder, match with the\n// predicate — never spell the wording anywhere else.\nconst IDEMPOTENCY_CONFLICT_FRAGMENT = \" already names a different event at offset \";\n\nexport function idempotencyConflictMessage(idempotencyKey: string, existingOffset: number) {\n return `idempotency key \"${idempotencyKey}\"${IDEMPOTENCY_CONFLICT_FRAGMENT}${existingOffset}`;\n}\n\nexport function isIdempotencyConflict(error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n return message.includes(IDEMPOTENCY_CONFLICT_FRAGMENT);\n}\n\n/** Whether a requested append names the SAME event an idempotency key already committed. */\nexport function sameIdempotentEvent(existing: EventBody, requested: EventBody): boolean {\n return (\n existing.type === requested.type &&\n jsonValuesEqual(existing.payload, requested.payload) &&\n jsonValuesEqual(existing.metadata, requested.metadata) &&\n existing.ephemeral === requested.ephemeral\n );\n}\n\n/** Structural JSON equality (key-order-insensitive). */\nexport function jsonValuesEqual(left: unknown, right: unknown): boolean {\n if (Object.is(left, right)) return true;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => jsonValuesEqual(value, right[index]))\n );\n }\n if (typeof left === \"object\" && typeof right === \"object\" && left !== null && right !== null) {\n const leftKeys = Object.keys(left).sort();\n const rightKeys = Object.keys(right).sort();\n if (leftKeys.length !== rightKeys.length) return false;\n return leftKeys.every(\n (key, index) =>\n key === rightKeys[index] &&\n jsonValuesEqual(\n (left as Record<string, unknown>)[key],\n (right as Record<string, unknown>)[key],\n ),\n );\n }\n return false;\n}\n"],"mappings":";AAgBA,MAAM,gCAAgC;AAEtC,SAAgB,2BAA2B,gBAAwB,gBAAwB;CACzF,OAAO,oBAAoB,eAAe,GAAG,gCAAgC;AAC/E;AAEA,SAAgB,sBAAsB,OAAgB;CAEpD,QADgB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAA,CACtD,SAAS,6BAA6B;AACvD;;AAGA,SAAgB,oBAAoB,UAAqB,WAA+B;CACtF,OACE,SAAS,SAAS,UAAU,QAC5B,gBAAgB,SAAS,SAAS,UAAU,OAAO,KACnD,gBAAgB,SAAS,UAAU,UAAU,QAAQ,KACrD,SAAS,cAAc,UAAU;AAErC;;AAGA,SAAgB,gBAAgB,MAAe,OAAyB;CACtE,IAAI,OAAO,GAAG,MAAM,KAAK,GAAG,OAAO;CACnC,IAAI,MAAM,QAAQ,IAAI,KAAK,MAAM,QAAQ,KAAK,GAC5C,OACE,MAAM,QAAQ,IAAI,KAClB,MAAM,QAAQ,KAAK,KACnB,KAAK,WAAW,MAAM,UACtB,KAAK,OAAO,OAAO,UAAU,gBAAgB,OAAO,MAAM,MAAM,CAAC;CAGrE,IAAI,OAAO,SAAS,YAAY,OAAO,UAAU,YAAY,SAAS,QAAQ,UAAU,MAAM;EAC5F,MAAM,WAAW,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK;EACxC,MAAM,YAAY,OAAO,KAAK,KAAK,CAAC,CAAC,KAAK;EAC1C,IAAI,SAAS,WAAW,UAAU,QAAQ,OAAO;EACjD,OAAO,SAAS,OACb,KAAK,UACJ,QAAQ,UAAU,UAClB,gBACG,KAAiC,MACjC,MAAkC,IACrC,CACJ;CACF;CACA,OAAO;AACT"}
|
package/dist/index.d.mts
DELETED
package/dist/index.mjs
DELETED
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export async function runCli() {\n const cli = await import(\"./cli.ts\");\n await cli.runCli();\n}\n"],"mappings":";AAAA,eAAsB,SAAS;CAE7B,OAAM,MADY,OAAO,sBAAA,CACf,OAAO;AACnB"}
|
package/dist/itx/api-url.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An OS deployment's `/api` WebSocket URL from its http(s) base — the ONE
|
|
3
|
-
* place that knows the path and the ws/wss protocol swap, shared by the
|
|
4
|
-
* shared session and the one-shot Node client.
|
|
5
|
-
*/
|
|
6
|
-
export declare function apiWebSocketUrl(baseUrl: string): URL;
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { type RpcStub as CapnRpcStub } from "@iterate-com/capnweb";
|
|
2
|
-
import type { Agent, ItxAuthCredentials, Project, Session, UnauthenticatedOs } from "../itx-api.generated.ts";
|
|
3
|
-
export type ItxWebSocketMessage = [timestamp: number, direction: "in" | "out", data: unknown];
|
|
4
|
-
type ConnectItxBaseInput = {
|
|
5
|
-
/** OS deployment base URL, e.g. the config's APP_CONFIG_BASE_URL. */
|
|
6
|
-
baseUrl: string;
|
|
7
|
-
/** Node WebSocket handshake headers, used by CLI/server callers with cookies. */
|
|
8
|
-
headers?: Record<string, string>;
|
|
9
|
-
/** Observe every decoded ws frame (e.g. the e2e suite's frame recorder). */
|
|
10
|
-
onWebSocketMessage?: (message: ItxWebSocketMessage) => void;
|
|
11
|
-
/**
|
|
12
|
-
* Observe the returned connection closing. Failed pre-ready dials use onRetry.
|
|
13
|
-
*
|
|
14
|
-
* A client whose job is to stay connected (a device, a long-lived agent)
|
|
15
|
-
* reconnects from HERE — the moment the transport dies — not lazily when
|
|
16
|
-
* its next call fails. The hook is a passive observer: this client stays
|
|
17
|
-
* vanilla capnweb and never reconnects, pings, or retries by itself; the
|
|
18
|
-
* consumer owns that loop.
|
|
19
|
-
*/
|
|
20
|
-
onWebSocketClose?: (close: {
|
|
21
|
-
code: number;
|
|
22
|
-
reason: string;
|
|
23
|
-
}) => void;
|
|
24
|
-
};
|
|
25
|
-
type ConnectItxAuthenticatedInput = ConnectItxBaseInput & {
|
|
26
|
-
auth: ItxAuthCredentials;
|
|
27
|
-
};
|
|
28
|
-
type ConnectProjectItxInput = ConnectItxAuthenticatedInput & {
|
|
29
|
-
projectId: string;
|
|
30
|
-
};
|
|
31
|
-
type ConnectAgentItxInput = ConnectItxAuthenticatedInput & {
|
|
32
|
-
agentPath: string;
|
|
33
|
-
projectId: string;
|
|
34
|
-
};
|
|
35
|
-
export type ItxInitialConnectionRetry = {
|
|
36
|
-
attemptDurationMs: number;
|
|
37
|
-
delayMs: number;
|
|
38
|
-
error: Error;
|
|
39
|
-
failedAttempt: 1;
|
|
40
|
-
nextAttempt: 2;
|
|
41
|
-
startedAt: string;
|
|
42
|
-
};
|
|
43
|
-
export type ConnectItxReadyOptions = {
|
|
44
|
-
/**
|
|
45
|
-
* Permit exactly one fresh dial while establishing the initial WebSocket.
|
|
46
|
-
*
|
|
47
|
-
* The retry boundary ends before the RPC session exists, so it can never
|
|
48
|
-
* replay authentication or a caller operation.
|
|
49
|
-
*/
|
|
50
|
-
retryInitialConnection?: {
|
|
51
|
-
/** Delay before the one retry. Defaults to 250ms; maximum 5s. */
|
|
52
|
-
delayMs?: number;
|
|
53
|
-
/** Observe the failed first dial before the retry begins. */
|
|
54
|
-
onRetry?: (retry: ItxInitialConnectionRetry) => Promise<void> | void;
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
export declare function connectItx(input: ConnectAgentItxInput): CapnRpcStub<Agent>;
|
|
58
|
-
export declare function connectItx(input: ConnectProjectItxInput): CapnRpcStub<Project>;
|
|
59
|
-
export declare function connectItx(input: ConnectItxAuthenticatedInput): CapnRpcStub<Session>;
|
|
60
|
-
export declare function connectItx(input: ConnectItxBaseInput): CapnRpcStub<UnauthenticatedOs>;
|
|
61
|
-
export declare function connectItxReady(input: ConnectAgentItxInput, options?: ConnectItxReadyOptions): Promise<CapnRpcStub<Agent>>;
|
|
62
|
-
export declare function connectItxReady(input: ConnectProjectItxInput, options?: ConnectItxReadyOptions): Promise<CapnRpcStub<Project>>;
|
|
63
|
-
export declare function connectItxReady(input: ConnectItxAuthenticatedInput, options?: ConnectItxReadyOptions): Promise<CapnRpcStub<Session>>;
|
|
64
|
-
export declare function connectItxReady(input: ConnectItxBaseInput, options?: ConnectItxReadyOptions): Promise<CapnRpcStub<UnauthenticatedOs>>;
|
|
65
|
-
export {};
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* itx-session — the framework-free half of the itx client: ONE WebSocket per
|
|
3
|
-
* process (browser tab, TUI, phone), connected to an OS deployment's `/api`,
|
|
4
|
-
* `authenticate()`d into a **Session** (the catalog that vends project itxs via
|
|
5
|
-
* `session.projects.get(slug)`), and kept alive through transport gaps.
|
|
6
|
-
*
|
|
7
|
-
* React never appears in this module. The hooks in ../sdk/itx/react.ts are a thin
|
|
8
|
-
* binding over the exact surface exported here (`subscribeSession` +
|
|
9
|
-
* `currentSnapshot` feed `useSyncExternalStore`; everything else is shared
|
|
10
|
-
* verbatim), so a non-React consumer — a node script, a future runtime —
|
|
11
|
-
* gets the same one-socket semantics by importing `iterate/client`.
|
|
12
|
-
*
|
|
13
|
-
* WHERE THE CONNECTION TARGET COMES FROM
|
|
14
|
-
* • In a browser, nothing to configure: the keeper connects
|
|
15
|
-
* `window.location`'s `/api` and authenticates with the session cookie
|
|
16
|
-
* riding the WebSocket handshake.
|
|
17
|
-
* • Anywhere else (the chat TUI, tests, scripts that want the KEEPER rather
|
|
18
|
-
* than the one-shot node dial), call {@link configureIterateSession} with a
|
|
19
|
-
* base URL and credentials. Calling it again for the same deployment
|
|
20
|
-
* refreshes the credential source without disturbing the socket; changing
|
|
21
|
-
* deployments deliberately replaces the socket. The runtime's global
|
|
22
|
-
* WebSocket carries the dial — node ≥ 22, bun, and React Native all satisfy
|
|
23
|
-
* capnweb's WebSocket needs.
|
|
24
|
-
*
|
|
25
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
26
|
-
* THE SESSION MODEL — one socket, generations, invisible reconnect
|
|
27
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
28
|
-
*
|
|
29
|
-
* • ONE WebSocket for the whole process, kept in module state (so in a
|
|
30
|
-
* browser it persists across client-side navigation). One connection attempt is a
|
|
31
|
-
* GENERATION ({@link Generation}): its WebSocket and its connecting
|
|
32
|
-
* promise. The session is the AWAITED `authenticate()` result — one settled
|
|
33
|
-
* stub identity shared by the snapshot, imperative awaiters, and the
|
|
34
|
-
* project-stub cache (resolving with the raw pipelined RpcPromise would
|
|
35
|
-
* fork identities: native promises assimilate thenables). The connection timeout
|
|
36
|
-
* spans the whole handshake (TCP/TLS/upgrade AND authenticate), and a REAL
|
|
37
|
-
* auth rejection over a working socket is terminal — it surfaces from the
|
|
38
|
-
* connecting promise instead of looping.
|
|
39
|
-
*
|
|
40
|
-
* • RECONNECT IS INVISIBLE. Readers see an immutable {@link Snapshot};
|
|
41
|
-
* `snapshot.session` holds the LAST live session and is kept across a
|
|
42
|
-
* transport gap. Before the FIRST session, awaiters share the STABLE
|
|
43
|
-
* {@link firstConnect} promise, which survives failed connection attempts (paced
|
|
44
|
-
* reconnects happen behind it; an individual attempt's rejection never reaches a suspended
|
|
45
|
-
* React tree) and rejects only on a TERMINAL failure. Kept-across-the-gap
|
|
46
|
-
* applies to TRANSPORT gaps only: a terminal auth rejection on a reconnect is
|
|
47
|
-
* an AUTHORITY loss — the halted snapshot drops the (already dead) session
|
|
48
|
-
* so the real error surfaces instead of zombie stubs.
|
|
49
|
-
*
|
|
50
|
-
* • PROJECT STUBS ARE SESSION-OWNED. `session.projects.get` allocates a
|
|
51
|
-
* capnweb import-table entry, so deriving stubs ad hoc (or inside React
|
|
52
|
-
* renders that may be discarded) would leak them. The module
|
|
53
|
-
* {@link projectStubCaches} WeakMap keyed by the session stub caches one
|
|
54
|
-
* real stub per (session, slug); a retired generation disposes them. The
|
|
55
|
-
* stub stays the REAL capnweb stub (a lazy wrapper that awaited the session
|
|
56
|
-
* per call would break pipelining — capnweb fork v0.8.0), and its identity
|
|
57
|
-
* changes exactly once per successful reconnect.
|
|
58
|
-
*
|
|
59
|
-
* • TRANSPORT HEALTH IS SOCKET-OWNED and GENERATION-GUARDED. A half-open
|
|
60
|
-
* socket (laptop sleep, network switch — no `close` event) is detected by
|
|
61
|
-
* one verifier per generation ({@link verifyTransport}: periodic +
|
|
62
|
-
* visibility/online probes, two-strike). Consumers REPORT suspicion
|
|
63
|
-
* ({@link reportTransportSuspicion}); they never close the shared socket
|
|
64
|
-
* themselves. Only two failed probes against the SAME generation retire it,
|
|
65
|
-
* and {@link reconnectIfCurrent} is a compare-and-swap on generation OBJECT
|
|
66
|
-
* IDENTITY — a late verdict against a superseded generation can never close
|
|
67
|
-
* its healthy successor. {@link reconnectIterateSession} is the separate,
|
|
68
|
-
* deliberate *semantic* reset (new claims after create/unlock).
|
|
69
|
-
*/
|
|
70
|
-
import { type RpcStub } from "@iterate-com/capnweb";
|
|
71
|
-
import type { ItxAuthCredentials, Project, Session } from "../itx-api.generated.ts";
|
|
72
|
-
/** The Session catalog (what `authenticate()` returns): vends project itxs. */
|
|
73
|
-
export type SessionStub = RpcStub<Session>;
|
|
74
|
-
/** A project capability handle — `session.projects.get(slug)`. */
|
|
75
|
-
export type ProjectStub = RpcStub<Project>;
|
|
76
|
-
export type { ProjectStub as Itx };
|
|
77
|
-
export type IterateSessionConfig = {
|
|
78
|
-
/** OS deployment base URL, e.g. `https://os.iterate.com`; `/api` is appended. */
|
|
79
|
-
baseUrl: string;
|
|
80
|
-
/**
|
|
81
|
-
* How `authenticate()` identifies the caller. A provider is resolved for
|
|
82
|
-
* every dial, so rotating credentials stay fresh across transport reconnects.
|
|
83
|
-
* If authentication rejects with an auth-shaped error, providers get one
|
|
84
|
-
* forced-refresh attempt before the failure becomes terminal. Default: the
|
|
85
|
-
* browser session cookie.
|
|
86
|
-
*/
|
|
87
|
-
credentials?: ItxAuthCredentials | ((options: {
|
|
88
|
-
forceRefresh: boolean;
|
|
89
|
-
}) => ItxAuthCredentials | Promise<ItxAuthCredentials>);
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* Point the keeper at a deployment explicitly — the non-browser entry into the
|
|
93
|
-
* one-socket model (the chat TUI, React Native, keeper-based scripts). Repeating
|
|
94
|
-
* the same target updates its credential source without disturbing the live
|
|
95
|
-
* socket. A different target retires the old deployment immediately and connects
|
|
96
|
-
* the new one, so authority can never cross deployments. In a browser this is
|
|
97
|
-
* optional (the default is `window.location`'s `/api` with cookie auth).
|
|
98
|
-
*/
|
|
99
|
-
export declare function configureIterateSession(config: IterateSessionConfig): void;
|
|
100
|
-
/**
|
|
101
|
-
* The immutable value readers see (React reads it via `useSyncExternalStore` +
|
|
102
|
-
* `use()`). Replaced wholesale on every transition, always BEFORE listeners are
|
|
103
|
-
* notified, so a concurrent render can never tear. `session` is the last live
|
|
104
|
-
* session and survives transport gaps — that is what makes reconnect
|
|
105
|
-
* invisible. `generation` is a monotonic number whose only job is being the
|
|
106
|
-
* reconnect dep for the React layer's reconnect-aware effect (the CAS is
|
|
107
|
-
* generation object identity, not this number). `connecting` is what
|
|
108
|
-
* first-load callers await/suspend on: before the FIRST session it is the
|
|
109
|
-
* stable {@link firstConnect} promise (survives closed-before-open retries
|
|
110
|
-
* without rejecting a suspended tree); afterwards `session` is always defined.
|
|
111
|
-
*/
|
|
112
|
-
type Snapshot = {
|
|
113
|
-
generation: number;
|
|
114
|
-
session: SessionStub | undefined;
|
|
115
|
-
connecting: Promise<SessionStub>;
|
|
116
|
-
};
|
|
117
|
-
export declare const subscribeSession: (onChange: () => void) => () => undefined;
|
|
118
|
-
export declare function projectStubFor(session: SessionStub, slug: string): ProjectStub;
|
|
119
|
-
/** getSnapshot for useSyncExternalStore: stable between transitions; connects when idle. */
|
|
120
|
-
export declare function currentSnapshot(): Snapshot;
|
|
121
|
-
export declare const serverSnapshot: () => never;
|
|
122
|
-
/**
|
|
123
|
-
* Ensure a live-or-connecting session and return its connecting promise. The
|
|
124
|
-
* imperative sibling of the React `useIterateSession()`: for handlers,
|
|
125
|
-
* `mutationFn`s, scripts, and lazy closures. Same one socket the hooks use.
|
|
126
|
-
* After a TERMINAL auth rejection this keeps returning that failure (the
|
|
127
|
-
* halted generation — see Generation.failed) until {@link reconnectIterateSession}.
|
|
128
|
-
*/
|
|
129
|
-
export declare function connectIterateSession(): Promise<SessionStub>;
|
|
130
|
-
/**
|
|
131
|
-
* The project itx for a slug (or `prj_…` id), imperatively. Pipelines through
|
|
132
|
-
* the returned promise — `(await connectItx(slug)).streams.get(path)`. Returns
|
|
133
|
-
* the session-owned cached stub, re-derived automatically after a reconnect.
|
|
134
|
-
*/
|
|
135
|
-
export declare function connectItx(slug: string): Promise<ProjectStub>;
|
|
136
|
-
/**
|
|
137
|
-
* Report that the shared transport may be half-open (a call hung). Consumers —
|
|
138
|
-
* the subscription watchdog, any long-lived reader — call this instead of
|
|
139
|
-
* closing the socket themselves — {@link verifyTransport} probes and, only on
|
|
140
|
-
* two strikes against the SAME generation, retires it. A stale report is a
|
|
141
|
-
* no-op.
|
|
142
|
-
*/
|
|
143
|
-
export declare function reportTransportSuspicion(): void;
|
|
144
|
-
/**
|
|
145
|
-
* The SEMANTIC reset (not a transport reconnect): drop the live socket and connection attempt
|
|
146
|
-
* a fresh one so the next reads run under the caller's CURRENT claims. Call
|
|
147
|
-
* after creating a project or unlocking admin — the live socket carries the
|
|
148
|
-
* connect-time principal. React callers that need already-cached data refreshed
|
|
149
|
-
* should also `invalidateQueries({ queryKey: ["itx"] })`.
|
|
150
|
-
*/
|
|
151
|
-
export declare function reconnectIterateSession(): void;
|
|
152
|
-
/**
|
|
153
|
-
* Revive only a generation parked by a terminal authentication failure. This
|
|
154
|
-
* is the imperative retry boundary for query/mutation clients: calling it
|
|
155
|
-
* before a repeated operation lets refreshed credentials dial again without
|
|
156
|
-
* disturbing a healthy or merely reconnecting session.
|
|
157
|
-
*/
|
|
158
|
-
export declare function retryFailedIterateSession(): void;
|
|
159
|
-
/**
|
|
160
|
-
* Release the current transport and authority without reconnecting. Intended
|
|
161
|
-
* for process-local lifecycle boundaries such as signing out of a native app;
|
|
162
|
-
* mounted consumers should be removed in the same transition. The explicit
|
|
163
|
-
* deployment configuration remains, so a later connect can dial it again.
|
|
164
|
-
*/
|
|
165
|
-
export declare function disconnectIterateSession(): void;
|
|
166
|
-
/**
|
|
167
|
-
* The four — and only four — transport-close rejections a caller may treat as
|
|
168
|
-
* "the socket died, retry on a fresh one": our own connection-closed rejection, capnweb's
|
|
169
|
-
* two WebSocket aborts (`Peer closed WebSocket: <code> <reason>` and
|
|
170
|
-
* `WebSocket connection failed.`), and capnweb's exact local-shutdown error
|
|
171
|
-
* from our forced bootstrap disposal. Deliberately NARROW: an
|
|
172
|
-
* application/auth/validation error that merely mentions "WebSocket" must never
|
|
173
|
-
* be mistaken for a transport failure and retried. This is the one discriminant
|
|
174
|
-
* shared by the query retry, the subscribe retry, and the liveness verifier.
|
|
175
|
-
*/
|
|
176
|
-
export declare function isItxTransportError(error: unknown): boolean;
|
|
177
|
-
/**
|
|
178
|
-
* The common lifecycle used by reconnecting stream and live-state clients.
|
|
179
|
-
* Concrete APIs adapt their own handle (`StreamConnectionHandle.close()` or
|
|
180
|
-
* `LiveStateSubscriptionHandle.unsubscribe()`) to `close()`. Real handles are
|
|
181
|
-
* capnweb stubs and therefore Disposable: `close()` closes the server-side
|
|
182
|
-
* resource, while `[Symbol.dispose]` releases the caller-owned stub —
|
|
183
|
-
* on the process-long shared socket, skipping the dispose leaks one
|
|
184
|
-
* import-table entry per subscribe cycle, so holders always do both.
|
|
185
|
-
*/
|
|
186
|
-
export type ItxRecoverableConnectionHandle = {
|
|
187
|
-
ping(): boolean | Promise<boolean>;
|
|
188
|
-
close(): unknown;
|
|
189
|
-
[Symbol.dispose]?(): void;
|
|
190
|
-
};
|
|
191
|
-
/**
|
|
192
|
-
* Release a push connection completely: `close()` closes the server side
|
|
193
|
-
* (already-dead is fine — the rejection is swallowed), then
|
|
194
|
-
* `[Symbol.dispose]` frees the caller-owned stub. One helper because
|
|
195
|
-
* forgetting either half leaks (see {@link ItxRecoverableConnectionHandle}).
|
|
196
|
-
*/
|
|
197
|
-
export declare function releaseItxConnection(handle: ItxRecoverableConnectionHandle): void;
|
|
198
|
-
/**
|
|
199
|
-
* Poll a push connection's `ping()` until it stops answering `true`, then
|
|
200
|
-
* recover. Server pushes fail SILENTLY: a dead Durable Object or half-open TCP
|
|
201
|
-
* stops delivering with no client-visible signal, so a consumer can show
|
|
202
|
-
* "live" forever while stale. The watchdog checks on an interval and — because
|
|
203
|
-
* those are exactly when sockets die — when a browser tab becomes visible or
|
|
204
|
-
* comes online (in runtimes without those signals, the interval carries it).
|
|
205
|
-
*
|
|
206
|
-
* `dead` → ping answered `false` or REJECTED: the socket works but the
|
|
207
|
-
* server-side connection is gone (DO restart / dropped
|
|
208
|
-
* callback). Recovery opens it again on the same socket.
|
|
209
|
-
* `timed-out` → ping never answered: the shared WebSocket is half-open. The
|
|
210
|
-
* watchdog REPORTS the suspicion to the socket-owned verifier
|
|
211
|
-
* ({@link reportTransportSuspicion}) — it never closes the socket
|
|
212
|
-
* itself — and the holder opens again once the generation
|
|
213
|
-
* reconnects.
|
|
214
|
-
*/
|
|
215
|
-
export declare function watchItxConnection(ping: () => boolean | Promise<boolean>, onDead: () => void): () => void;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Groups an authenticated child stub with the parent stubs that keep it alive.
|
|
3
|
-
*
|
|
4
|
-
* Cap'n Web callers often want `using project = connectItx({ projectId })`, but
|
|
5
|
-
* that project stub is reached through a root session and authentication stub.
|
|
6
|
-
* This proxy makes disposal and `dup()` preserve the whole ownership chain, so
|
|
7
|
-
* disposing the child also tells the server it can release the parent stubs.
|
|
8
|
-
*/
|
|
9
|
-
type DisposableLike = {
|
|
10
|
-
[Symbol.dispose]?(): void;
|
|
11
|
-
dup?(): DisposableLike;
|
|
12
|
-
};
|
|
13
|
-
export declare function withOwnedRpcSession<T extends object>(stub: T, ...owned: DisposableLike[]): T;
|
|
14
|
-
export {};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
-
/**
|
|
3
|
-
* One Query client policy for every Iterate React renderer.
|
|
4
|
-
*
|
|
5
|
-
* The browser dashboard and the OpenTUI client intentionally share these
|
|
6
|
-
* defaults. Renderer-specific entrypoints own only where the provider mounts;
|
|
7
|
-
* cache lifetime, refetch behavior, and mutation retries must not drift by
|
|
8
|
-
* surface.
|
|
9
|
-
*/
|
|
10
|
-
export declare function createIterateQueryClient(): QueryClient;
|