@demlik/tea 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/chunk-27N2SPTN.js +222 -0
- package/dist/chunk-27N2SPTN.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-3XJWC7VW.js +61 -0
- package/dist/chunk-3XJWC7VW.js.map +1 -0
- package/dist/chunk-5DEQ5I3U.js +132 -0
- package/dist/chunk-5DEQ5I3U.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/chunk-6RGBKODV.js +268 -0
- package/dist/chunk-6RGBKODV.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7W4LMPUW.js +186 -0
- package/dist/chunk-7W4LMPUW.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
- package/dist/chunk-DILMKOZ7.js.map +1 -0
- package/dist/chunk-EHIX7NYZ.js +93 -0
- package/dist/chunk-EHIX7NYZ.js.map +1 -0
- package/dist/chunk-HCEVHFJ7.js +54 -0
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-J6527VRQ.js +56 -0
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-K7Q23EQ4.js +20 -0
- package/dist/chunk-K7Q23EQ4.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
- package/dist/chunk-LP7VQBRG.js.map +1 -0
- package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
- package/dist/chunk-MMH7CW5K.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-ON24RKSE.js +60 -0
- package/dist/chunk-ON24RKSE.js.map +1 -0
- package/dist/chunk-PS23H6KQ.js +87 -0
- package/dist/chunk-PS23H6KQ.js.map +1 -0
- package/dist/chunk-QLA76GIT.js +162 -0
- package/dist/chunk-QLA76GIT.js.map +1 -0
- package/dist/chunk-S3FQN4SL.js +506 -0
- package/dist/chunk-S3FQN4SL.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-SQTCS44W.js +33 -0
- package/dist/chunk-SQTCS44W.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-VLGARN6H.js +202 -0
- package/dist/chunk-VLGARN6H.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-BoB6t7dB.d.ts +188 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1723 -92
- package/dist/do/index.js +673 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-CO9Uh971.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +5 -9
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +92 -0
- package/dist/parity/index.js +78 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +6 -11
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +130 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +43 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +294 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- package/dist/retry-to-success/index.d.ts +109 -0
- package/dist/retry-to-success/index.js +35 -0
- package/dist/retry-to-success/index.js.map +1 -0
- package/dist/saga/index.d.ts +238 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +380 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +111 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +251 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +70 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +450 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +168 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- package/dist/chunk-ZRUST6BK.js.map +0 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { DeadlineExceeded, DeadlineSub } from '../deadline/index.js';
|
|
2
|
+
export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
|
|
3
|
+
import { I as Interpret, C as Cmd } from '../core-BoB6t7dB.js';
|
|
4
|
+
import { SnapshotState, SnapshotWriteCmd, SnapshotStore, SnapshotSavedMsg, SnapshotFailedMsg, SnapshotLoadedMsg, SnapshotLoadFailedMsg, SnapshotLoadCmd } from '../snapshot/index.js';
|
|
5
|
+
import { QueueItem } from '../work-queue/index.js';
|
|
6
|
+
import '../types-B-8bcAp8.js';
|
|
7
|
+
import '../index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @demlik/tea/monitored-run — a long-running operation that is BOTH staged and
|
|
11
|
+
* watched: an ordered stage pipeline whose POSITION survives eviction, wrapped
|
|
12
|
+
* by a no-progress safety deadline and (optionally) a periodic durable
|
|
13
|
+
* checkpoint.
|
|
14
|
+
*
|
|
15
|
+
* This is the merge of two field-guide concerns the audit-agent machine
|
|
16
|
+
* hand-wired together (`seed/audit-agent-machine`): the OUTER pipeline position
|
|
17
|
+
* (`RunStage` — planning → testing → summarizing → reporting, replayed on cold
|
|
18
|
+
* wake by re-emitting the one outstanding effect) and the no-progress safety
|
|
19
|
+
* alarm (`subs.ts`, keyed on `progressSeq` so every progress event retires the
|
|
20
|
+
* old alarm and arms a fresh one). `monitored-run` lifts both into one config-
|
|
21
|
+
* driven knob with the uniform L2 contract `resilient-call` established:
|
|
22
|
+
* `init()`, the verbs (`start` / `advance` / `progress` / `onDeadline` /
|
|
23
|
+
* `boot`), `subs(state)`, and `handlers(ports)`.
|
|
24
|
+
*
|
|
25
|
+
* - It is **staged, optionally**. Omit `stages` and the run is single-shot:
|
|
26
|
+
* `start` enters `running`, `advance` finishes it to `done`. Provide
|
|
27
|
+
* `stages` and it becomes an ordered pipeline — each `advance` retires the
|
|
28
|
+
* current stage and claims the next, finishing to `done` only when the last
|
|
29
|
+
* stage is retired. The pipeline POSITION lives in the Model (a
|
|
30
|
+
* `work-queue` of stage items), so an eviction mid-pipeline resumes at the
|
|
31
|
+
* same stage via `boot` — the audit machine's "pipeline position survives
|
|
32
|
+
* eviction" property, generalized.
|
|
33
|
+
* - It is **watched**. While `running`, a no-progress deadline is armed at
|
|
34
|
+
* `lastProgressAt + deadlineMs` and keyed on `progressSeq`. Every
|
|
35
|
+
* `progress` / `advance` bumps the seq, so the substrate retires the old
|
|
36
|
+
* alarm and arms a new one — a self-rearming watchdog. If the alarm fires
|
|
37
|
+
* (`onDeadline`) the run goes `failed` with a deadline reason. Omit
|
|
38
|
+
* `deadlineMs` → no watchdog (the `stale` phase is never reachable).
|
|
39
|
+
* - It **checkpoints, optionally**. Provide `snapshotEvery` and every
|
|
40
|
+
* progress unit is accounted to a `../snapshot` slice; once the cadence is
|
|
41
|
+
* reached a checkpoint Cmd is emitted (interpreted by the spliced snapshot
|
|
42
|
+
* `handlers`), so a crashed run restarts from the last durable checkpoint
|
|
43
|
+
* rather than from zero. Omit it → no checkpointing.
|
|
44
|
+
*
|
|
45
|
+
* ## Why the run lifecycle is INLINE (run-tracker not extracted)
|
|
46
|
+
*
|
|
47
|
+
* The phase union (`running` / `done` / `failed` / `stale`) and its
|
|
48
|
+
* bookkeeping (`runId`, `startedAt`, `lastProgressAt`, `progressSeq`) live
|
|
49
|
+
* directly on this slice rather than behind a `run-tracker` sub-knob.
|
|
50
|
+
* Extraction earns its keep when two independent axes vary; here the lifecycle
|
|
51
|
+
* has exactly one anchoring axis (this run), so a separate module would be a
|
|
52
|
+
* premature seam. The two genuinely independent axes — the safety DEADLINE and
|
|
53
|
+
* the periodic SNAPSHOT — ARE extracted (`../deadline`, `../snapshot`), and the
|
|
54
|
+
* stage list is a `../work-queue` slice (`QueueItem<Stage>[]`) whose every
|
|
55
|
+
* status flip is DELEGATED to the blessed `../work-queue/ops` (`enqueueOp` /
|
|
56
|
+
* `claimNextOp` / `markDoneOp`) — never re-rolled here. The lifecycle is the
|
|
57
|
+
* glue between them.
|
|
58
|
+
*
|
|
59
|
+
* ## The two non-negotiables (canon)
|
|
60
|
+
*
|
|
61
|
+
* - **Durable** — the slice is plain data (the stage queue is a
|
|
62
|
+
* JSON-serializable `QueueItem<Stage>[]`, the snapshot slice is plain
|
|
63
|
+
* numbers), so it survives DO eviction / reload.
|
|
64
|
+
* - **Replayable** — every transition is a pure verb returning new state +
|
|
65
|
+
* Cmds; nothing here reads the clock or RNG. Time arrives as an `at` / `atMs`
|
|
66
|
+
* parameter; the only clock read is `Date.now()` inside the snapshot
|
|
67
|
+
* `handlers` port (the effect boundary), exactly as `../snapshot` stamps it.
|
|
68
|
+
*
|
|
69
|
+
* ## Typical wiring
|
|
70
|
+
*
|
|
71
|
+
* const run = createMonitoredRun<MyStage, MyState>({
|
|
72
|
+
* stages: ["plan", "test", "report"],
|
|
73
|
+
* deadlineMs: 10 * 60_000,
|
|
74
|
+
* snapshotEvery: 25,
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* init: (loaded) => loaded ? [loaded, []] : [{ run: run.init() }, []],
|
|
78
|
+
* update: {
|
|
79
|
+
* begin: (s, m) => lift(s, run.start(s.run, m.runId, m.at)),
|
|
80
|
+
* advance: (s, m) => lift(s, run.advance(s.run, checkpoint(s), m.result, m.at)),
|
|
81
|
+
* progress: (s, m) => lift(s, run.progress(s.run, checkpoint(s), m.at)),
|
|
82
|
+
* run_deadline: (s, m) => lift(s, run.onDeadline(s.run, m)),
|
|
83
|
+
* boot: (s, m) => lift(s, run.boot(s.run, m.at)),
|
|
84
|
+
* },
|
|
85
|
+
* subscriptions: (s) => run.subs(s.run),
|
|
86
|
+
* subscribe: { deadline: subscribeDeadline },
|
|
87
|
+
* interpret: run.handlers({ store: r2 }),
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The monitored-run knob. EVERY field is optional:
|
|
92
|
+
* - omit `stages` → single-shot run (no pipeline; one `advance` finishes).
|
|
93
|
+
* - omit `deadlineMs` → no no-progress watchdog (the run is never auto-failed).
|
|
94
|
+
* - omit `snapshotEvery`→ no periodic checkpoint (no `snapshot_write` Cmds).
|
|
95
|
+
*
|
|
96
|
+
* An empty config `{}` is a valid single-shot, unwatched, uncheckpointed run.
|
|
97
|
+
*
|
|
98
|
+
* `Stage` is the consumer's stage identifier — any JSON-serializable value
|
|
99
|
+
* (a string id, a `{ kind, ... }` record). It rides through the stage queue
|
|
100
|
+
* unchanged so the consumer's reducer can branch on the current stage to emit
|
|
101
|
+
* that stage's outstanding effect (the audit machine's `outstandingEffect`).
|
|
102
|
+
*/
|
|
103
|
+
interface MonitoredRunConfig<Stage> {
|
|
104
|
+
/**
|
|
105
|
+
* The ordered stages of the pipeline. Position survives eviction (lives in
|
|
106
|
+
* the Model's stage queue). Omit for a single-shot run. An empty array is
|
|
107
|
+
* treated as single-shot too — there is nothing to pipeline through.
|
|
108
|
+
*/
|
|
109
|
+
readonly stages?: readonly Stage[];
|
|
110
|
+
/**
|
|
111
|
+
* No-progress watchdog budget, in ms. The safety deadline is armed at
|
|
112
|
+
* `lastProgressAt + deadlineMs` and re-armed on every progress event. Omit
|
|
113
|
+
* for no watchdog.
|
|
114
|
+
*/
|
|
115
|
+
readonly deadlineMs?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Checkpoint cadence — write a durable snapshot once this many progress
|
|
118
|
+
* units have accumulated. Threaded straight into `../snapshot`'s `every`.
|
|
119
|
+
* Omit for no checkpointing.
|
|
120
|
+
*/
|
|
121
|
+
readonly snapshotEvery?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Store key the rolling checkpoint is written under. Forwarded to
|
|
124
|
+
* `../snapshot`'s `key`; defaults to that module's `@@snapshot`. Ignored
|
|
125
|
+
* when `snapshotEvery` is omitted.
|
|
126
|
+
*/
|
|
127
|
+
readonly snapshotKey?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Why a run terminated as `failed`. Discriminated so each reason carries only
|
|
131
|
+
* its own data (pattern 11):
|
|
132
|
+
* - `deadline` — the no-progress watchdog fired (`onDeadline`).
|
|
133
|
+
* - `stage` — the consumer reported a stage failure via `advance(..., fail)`.
|
|
134
|
+
*
|
|
135
|
+
* `stale` is a SEPARATE phase (not a fail reason): a run is `stale` only by an
|
|
136
|
+
* explicit consumer signal that progress has wedged WITHOUT the deadline having
|
|
137
|
+
* fired — a soft, recoverable observation. The deadline firing is a hard
|
|
138
|
+
* terminal (`failed { reason: "deadline" }`).
|
|
139
|
+
*
|
|
140
|
+
* The `stage` variant carries the FAILING STAGE VALUE (the `Stage` itself), not
|
|
141
|
+
* a positional index: done stages leave the queue (`markDoneOp` filters them),
|
|
142
|
+
* so a residual index drifts as the pipeline advances. The stage value is the
|
|
143
|
+
* stable, meaningful identifier — `undefined` for a single-shot run (no stage).
|
|
144
|
+
*/
|
|
145
|
+
type RunFailure<Stage> = {
|
|
146
|
+
readonly reason: "deadline";
|
|
147
|
+
readonly at: number;
|
|
148
|
+
} | {
|
|
149
|
+
readonly reason: "stage";
|
|
150
|
+
readonly stage: Stage | undefined;
|
|
151
|
+
readonly error: unknown;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Run lifecycle phase. Discriminated on `phase`:
|
|
155
|
+
*
|
|
156
|
+
* - `running` — the run is live. Carries the current pipeline position
|
|
157
|
+
* (`stepStates` is the stage queue; the single `running` item in it is the
|
|
158
|
+
* current stage, `undefined` for a single-shot run) and the watchdog
|
|
159
|
+
* bookkeeping (`lastProgressAt` / `progressSeq` keying the safety alarm).
|
|
160
|
+
* - `stale` — a soft "no progress observed" mark the consumer can set and
|
|
161
|
+
* clear; the run is still resumable (a `progress` clears it back to
|
|
162
|
+
* `running`). Distinct from a fired deadline, which is terminal.
|
|
163
|
+
* - `done` — every stage retired (or the single-shot `advance` landed).
|
|
164
|
+
* - `failed` — terminal failure; `failure` carries why.
|
|
165
|
+
*/
|
|
166
|
+
type RunPhase = "running" | "stale" | "done" | "failed";
|
|
167
|
+
/**
|
|
168
|
+
* The slice. Plain data end to end:
|
|
169
|
+
* - `phase` — the lifecycle discriminant.
|
|
170
|
+
* - `runId` — host-minted run identity, stamped at `start` (the
|
|
171
|
+
* machine never mints — see the audit seed's
|
|
172
|
+
* "run identity is host-minted").
|
|
173
|
+
* - `stepStates` — the stage pipeline as a `work-queue` of stage items.
|
|
174
|
+
* Empty for a single-shot run. The ONE `running` item is
|
|
175
|
+
* the current stage; `done` items are retired stages
|
|
176
|
+
* (they leave the queue via `markDoneOp`, so the queue
|
|
177
|
+
* shrinks as the pipeline advances and is empty at
|
|
178
|
+
* `done`). POSITION survives eviction because this list
|
|
179
|
+
* is durable.
|
|
180
|
+
* - `startedAt` — `at` of `start`. Observability; never compared in a verb.
|
|
181
|
+
* - `lastProgressAt` — `at` of the most recent progress event; the watchdog
|
|
182
|
+
* base. Re-armed by `progress` / `advance`.
|
|
183
|
+
* - `progressSeq` — monotonic progress counter; keys the safety alarm Sub
|
|
184
|
+
* id so every bump retires the old alarm and arms a new
|
|
185
|
+
* one (the audit subs' `safety:{progressSeq}` discipline).
|
|
186
|
+
* - `failure` — present iff `phase === "failed"`; the typed reason.
|
|
187
|
+
* - `snapshot` — the `../snapshot` slice (cadence counter + durable
|
|
188
|
+
* watermark). Always present (uniform shape); the gate
|
|
189
|
+
* is simply never driven when `snapshotEvery` is omitted.
|
|
190
|
+
*/
|
|
191
|
+
interface MonitoredRunState<Stage> {
|
|
192
|
+
readonly phase: RunPhase;
|
|
193
|
+
readonly runId: string;
|
|
194
|
+
readonly stepStates: readonly QueueItem<Stage>[];
|
|
195
|
+
readonly startedAt: number;
|
|
196
|
+
readonly lastProgressAt: number;
|
|
197
|
+
readonly progressSeq: number;
|
|
198
|
+
readonly failure: RunFailure<Stage> | null;
|
|
199
|
+
readonly snapshot: SnapshotState;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The outcome a consumer reports to `advance`: the current stage either
|
|
203
|
+
* succeeded (retire it, claim the next) or failed (terminate the run). Plain
|
|
204
|
+
* data — the consumer parses its stage result at its own boundary and hands a
|
|
205
|
+
* settled outcome here.
|
|
206
|
+
*/
|
|
207
|
+
type StageResult = {
|
|
208
|
+
readonly kind: "ok";
|
|
209
|
+
} | {
|
|
210
|
+
readonly kind: "fail";
|
|
211
|
+
readonly error: unknown;
|
|
212
|
+
};
|
|
213
|
+
/** The checkpoint-write Cmd, generic over the consumer's checkpoint value `V`. */
|
|
214
|
+
type MonitoredRunCmd<V> = SnapshotWriteCmd<V>;
|
|
215
|
+
/** The deadline Msg the safety alarm dispatches when the watchdog fires. */
|
|
216
|
+
type MonitoredRunTimerMsg = DeadlineExceeded;
|
|
217
|
+
/** Ports the consumer supplies to `handlers` — just the checkpoint store. */
|
|
218
|
+
interface MonitoredRunPorts<V> {
|
|
219
|
+
/** Where periodic checkpoints are written (R2 / KV / DO sub-key / Map). */
|
|
220
|
+
readonly store: SnapshotStore<V>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Build a monitored-run knob from `config`. `Stage` is the consumer's stage
|
|
224
|
+
* identifier type; `V` is the consumer's checkpoint value type (what
|
|
225
|
+
* `snapshotEvery` checkpoints). Returns the uniform L2 knob contract.
|
|
226
|
+
*
|
|
227
|
+
* No `rng` parameter: unlike `resilient-call`, nothing here jitters — the only
|
|
228
|
+
* external inputs are time (an `at` / `atMs` parameter) and the consumer's
|
|
229
|
+
* stage outcomes. The clock is read only inside the snapshot `handlers` port.
|
|
230
|
+
*/
|
|
231
|
+
declare function createMonitoredRun<Stage, V = unknown>(config?: MonitoredRunConfig<Stage>): {
|
|
232
|
+
init: () => MonitoredRunState<Stage>;
|
|
233
|
+
start: (s: MonitoredRunState<Stage>, runId: string, at: number) => readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]];
|
|
234
|
+
progress: (s: MonitoredRunState<Stage>, payload: V, at: number) => readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]];
|
|
235
|
+
markStale: (s: MonitoredRunState<Stage>) => readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]];
|
|
236
|
+
advance: (s: MonitoredRunState<Stage>, payload: V, result: StageResult, at: number) => readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]];
|
|
237
|
+
onDeadline: (s: MonitoredRunState<Stage>, msg: MonitoredRunTimerMsg) => readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]];
|
|
238
|
+
boot: (s: MonitoredRunState<Stage>, at: number) => readonly [MonitoredRunState<Stage>, readonly MonitoredRunCmd<V>[]];
|
|
239
|
+
confirmSnapshot: (s: MonitoredRunState<Stage>, msg: SnapshotSavedMsg) => MonitoredRunState<Stage>;
|
|
240
|
+
subs: (s: MonitoredRunState<Stage>) => readonly DeadlineSub[];
|
|
241
|
+
handlers: (ports: MonitoredRunPorts<V>) => Interpret<SnapshotSavedMsg | SnapshotFailedMsg | SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg, SnapshotWriteCmd<V> | SnapshotLoadCmd, unknown>;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the
|
|
245
|
+
* slice lives at `state.run`. Convenience for the common single-slice host;
|
|
246
|
+
* hosts with a differently-named field spread by hand. PURE — a thin record
|
|
247
|
+
* rebuild, no clock / RNG.
|
|
248
|
+
*/
|
|
249
|
+
declare function liftRun<S extends {
|
|
250
|
+
run: MonitoredRunState<Stage>;
|
|
251
|
+
}, Stage, C extends Cmd>(state: S, [slice, cmds]: readonly [MonitoredRunState<Stage>, readonly C[]]): readonly [S, readonly C[]];
|
|
252
|
+
|
|
253
|
+
export { DeadlineExceeded, DeadlineSub, type MonitoredRunCmd, type MonitoredRunConfig, type MonitoredRunPorts, type MonitoredRunState, type MonitoredRunTimerMsg, type RunFailure, type RunPhase, SnapshotSavedMsg, SnapshotWriteCmd, type StageResult, createMonitoredRun, liftRun };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createMonitoredRun, liftRun } from '../chunk-7W4LMPUW.js';
|
|
2
|
+
import '../chunk-EHIX7NYZ.js';
|
|
3
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-K7Q23EQ4.js';
|
|
4
|
+
import '../chunk-HJO6VNQI.js';
|
|
5
|
+
import '../chunk-TGG26XMK.js';
|
|
6
|
+
import '../chunk-2YFLOCWX.js';
|
|
7
|
+
import '../chunk-MMH7CW5K.js';
|
|
8
|
+
import '../chunk-QLA76GIT.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Store, SubId, Sub } from '../index.js';
|
|
2
1
|
import WebSocket from 'ws';
|
|
2
|
+
import { Store } from '../index.js';
|
|
3
|
+
import { a as SubId, S as Sub } from '../core-BoB6t7dB.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @demlik/tea/node — Node host adapter for `@demlik/tea`.
|
|
@@ -99,10 +100,9 @@ type NodeWsRegistry = Map<SubId, WebSocket>;
|
|
|
99
100
|
* The Ctx fields the node Subs depend on. A consumer's Ctx must extend this so
|
|
100
101
|
* `node_ws` can register the live socket and Cmd handlers can reach it.
|
|
101
102
|
*
|
|
102
|
-
* Deliberately NOT parameterized by Msg
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* callback on ctx (the way tea-do's registries do), this becomes
|
|
103
|
+
* Deliberately NOT parameterized by Msg: the registry holds raw `WebSocket`s,
|
|
104
|
+
* and each Sub variant carries its own Msg-typed callbacks. Trip-wire: if a
|
|
105
|
+
* future node Sub holds a Msg-typed callback on ctx, this becomes
|
|
106
106
|
* `NodeSubscribeCtx<M>`.
|
|
107
107
|
*/
|
|
108
108
|
interface NodeSubscribeCtx {
|
package/dist/node/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/node/index.ts"],"names":["handle"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/node — Node host adapter for `@demlik/tea`.\n *\n * Every host adapter ships two halves (mirrors `@demlik/tea/do`):\n *\n * 1. `fileStore<S>(path, parse)` — a `Store<S>` over a JSON file on disk.\n * Atomic save (temp + rename). Absent file → `load()` resolves `null`\n * (fresh boot). Structurally malformed JSON throws at `load()` (an infra\n * error), matching `doStore`; a shape mismatch is NOT a throw — the\n * substrate calls `migrate`, which returns `null` and boots fresh.\n * Use for resumable scripts / local repro of prod machines. Ephemeral\n * CLIs can skip persistence with `memoryStore` from `@demlik/tea/mem`.\n *\n * 2. `nodeSubscribe<M, Ctx>()` — handler registry for the three node-native\n * Sub types this package owns:\n * - `node_ws` — opens a `ws` WebSocket; routes open/message/close/\n * error into Msgs. Registers the live socket in\n * `ctx.wsRegistry` so a Cmd handler can write to it\n * (see `sendToWebSocket`). Cleanup closes the socket\n * for ANY non-CLOSED state.\n * - `node_timer` — setTimeout / setInterval; cleanup clears it.\n * - `node_signal` — a process signal (SIGINT/SIGTERM/…) → Msg; cleanup\n * detaches the listener.\n *\n * Unlike tea-do — whose `alarm()` / `webSocketMessage()` are DO lifecycle\n * methods the handler can't observe from the outside, hence its registries —\n * a node process is continuously alive: each handler holds its resource in\n * closure and the returned cleanup closes it. The substrate's subscription\n * reconciler runs that cleanup the instant `subscriptions(state)` stops listing\n * the Sub — end-then-start choreography for free (Rule 9, [invariant 4]).\n * A lifecycle-bound resource (a socket, a timer) becomes impossible to leak\n * from a reducer cell, because no reducer cell owns its teardown.\n *\n * `ctx.wsRegistry` is the one shared surface: `node_ws` populates it; a Cmd\n * interpreter reads it to write to the open socket. Same shape as tea-do's\n * `wsRegistry`.\n */\n\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { Store, Sub, SubId } from \"../index\";\nimport WebSocket, { type RawData } from \"ws\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// fileStore — `Store<S>` over a JSON file.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * `Store<S>` backed by a JSON file at `path`.\n *\n * `parse` is REQUIRED because the file is a real serialization boundary — the\n * bytes that come back through `JSON.parse` are structurally `unknown`, and the\n * caller (who owns `S`) is the only party that can validate the shape.\n * Returning `null` from `parse` means \"no usable persisted state\" — the\n * substrate boots `init` with `loaded = null`. `parse` must NOT throw per the\n * `Store<S>.migrate` contract.\n */\nexport function fileStore<S>(path: string, parse: (raw: unknown) => S | null): Store<S> {\n return {\n async load(): Promise<unknown> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n // JSON.parse throws on structural malformation — propagate (infra error),\n // matching `doStore`. The decoded value is intentionally returned as\n // `unknown`; the substrate's `migrate` callback is the boundary parse.\n return JSON.parse(raw);\n },\n async save(state: S): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n // Write to a pid-scoped temp then atomically rename. The rename gives\n // torn-write safety (a reader sees the old file or the new one, never a\n // partial) and survives process crash / Ctrl-C — the only failure a local\n // resumable run resumes from. fsync (file + dir) would add power-loss\n // durability the use case doesn't need; the substrate serializes saves\n // through run()'s queue, so there's never a second concurrent writer.\n const tmp = `${path}.${process.pid}.tmp`;\n await writeFile(tmp, JSON.stringify(state), \"utf8\");\n await rename(tmp, path);\n },\n migrate(raw: unknown): S | null {\n return parse(raw);\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// NodeSub — the three node-native subscription variants.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A node WebSocket sub. The substrate diffs subs by `id`; the same `id` across\n * transitions = the same socket (kept open). Emit a DIFFERENT `id` to force a\n * reconnect.\n *\n * Only `onMessage` returns `M | null` (null drops the frame) because inbound\n * frames are the boundary that needs a parse-and-maybe-discard seam. `onOpen`/\n * `onClose`/`onError` always dispatch — a spurious one is dropped in the reducer\n * (the cell ignores it), not here.\n */\nexport type NodeWsSub<M> = {\n id: SubId;\n type: \"node_ws\";\n url: string;\n onMessage: (data: string) => M | null;\n onOpen?: () => M;\n onClose?: (code: number, reason: string) => M;\n onError?: (message: string) => M;\n};\n\n/**\n * A node timer sub. One-shot `setTimeout` by default; `repeat: true` →\n * `setInterval`. Cleanup clears the timer — a timer keyed to a phase is\n * cancelled the moment the machine leaves that phase.\n */\nexport type NodeTimerSub<M> = {\n id: SubId;\n type: \"node_timer\";\n delayMs: number;\n msg: M;\n repeat?: boolean;\n};\n\n/**\n * A process-signal sub: `signal` (SIGINT/SIGTERM/…) → `msg`. Cleanup detaches\n * the listener. Lets graceful shutdown be a real machine transition rather than\n * an out-of-band `process.on` that the reducer can't see.\n */\nexport type NodeSignalSub<M> = {\n id: SubId;\n type: \"node_signal\";\n signal: NodeJS.Signals;\n msg: M;\n};\n\nexport type NodeSub<M> = NodeWsSub<M> | NodeTimerSub<M> | NodeSignalSub<M>;\n\n// Compile-time guarantee: `NodeSub<M>` is structurally a `Sub` (has `id`+`type`).\n// Erased at runtime — the `extends Sub` constraint on the alias's type\n// parameter fails to compile if `NodeSub` ever drifts off the `Sub` shape.\nexport type AssertNodeSubIsSub<T extends Sub = NodeSub<unknown>> = T;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Ctx — what the node Subs read / populate.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type NodeWsRegistry = Map<SubId, WebSocket>;\n\n/**\n * The Ctx fields the node Subs depend on. A consumer's Ctx must extend this so\n * `node_ws` can register the live socket and Cmd handlers can reach it.\n *\n * Deliberately NOT parameterized by Msg (unlike tea-do's `DoSubscribeCtx<M>`):\n * the registry holds raw `WebSocket`s, and each Sub variant carries its own\n * Msg-typed callbacks. Trip-wire: if a future node Sub holds a Msg-typed\n * callback on ctx (the way tea-do's registries do), this becomes\n * `NodeSubscribeCtx<M>`.\n */\nexport interface NodeSubscribeCtx {\n wsRegistry: NodeWsRegistry;\n}\n\n/**\n * Write a frame to a registered `node_ws` socket. No-op (returns `false`) when\n * the socket is absent, not OPEN, or `send()` throws. The Cmd-side companion to\n * the `node_ws` Sub: the Sub owns the socket's lifetime; this writes to it.\n *\n * The contract is a boolean no-op, never a throw: the socket can transition out\n * of OPEN between the `readyState` check and `send()` (or `ws` can throw on an\n * errored underlying connection), and a Cmd interpreter must not crash on that.\n */\nexport function sendToWebSocket(ctx: NodeSubscribeCtx, id: SubId, data: string): boolean {\n const socket = ctx.wsRegistry.get(id);\n if (!socket || socket.readyState !== WebSocket.OPEN) return false;\n try {\n socket.send(data);\n return true;\n } catch {\n return false;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// nodeSubscribe — the handler registry.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build the `subscribe` handlers for `node_ws`, `node_timer`, and\n * `node_signal`. Matches `Machine#subscribe`'s\n * `[K in U[\"type\"]]: (sub, ctx, dispatch) => () => void` contract for the\n * `NodeSub<M>` variants. Generic `Ctx` must extend `NodeSubscribeCtx` so the\n * `node_ws` handler can register its socket.\n */\nexport function nodeSubscribe<M, Ctx extends NodeSubscribeCtx>(): {\n node_ws: (sub: NodeWsSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;\n node_timer: (sub: NodeTimerSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;\n node_signal: (sub: NodeSignalSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;\n} {\n return {\n node_ws: (sub, ctx, dispatch) => {\n const socket = new WebSocket(sub.url);\n ctx.wsRegistry.set(sub.id, socket);\n\n socket.on(\"open\", () => {\n if (sub.onOpen) dispatch(sub.onOpen());\n });\n socket.on(\"message\", (data: RawData) => {\n const msg = sub.onMessage(data.toString());\n if (msg !== null) dispatch(msg);\n });\n socket.on(\"close\", (code: number, reason: Buffer) => {\n if (sub.onClose) dispatch(sub.onClose(code, reason.toString()));\n });\n socket.on(\"error\", (err: Error) => {\n if (sub.onError) dispatch(sub.onError(err.message));\n });\n\n return () => {\n ctx.wsRegistry.delete(sub.id);\n socket.removeAllListeners();\n // Swallow teardown-induced errors: terminate()-ing a CONNECTING socket\n // emits an 'error' (\"closed before the connection was established\"),\n // and with our listeners gone that would be an unhandled 'error' that\n // crashes the process. A no-op listener absorbs it.\n socket.on(\"error\", () => {});\n // Close for ANY non-closed state: a graceful 1000 when OPEN, a hard\n // terminate() otherwise (CONNECTING/CLOSING) so a socket caught mid-\n // handshake can't dangle. This is the teardown the hand-driven Cmd-pair\n // approach (`cleanup_browser` closing the ws only `if OPEN`) could not\n // guarantee.\n if (socket.readyState === WebSocket.OPEN) {\n socket.close(1000, \"tea-node sub cleanup\");\n } else if (socket.readyState !== WebSocket.CLOSED) {\n socket.terminate();\n }\n };\n },\n\n node_timer: (sub, _ctx, dispatch) => {\n // Two reconcile lifecycles under one variant: a one-shot (`setTimeout`)\n // that `subscriptions(state)` should drop on the render after it fires,\n // vs a repeating (`setInterval`) one the state keeps listing across\n // renders. Same Sub.type; the `repeat` flag picks the install + cleanup.\n if (sub.repeat) {\n const handle = setInterval(() => dispatch(sub.msg), sub.delayMs);\n return () => clearInterval(handle);\n }\n const handle = setTimeout(() => dispatch(sub.msg), sub.delayMs);\n return () => clearTimeout(handle);\n },\n\n node_signal: (sub, _ctx, dispatch) => {\n const handler = () => dispatch(sub.msg);\n process.on(sub.signal, handler);\n return () => {\n process.off(sub.signal, handler);\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/node/index.ts"],"names":["handle"],"mappings":";;;;;AAyDO,SAAS,SAAA,CACd,MACA,KAAA,EACU;AACV,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,GAAyB;AAC7B,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,GAAA,GAAM,MAAM,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AAAA,MACnC,SAAS,GAAA,EAAK;AACZ,QAAA,IAAK,GAAA,CAA8B,IAAA,KAAS,QAAA,EAAU,OAAO,IAAA;AAC7D,QAAA,MAAM,GAAA;AAAA,MACR;AAIA,MAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,IACvB,CAAA;AAAA,IACA,MAAM,KAAK,KAAA,EAAyB;AAClC,MAAA,MAAM,MAAM,OAAA,CAAQ,IAAI,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAO9C,MAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,QAAQ,GAAG,CAAA,IAAA,CAAA;AAClC,MAAA,MAAM,UAAU,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,GAAG,MAAM,CAAA;AAClD,MAAA,MAAM,MAAA,CAAO,KAAK,IAAI,CAAA;AAAA,IACxB,CAAA;AAAA,IACA,QAAQ,GAAA,EAAwB;AAC9B,MAAA,OAAO,MAAM,GAAG,CAAA;AAAA,IAClB;AAAA,GACF;AACF;AAsFO,SAAS,eAAA,CACd,GAAA,EACA,EAAA,EACA,IAAA,EACS;AACT,EAAA,MAAM,MAAA,GAAS,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,EAAE,CAAA;AACpC,EAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,UAAA,KAAe,SAAA,CAAU,MAAM,OAAO,KAAA;AAC5D,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,KAAK,IAAI,CAAA;AAChB,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAaO,SAAS,aAAA,GAgBd;AACA,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAC,GAAA,EAAK,GAAA,EAAK,QAAA,KAAa;AAC/B,MAAA,MAAM,MAAA,GAAS,IAAI,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA;AACpC,MAAA,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,MAAM,CAAA;AAEjC,MAAA,MAAA,CAAO,EAAA,CAAG,QAAQ,MAAM;AACtB,QAAA,IAAI,GAAA,CAAI,MAAA,EAAQ,QAAA,CAAS,GAAA,CAAI,QAAQ,CAAA;AAAA,MACvC,CAAC,CAAA;AACD,MAAA,MAAA,CAAO,EAAA,CAAG,SAAA,EAAW,CAAC,IAAA,KAAkB;AACtC,QAAA,MAAM,GAAA,GAAM,GAAA,CAAI,SAAA,CAAU,IAAA,CAAK,UAAU,CAAA;AACzC,QAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,QAAA,CAAS,GAAG,CAAA;AAAA,MAChC,CAAC,CAAA;AACD,MAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,EAAc,MAAA,KAAmB;AACnD,QAAA,IAAI,GAAA,CAAI,SAAS,QAAA,CAAS,GAAA,CAAI,QAAQ,IAAA,EAAM,MAAA,CAAO,QAAA,EAAU,CAAC,CAAA;AAAA,MAChE,CAAC,CAAA;AACD,MAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAe;AACjC,QAAA,IAAI,IAAI,OAAA,EAAS,QAAA,CAAS,IAAI,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,MACpD,CAAC,CAAA;AAED,MAAA,OAAO,MAAM;AACX,QAAA,GAAA,CAAI,UAAA,CAAW,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA;AAC5B,QAAA,MAAA,CAAO,kBAAA,EAAmB;AAK1B,QAAA,MAAA,CAAO,EAAA,CAAG,SAAS,MAAM;AAAA,QAAC,CAAC,CAAA;AAM3B,QAAA,IAAI,MAAA,CAAO,UAAA,KAAe,SAAA,CAAU,IAAA,EAAM;AACxC,UAAA,MAAA,CAAO,KAAA,CAAM,KAAM,sBAAsB,CAAA;AAAA,QAC3C,CAAA,MAAA,IAAW,MAAA,CAAO,UAAA,KAAe,SAAA,CAAU,MAAA,EAAQ;AACjD,UAAA,MAAA,CAAO,SAAA,EAAU;AAAA,QACnB;AAAA,MACF,CAAA;AAAA,IACF,CAAA;AAAA,IAEA,UAAA,EAAY,CAAC,GAAA,EAAK,IAAA,EAAM,QAAA,KAAa;AAKnC,MAAA,IAAI,IAAI,MAAA,EAAQ;AACd,QAAA,MAAMA,OAAAA,GAAS,YAAY,MAAM,QAAA,CAAS,IAAI,GAAG,CAAA,EAAG,IAAI,OAAO,CAAA;AAC/D,QAAA,OAAO,MAAM,cAAcA,OAAM,CAAA;AAAA,MACnC;AACA,MAAA,MAAM,MAAA,GAAS,WAAW,MAAM,QAAA,CAAS,IAAI,GAAG,CAAA,EAAG,IAAI,OAAO,CAAA;AAC9D,MAAA,OAAO,MAAM,aAAa,MAAM,CAAA;AAAA,IAClC,CAAA;AAAA,IAEA,WAAA,EAAa,CAAC,GAAA,EAAK,IAAA,EAAM,QAAA,KAAa;AACpC,MAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AACtC,MAAA,OAAA,CAAQ,EAAA,CAAG,GAAA,CAAI,MAAA,EAAQ,OAAO,CAAA;AAC9B,MAAA,OAAO,MAAM;AACX,QAAA,OAAA,CAAQ,GAAA,CAAI,GAAA,CAAI,MAAA,EAAQ,OAAO,CAAA;AAAA,MACjC,CAAA;AAAA,IACF;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * @demlik/tea/node — Node host adapter for `@demlik/tea`.\n *\n * Every host adapter ships two halves (mirrors `@demlik/tea/do`):\n *\n * 1. `fileStore<S>(path, parse)` — a `Store<S>` over a JSON file on disk.\n * Atomic save (temp + rename). Absent file → `load()` resolves `null`\n * (fresh boot). Structurally malformed JSON throws at `load()` (an infra\n * error), matching `doStore`; a shape mismatch is NOT a throw — the\n * substrate calls `migrate`, which returns `null` and boots fresh.\n * Use for resumable scripts / local repro of prod machines. Ephemeral\n * CLIs can skip persistence with `memoryStore` from `@demlik/tea/mem`.\n *\n * 2. `nodeSubscribe<M, Ctx>()` — handler registry for the three node-native\n * Sub types this package owns:\n * - `node_ws` — opens a `ws` WebSocket; routes open/message/close/\n * error into Msgs. Registers the live socket in\n * `ctx.wsRegistry` so a Cmd handler can write to it\n * (see `sendToWebSocket`). Cleanup closes the socket\n * for ANY non-CLOSED state.\n * - `node_timer` — setTimeout / setInterval; cleanup clears it.\n * - `node_signal` — a process signal (SIGINT/SIGTERM/…) → Msg; cleanup\n * detaches the listener.\n *\n * Unlike tea-do — whose `alarm()` / `webSocketMessage()` are DO lifecycle\n * methods the handler can't observe from the outside, hence its registries —\n * a node process is continuously alive: each handler holds its resource in\n * closure and the returned cleanup closes it. The substrate's subscription\n * reconciler runs that cleanup the instant `subscriptions(state)` stops listing\n * the Sub — end-then-start choreography for free (Rule 9, [invariant 4]).\n * A lifecycle-bound resource (a socket, a timer) becomes impossible to leak\n * from a reducer cell, because no reducer cell owns its teardown.\n *\n * `ctx.wsRegistry` is the one shared surface: `node_ws` populates it; a Cmd\n * interpreter reads it to write to the open socket. Same shape as tea-do's\n * `wsRegistry`.\n */\n\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport WebSocket, { type RawData } from \"ws\";\nimport type { Store, Sub, SubId } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// fileStore — `Store<S>` over a JSON file.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * `Store<S>` backed by a JSON file at `path`.\n *\n * `parse` is REQUIRED because the file is a real serialization boundary — the\n * bytes that come back through `JSON.parse` are structurally `unknown`, and the\n * caller (who owns `S`) is the only party that can validate the shape.\n * Returning `null` from `parse` means \"no usable persisted state\" — the\n * substrate boots `init` with `loaded = null`. `parse` must NOT throw per the\n * `Store<S>.migrate` contract.\n */\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n): Store<S> {\n return {\n async load(): Promise<unknown> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n // JSON.parse throws on structural malformation — propagate (infra error),\n // matching `doStore`. The decoded value is intentionally returned as\n // `unknown`; the substrate's `migrate` callback is the boundary parse.\n return JSON.parse(raw);\n },\n async save(state: S): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n // Write to a pid-scoped temp then atomically rename. The rename gives\n // torn-write safety (a reader sees the old file or the new one, never a\n // partial) and survives process crash / Ctrl-C — the only failure a local\n // resumable run resumes from. fsync (file + dir) would add power-loss\n // durability the use case doesn't need; the substrate serializes saves\n // through run()'s queue, so there's never a second concurrent writer.\n const tmp = `${path}.${process.pid}.tmp`;\n await writeFile(tmp, JSON.stringify(state), \"utf8\");\n await rename(tmp, path);\n },\n migrate(raw: unknown): S | null {\n return parse(raw);\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// NodeSub — the three node-native subscription variants.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A node WebSocket sub. The substrate diffs subs by `id`; the same `id` across\n * transitions = the same socket (kept open). Emit a DIFFERENT `id` to force a\n * reconnect.\n *\n * Only `onMessage` returns `M | null` (null drops the frame) because inbound\n * frames are the boundary that needs a parse-and-maybe-discard seam. `onOpen`/\n * `onClose`/`onError` always dispatch — a spurious one is dropped in the reducer\n * (the cell ignores it), not here.\n */\nexport type NodeWsSub<M> = {\n id: SubId;\n type: \"node_ws\";\n url: string;\n onMessage: (data: string) => M | null;\n onOpen?: () => M;\n onClose?: (code: number, reason: string) => M;\n onError?: (message: string) => M;\n};\n\n/**\n * A node timer sub. One-shot `setTimeout` by default; `repeat: true` →\n * `setInterval`. Cleanup clears the timer — a timer keyed to a phase is\n * cancelled the moment the machine leaves that phase.\n */\nexport type NodeTimerSub<M> = {\n id: SubId;\n type: \"node_timer\";\n delayMs: number;\n msg: M;\n repeat?: boolean;\n};\n\n/**\n * A process-signal sub: `signal` (SIGINT/SIGTERM/…) → `msg`. Cleanup detaches\n * the listener. Lets graceful shutdown be a real machine transition rather than\n * an out-of-band `process.on` that the reducer can't see.\n */\nexport type NodeSignalSub<M> = {\n id: SubId;\n type: \"node_signal\";\n signal: NodeJS.Signals;\n msg: M;\n};\n\nexport type NodeSub<M> = NodeWsSub<M> | NodeTimerSub<M> | NodeSignalSub<M>;\n\n// Compile-time guarantee: `NodeSub<M>` is structurally a `Sub` (has `id`+`type`).\n// Erased at runtime — the `extends Sub` constraint on the alias's type\n// parameter fails to compile if `NodeSub` ever drifts off the `Sub` shape.\nexport type AssertNodeSubIsSub<T extends Sub = NodeSub<unknown>> = T;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Ctx — what the node Subs read / populate.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type NodeWsRegistry = Map<SubId, WebSocket>;\n\n/**\n * The Ctx fields the node Subs depend on. A consumer's Ctx must extend this so\n * `node_ws` can register the live socket and Cmd handlers can reach it.\n *\n * Deliberately NOT parameterized by Msg: the registry holds raw `WebSocket`s,\n * and each Sub variant carries its own Msg-typed callbacks. Trip-wire: if a\n * future node Sub holds a Msg-typed callback on ctx, this becomes\n * `NodeSubscribeCtx<M>`.\n */\nexport interface NodeSubscribeCtx {\n wsRegistry: NodeWsRegistry;\n}\n\n/**\n * Write a frame to a registered `node_ws` socket. No-op (returns `false`) when\n * the socket is absent, not OPEN, or `send()` throws. The Cmd-side companion to\n * the `node_ws` Sub: the Sub owns the socket's lifetime; this writes to it.\n *\n * The contract is a boolean no-op, never a throw: the socket can transition out\n * of OPEN between the `readyState` check and `send()` (or `ws` can throw on an\n * errored underlying connection), and a Cmd interpreter must not crash on that.\n */\nexport function sendToWebSocket(\n ctx: NodeSubscribeCtx,\n id: SubId,\n data: string,\n): boolean {\n const socket = ctx.wsRegistry.get(id);\n if (!socket || socket.readyState !== WebSocket.OPEN) return false;\n try {\n socket.send(data);\n return true;\n } catch {\n return false;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// nodeSubscribe — the handler registry.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build the `subscribe` handlers for `node_ws`, `node_timer`, and\n * `node_signal`. Matches `Machine#subscribe`'s\n * `[K in U[\"type\"]]: (sub, ctx, dispatch) => () => void` contract for the\n * `NodeSub<M>` variants. Generic `Ctx` must extend `NodeSubscribeCtx` so the\n * `node_ws` handler can register its socket.\n */\nexport function nodeSubscribe<M, Ctx extends NodeSubscribeCtx>(): {\n node_ws: (\n sub: NodeWsSub<M>,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n ) => () => void;\n node_timer: (\n sub: NodeTimerSub<M>,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n ) => () => void;\n node_signal: (\n sub: NodeSignalSub<M>,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n ) => () => void;\n} {\n return {\n node_ws: (sub, ctx, dispatch) => {\n const socket = new WebSocket(sub.url);\n ctx.wsRegistry.set(sub.id, socket);\n\n socket.on(\"open\", () => {\n if (sub.onOpen) dispatch(sub.onOpen());\n });\n socket.on(\"message\", (data: RawData) => {\n const msg = sub.onMessage(data.toString());\n if (msg !== null) dispatch(msg);\n });\n socket.on(\"close\", (code: number, reason: Buffer) => {\n if (sub.onClose) dispatch(sub.onClose(code, reason.toString()));\n });\n socket.on(\"error\", (err: Error) => {\n if (sub.onError) dispatch(sub.onError(err.message));\n });\n\n return () => {\n ctx.wsRegistry.delete(sub.id);\n socket.removeAllListeners();\n // Swallow teardown-induced errors: terminate()-ing a CONNECTING socket\n // emits an 'error' (\"closed before the connection was established\"),\n // and with our listeners gone that would be an unhandled 'error' that\n // crashes the process. A no-op listener absorbs it.\n socket.on(\"error\", () => {});\n // Close for ANY non-closed state: a graceful 1000 when OPEN, a hard\n // terminate() otherwise (CONNECTING/CLOSING) so a socket caught mid-\n // handshake can't dangle. This is the teardown the hand-driven Cmd-pair\n // approach (`cleanup_browser` closing the ws only `if OPEN`) could not\n // guarantee.\n if (socket.readyState === WebSocket.OPEN) {\n socket.close(1000, \"tea-node sub cleanup\");\n } else if (socket.readyState !== WebSocket.CLOSED) {\n socket.terminate();\n }\n };\n },\n\n node_timer: (sub, _ctx, dispatch) => {\n // Two reconcile lifecycles under one variant: a one-shot (`setTimeout`)\n // that `subscriptions(state)` should drop on the render after it fires,\n // vs a repeating (`setInterval`) one the state keeps listing across\n // renders. Same Sub.type; the `repeat` flag picks the install + cleanup.\n if (sub.repeat) {\n const handle = setInterval(() => dispatch(sub.msg), sub.delayMs);\n return () => clearInterval(handle);\n }\n const handle = setTimeout(() => dispatch(sub.msg), sub.delayMs);\n return () => clearTimeout(handle);\n },\n\n node_signal: (sub, _ctx, dispatch) => {\n const handler = () => dispatch(sub.msg);\n process.on(sub.signal, handler);\n return () => {\n process.off(sub.signal, handler);\n };\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { C as Cmd } from '../core-BoB6t7dB.js';
|
|
2
|
+
import { PaginatorState } from '../paginator/index.js';
|
|
3
|
+
import { CircuitConfig, RateLimitConfig, DeadlineConfig, ResilientState, RunCmd, SucceedMsg, FailMsg, ResilientTimerMsg, ResilientPorts } from '../resilient-call/index.js';
|
|
4
|
+
import { RetryPolicy } from '../retry-backoff/index.js';
|
|
5
|
+
import { DeadlineSub } from '../deadline/index.js';
|
|
6
|
+
export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
|
|
7
|
+
import '../cache/index.js';
|
|
8
|
+
import '../types-B-8bcAp8.js';
|
|
9
|
+
import '../circuit-breaker/index.js';
|
|
10
|
+
import '../protocol-B5YmJr0d.js';
|
|
11
|
+
import '../rate-limit/index.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @demlik/tea/paginated-walk — traverse a paginated API / sitemap end to end
|
|
15
|
+
* WITHOUT a fake clock, without 429s, and resumable across a Durable-Object
|
|
16
|
+
* eviction. The L2 composition that bolts `../resilient-call`'s resilience onto
|
|
17
|
+
* `../paginator`'s cursor walk: each page is fetched as ONE keyed resilient
|
|
18
|
+
* call (retry + backoff + rate-limit gate), and the cursor advances only once a
|
|
19
|
+
* page settles `ok`.
|
|
20
|
+
*
|
|
21
|
+
* ## The recipe (the two siblings it composes)
|
|
22
|
+
*
|
|
23
|
+
* - **`../paginator`** owns the walk loop: the opaque `Cursor`, the `seen` /
|
|
24
|
+
* `pages` counters, and the `idle → fetching → … → done` phase machine.
|
|
25
|
+
* `paginated-walk` never re-implements cursor advancement — it folds each
|
|
26
|
+
* settled page through `recordPage`, letting the paginator decide
|
|
27
|
+
* `done` / `fetching(next)` / `paused`. Backpressure is a TWO-way valve:
|
|
28
|
+
* `recordPage` parks the walk in `paused` once `seen >= highWaterMark`, and
|
|
29
|
+
* the `drain` / `resume` verbs (delegating to the paginator's own
|
|
30
|
+
* `drain` / `resume`) lower the gauge and re-open the parked fetch — without
|
|
31
|
+
* them `highWaterMark` would be a one-way valve that strands a paused walk.
|
|
32
|
+
* - **`../resilient-call`** owns the page fetch: the single `resilient_run`
|
|
33
|
+
* effect per page is gated through that knob's cache → circuit → rate-limit
|
|
34
|
+
* → retry pipeline. A transient page failure backs off and re-issues the
|
|
35
|
+
* SAME cursor (idempotent retry); the walk does not advance until the page
|
|
36
|
+
* genuinely settles.
|
|
37
|
+
*
|
|
38
|
+
* Those are the only two siblings this module calls into. The resilience knobs
|
|
39
|
+
* (`retry` / `circuit` / `rateLimit` / `deadline`) are NOT separate bricks
|
|
40
|
+
* `paginated-walk` composes — they belong to `resilient-call`, which owns the
|
|
41
|
+
* rate-limit gate internally. This module merely passes them through to
|
|
42
|
+
* `createResilientCall` and re-uses two config TYPES at the boundary:
|
|
43
|
+
* `RateLimitConfig` (re-exported by `resilient-call`) and `RetryPolicy` (from
|
|
44
|
+
* `../retry-backoff`). Pass a `rateLimit` and `resilient-call`'s token bucket
|
|
45
|
+
* throttles page fetches ("don't 429 the upstream"); omit it and the walk runs
|
|
46
|
+
* as fast as the upstream answers.
|
|
47
|
+
*
|
|
48
|
+
* Because the page-fetch resilience IS a `resilient-call` slice, this knob
|
|
49
|
+
* **inherits that knob's contract verbatim**: the slice is a flat record, every
|
|
50
|
+
* verb returns `readonly [State, Cmd[]]`, time is always an `at` parameter, and
|
|
51
|
+
* the jitter RNG is injected once at `createPaginatedWalk(config, rng)` — never
|
|
52
|
+
* read inside a verb (per the spec, `paginated-walk` *extends* `resilient-call`).
|
|
53
|
+
*
|
|
54
|
+
* ## The two non-negotiables (canon)
|
|
55
|
+
*
|
|
56
|
+
* - **Durable** — the slice is plain data: the paginator phase + counters, and
|
|
57
|
+
* the resilient-call brick states (all JSON-serializable). The cursor lives
|
|
58
|
+
* in the Model, not a closure, so a walk survives eviction mid-traversal and
|
|
59
|
+
* resumes from the exact page it was fetching.
|
|
60
|
+
* - **Replayable** — every transition is a verb returning new state + Cmds.
|
|
61
|
+
* `nextCursor` / `onPage` are pure config functions applied at the verb
|
|
62
|
+
* boundary; the Cmds they yield are plain data. `replay` reconstructs the
|
|
63
|
+
* whole walk exactly.
|
|
64
|
+
*
|
|
65
|
+
* ## Where the clock / RNG live
|
|
66
|
+
*
|
|
67
|
+
* Inside the verbs: nowhere. `at` arrives on `start` / `pageOk` / `pageErr` /
|
|
68
|
+
* `onTimer` and is threaded straight into the underlying resilient-call gate (so
|
|
69
|
+
* a rate-limited or retried page measures backoff from that `at`). The jitter
|
|
70
|
+
* RNG is injected at construction. The only clock read is `Date.now()` inside
|
|
71
|
+
* the resilient-call `handlers` port (the effect boundary), inherited unchanged.
|
|
72
|
+
*
|
|
73
|
+
* ## The fixed fetch key
|
|
74
|
+
*
|
|
75
|
+
* A walk fetches one page at a time, so the resilient-call layer tracks exactly
|
|
76
|
+
* one logical call under the constant key `PAGE_KEY`. The cursor being fetched
|
|
77
|
+
* is the resilient call's `input`, so a post-eviction resume re-issues the same
|
|
78
|
+
* page request. (`resilient-call` is keyed to support N concurrent calls; a walk
|
|
79
|
+
* deliberately uses a single key — the paginator is the sequencer.)
|
|
80
|
+
*
|
|
81
|
+
* ## Typical wiring
|
|
82
|
+
*
|
|
83
|
+
* const walk = createPaginatedWalk<PageToken, ApiPage>({
|
|
84
|
+
* firstPage: null, // the API's "page one" sentinel
|
|
85
|
+
* nextCursor: (page) => page.nextToken ?? null,
|
|
86
|
+
* onPage: (page) => [{ type: "index_rows", rows: page.items }],
|
|
87
|
+
* rateLimit: { capacity: 10, refillPerSec: 5 },
|
|
88
|
+
* retry: defaultRetryPolicy,
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* // in the machine:
|
|
92
|
+
* init: () => [{ walk: walk.init() }, []],
|
|
93
|
+
* update: {
|
|
94
|
+
* start: (s, m) => lift(s, walk.start(s.walk, m.at)),
|
|
95
|
+
* page_ok: (s, m) => lift(s, walk.pageOk(s.walk, m.result, m.at)),
|
|
96
|
+
* page_err: (s, m) => lift(s, walk.pageErr(s.walk, m.error, m.at)),
|
|
97
|
+
* retry_due:(s, m) => lift(s, walk.onTimer(s.walk, m)),
|
|
98
|
+
* drained: (s, m) => lift(s, walk.drain(s.walk, m.n)),
|
|
99
|
+
* resume: (s, m) => lift(s, walk.resume(s.walk, m.at)),
|
|
100
|
+
* },
|
|
101
|
+
* subscriptions: (s) => walk.subs(s.walk),
|
|
102
|
+
* subscribe: { deadline: subscribeDeadline },
|
|
103
|
+
* interpret: walk.handlers({ run: (cursor) => api.fetchPage(cursor) }),
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The paginated-walk knob. Generic in `Cursor` (the opaque page handle the
|
|
108
|
+
* upstream API hands back — offset, page token, "next" URL, …) and `Page` (the
|
|
109
|
+
* shape your fetch port resolves with).
|
|
110
|
+
*
|
|
111
|
+
* `nextCursor` and `onPage` are PURE config functions applied at the verb
|
|
112
|
+
* boundary, never stored in the slice or in a Cmd (invariant 3 — Cmds are
|
|
113
|
+
* plain data, not closures). `nextCursor(page)` extracts the API's next-page
|
|
114
|
+
* handle (or `null` at the end); `onPage(page)` yields the Cmds the consumer
|
|
115
|
+
* wants emitted for each successfully fetched page (index rows, write to R2,
|
|
116
|
+
* dispatch a downstream Msg — whatever; an empty array is fine).
|
|
117
|
+
*
|
|
118
|
+
* The resilience bricks (`retry` / `circuit` / `rateLimit` / `deadline`) are
|
|
119
|
+
* the SAME optional knobs `resilient-call` exposes — paginated-walk extends it.
|
|
120
|
+
* `rateLimit` is the brick that keeps the walk from 429-ing the upstream.
|
|
121
|
+
*/
|
|
122
|
+
interface PaginatedWalkConfig<Cursor, Page, EmittedCmd extends Cmd> {
|
|
123
|
+
/**
|
|
124
|
+
* The cursor the walk fetches first. For an offset API this is typically `0`;
|
|
125
|
+
* for a token API, whatever sentinel the API treats as "page one" (often
|
|
126
|
+
* `null` or `""`). Threaded into `paginator`'s `firstCursor`.
|
|
127
|
+
*/
|
|
128
|
+
readonly firstPage: Cursor;
|
|
129
|
+
/**
|
|
130
|
+
* Extract the next cursor from a just-fetched page. Return a `Cursor` to keep
|
|
131
|
+
* walking, or `null` when the API signalled the end (the walk finishes). PURE
|
|
132
|
+
* + consumer-specific (only the consumer knows the API's pagination shape).
|
|
133
|
+
*/
|
|
134
|
+
readonly nextCursor: (page: Page) => Cursor | null;
|
|
135
|
+
/**
|
|
136
|
+
* The Cmds to emit for each successfully fetched page — the consumer's work
|
|
137
|
+
* per page (index, persist, fan out, …). Returns plain-data Cmds; `[]` is a
|
|
138
|
+
* valid "I process the page synchronously elsewhere" answer. PURE.
|
|
139
|
+
*/
|
|
140
|
+
readonly onPage: (page: Page) => readonly EmittedCmd[];
|
|
141
|
+
/**
|
|
142
|
+
* How many items a page contributes to the paginator's backpressure gauge
|
|
143
|
+
* (`seen`). Defaults to `1` per page (count pages, not items) when omitted; a
|
|
144
|
+
* consumer that wants per-item backpressure returns `page.items.length`. PURE.
|
|
145
|
+
*/
|
|
146
|
+
readonly pageSize?: (page: Page) => number;
|
|
147
|
+
/**
|
|
148
|
+
* Backpressure threshold handed to the paginator. `seen >= highWaterMark`
|
|
149
|
+
* parks the walk in `paused` instead of fetching the next page. Non-positive
|
|
150
|
+
* (the default `0`) disables backpressure — the walk runs flat out until the
|
|
151
|
+
* API exhausts. See `../paginator`'s `highWaterMark` docs.
|
|
152
|
+
*/
|
|
153
|
+
readonly highWaterMark?: number;
|
|
154
|
+
/** Exponential-backoff retry policy for a transient page-fetch failure. */
|
|
155
|
+
readonly retry?: RetryPolicy;
|
|
156
|
+
/** Circuit breaker around the page fetch (one upstream → one breaker). */
|
|
157
|
+
readonly circuit?: CircuitConfig;
|
|
158
|
+
/** Token-bucket rate limit — the "don't 429 the upstream" knob. */
|
|
159
|
+
readonly rateLimit?: RateLimitConfig;
|
|
160
|
+
/** Overall wall-clock deadline per individual page fetch. */
|
|
161
|
+
readonly deadline?: DeadlineConfig;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The slice. `walk` is the cursor/phase/counters from `../paginator`;
|
|
165
|
+
* `resilience` is the `../resilient-call` slice that gates the page fetch.
|
|
166
|
+
* Every field is JSON-serializable, so the whole walk survives a reload.
|
|
167
|
+
*
|
|
168
|
+
* The two move in lockstep: `walk` decides WHICH cursor to fetch next, while
|
|
169
|
+
* `resilience` decides HOW that fetch is attempted (gated, retried, backed
|
|
170
|
+
* off). `pageOk` advances `walk`; `pageErr` defers to `resilience` and leaves
|
|
171
|
+
* `walk` parked on the same cursor until the page truly settles.
|
|
172
|
+
*/
|
|
173
|
+
interface PaginatedWalkState<Cursor, Page> {
|
|
174
|
+
readonly walk: PaginatorState<Cursor>;
|
|
175
|
+
readonly resilience: ResilientState<Cursor, Page>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The page-fetch effect this knob emits: the inherited `resilient_run` Cmd from
|
|
179
|
+
* resilient-call, whose `input` is the `Cursor` to fetch and whose `key` is the
|
|
180
|
+
* fixed `PAGE_KEY`. The consumer's `handlers(ports)` interprets it.
|
|
181
|
+
*/
|
|
182
|
+
type FetchPageCmd<Cursor> = RunCmd<Cursor>;
|
|
183
|
+
/** Page-settled Msgs the `handlers` port dispatches back (inherited verbatim). */
|
|
184
|
+
type PageOkMsg<Page> = SucceedMsg<Page>;
|
|
185
|
+
type PageErrMsg = FailMsg;
|
|
186
|
+
/** The retry / deadline timer Msg — inherited from resilient-call. */
|
|
187
|
+
type PaginatedWalkTimerMsg = ResilientTimerMsg;
|
|
188
|
+
/**
|
|
189
|
+
* Ports the consumer supplies to `handlers`. `run(cursor)` fetches ONE page for
|
|
190
|
+
* the given cursor; throwing routes to `pageErr` (and thus the resilience
|
|
191
|
+
* backoff). Same shape as resilient-call's port, specialized to the page fetch.
|
|
192
|
+
*/
|
|
193
|
+
interface PaginatedWalkPorts<Cursor, Page> extends ResilientPorts<Cursor, Page> {
|
|
194
|
+
}
|
|
195
|
+
/** The single resilient-call key every page fetch runs under. */
|
|
196
|
+
declare const PAGE_KEY = "page";
|
|
197
|
+
/**
|
|
198
|
+
* Build a paginated-walk knob from `config`. `rng` is injected for the
|
|
199
|
+
* underlying retry jitter — pass a fixed `() => 0` in tests to pin backoff;
|
|
200
|
+
* defaults to `Math.random` (read at the verb boundary inside resilient-call,
|
|
201
|
+
* never in a `paginated-walk` verb). Inherited straight from resilient-call.
|
|
202
|
+
*
|
|
203
|
+
* Returns the uniform L2 knob contract: `init()`, the verbs `start` / `pageOk`
|
|
204
|
+
* / `pageErr` / `onTimer`, `subs(state)`, and `handlers(ports)`. `Cursor` is
|
|
205
|
+
* the page handle, `Page` the fetch result, `EmittedCmd` the consumer's
|
|
206
|
+
* per-page Cmds plus the inherited `FetchPageCmd`.
|
|
207
|
+
*/
|
|
208
|
+
declare function createPaginatedWalk<Cursor, Page, EmittedCmd extends Cmd = Cmd>(config: PaginatedWalkConfig<Cursor, Page, EmittedCmd>, rng?: () => number): {
|
|
209
|
+
init: () => PaginatedWalkState<Cursor, Page>;
|
|
210
|
+
start: (s: PaginatedWalkState<Cursor, Page>, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
|
|
211
|
+
pageOk: (s: PaginatedWalkState<Cursor, Page>, page: Page, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
|
|
212
|
+
pageErr: (s: PaginatedWalkState<Cursor, Page>, error: unknown, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
|
|
213
|
+
onTimer: (s: PaginatedWalkState<Cursor, Page>, msg: PaginatedWalkTimerMsg) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
|
|
214
|
+
drain: (s: PaginatedWalkState<Cursor, Page>, n: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
|
|
215
|
+
resume: (s: PaginatedWalkState<Cursor, Page>, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
|
|
216
|
+
isComplete: (s: PaginatedWalkState<Cursor, Page>) => boolean;
|
|
217
|
+
failure: (s: PaginatedWalkState<Cursor, Page>) => unknown;
|
|
218
|
+
isStuck: (s: PaginatedWalkState<Cursor, Page>) => boolean;
|
|
219
|
+
subs: (s: PaginatedWalkState<Cursor, Page>) => readonly DeadlineSub[];
|
|
220
|
+
handlers: (ports: PaginatedWalkPorts<Cursor, Page>) => {
|
|
221
|
+
resilient_run: (cmd: RunCmd<Cursor>, ctx: Readonly<Record<never, never>>) => Promise<FailMsg | SucceedMsg<Page>>;
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice
|
|
226
|
+
* lives at `state.walk`. A convenience for the common single-slice host;
|
|
227
|
+
* consumers with a differently-named field spread by hand. Pure — a thin record
|
|
228
|
+
* rebuild, no clock / RNG.
|
|
229
|
+
*/
|
|
230
|
+
declare function liftWalk<S extends {
|
|
231
|
+
walk: PaginatedWalkState<Cursor, Page>;
|
|
232
|
+
}, Cursor, Page, C extends Cmd>(state: S, [slice, cmds]: readonly [PaginatedWalkState<Cursor, Page>, readonly C[]]): readonly [S, readonly C[]];
|
|
233
|
+
|
|
234
|
+
export { DeadlineSub, type FetchPageCmd, PAGE_KEY, type PageErrMsg, type PageOkMsg, type PaginatedWalkConfig, type PaginatedWalkPorts, type PaginatedWalkState, type PaginatedWalkTimerMsg, createPaginatedWalk, liftWalk };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { PAGE_KEY, createPaginatedWalk, liftWalk } from '../chunk-5DEQ5I3U.js';
|
|
2
|
+
import '../chunk-SINXE47Y.js';
|
|
3
|
+
import '../chunk-27N2SPTN.js';
|
|
4
|
+
import '../chunk-JELSZN25.js';
|
|
5
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-K7Q23EQ4.js';
|
|
6
|
+
import '../chunk-5VNWZADP.js';
|
|
7
|
+
import '../chunk-2YOE5PIS.js';
|
|
8
|
+
import '../chunk-SC3FAFZN.js';
|
|
9
|
+
import '../chunk-IN33Q4VJ.js';
|
|
10
|
+
import '../chunk-NSDFA6M6.js';
|
|
11
|
+
import '../chunk-2YFLOCWX.js';
|
|
12
|
+
import '../chunk-MMH7CW5K.js';
|
|
13
|
+
import '../chunk-QLA76GIT.js';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|