@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.
Files changed (243) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/chunk-27N2SPTN.js +222 -0
  18. package/dist/chunk-27N2SPTN.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-3XJWC7VW.js +61 -0
  24. package/dist/chunk-3XJWC7VW.js.map +1 -0
  25. package/dist/chunk-5DEQ5I3U.js +132 -0
  26. package/dist/chunk-5DEQ5I3U.js.map +1 -0
  27. package/dist/chunk-5VNWZADP.js +49 -0
  28. package/dist/chunk-5VNWZADP.js.map +1 -0
  29. package/dist/chunk-6RGBKODV.js +268 -0
  30. package/dist/chunk-6RGBKODV.js.map +1 -0
  31. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  32. package/dist/chunk-7FYQGDMF.js.map +1 -0
  33. package/dist/chunk-7W4LMPUW.js +186 -0
  34. package/dist/chunk-7W4LMPUW.js.map +1 -0
  35. package/dist/chunk-BYQUGDD3.js +16 -0
  36. package/dist/chunk-BYQUGDD3.js.map +1 -0
  37. package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
  38. package/dist/chunk-DILMKOZ7.js.map +1 -0
  39. package/dist/chunk-EHIX7NYZ.js +93 -0
  40. package/dist/chunk-EHIX7NYZ.js.map +1 -0
  41. package/dist/chunk-HCEVHFJ7.js +54 -0
  42. package/dist/chunk-HCEVHFJ7.js.map +1 -0
  43. package/dist/chunk-HJO6VNQI.js +15 -0
  44. package/dist/chunk-HJO6VNQI.js.map +1 -0
  45. package/dist/chunk-IN33Q4VJ.js +52 -0
  46. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  47. package/dist/chunk-J6527VRQ.js +56 -0
  48. package/dist/chunk-J6527VRQ.js.map +1 -0
  49. package/dist/chunk-JELSZN25.js +47 -0
  50. package/dist/chunk-JELSZN25.js.map +1 -0
  51. package/dist/chunk-K575PT4S.js +58 -0
  52. package/dist/chunk-K575PT4S.js.map +1 -0
  53. package/dist/chunk-K7Q23EQ4.js +20 -0
  54. package/dist/chunk-K7Q23EQ4.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
  58. package/dist/chunk-LP7VQBRG.js.map +1 -0
  59. package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
  60. package/dist/chunk-MMH7CW5K.js.map +1 -0
  61. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  62. package/dist/chunk-NSDFA6M6.js.map +1 -0
  63. package/dist/chunk-ON24RKSE.js +60 -0
  64. package/dist/chunk-ON24RKSE.js.map +1 -0
  65. package/dist/chunk-PS23H6KQ.js +87 -0
  66. package/dist/chunk-PS23H6KQ.js.map +1 -0
  67. package/dist/chunk-QLA76GIT.js +162 -0
  68. package/dist/chunk-QLA76GIT.js.map +1 -0
  69. package/dist/chunk-S3FQN4SL.js +506 -0
  70. package/dist/chunk-S3FQN4SL.js.map +1 -0
  71. package/dist/chunk-SC3FAFZN.js +53 -0
  72. package/dist/chunk-SC3FAFZN.js.map +1 -0
  73. package/dist/chunk-SINXE47Y.js +55 -0
  74. package/dist/chunk-SINXE47Y.js.map +1 -0
  75. package/dist/chunk-SQTCS44W.js +33 -0
  76. package/dist/chunk-SQTCS44W.js.map +1 -0
  77. package/dist/chunk-TGG26XMK.js +52 -0
  78. package/dist/chunk-TGG26XMK.js.map +1 -0
  79. package/dist/chunk-UV6UWGEO.js +147 -0
  80. package/dist/chunk-UV6UWGEO.js.map +1 -0
  81. package/dist/chunk-UYWUVEP6.js +158 -0
  82. package/dist/chunk-UYWUVEP6.js.map +1 -0
  83. package/dist/chunk-VLGARN6H.js +202 -0
  84. package/dist/chunk-VLGARN6H.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-BoB6t7dB.d.ts +188 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.d.ts +6 -0
  92. package/dist/debounce/index.js +1 -54
  93. package/dist/debounce/index.js.map +1 -1
  94. package/dist/devtools/index.d.ts +1 -1
  95. package/dist/devtools/index.js +18 -6
  96. package/dist/devtools/index.js.map +1 -1
  97. package/dist/do/index.d.ts +1723 -92
  98. package/dist/do/index.js +673 -31
  99. package/dist/do/index.js.map +1 -1
  100. package/dist/durable-effects-CO9Uh971.d.ts +180 -0
  101. package/dist/extension/index.d.ts +31 -13
  102. package/dist/extension/index.js +5 -3
  103. package/dist/extension/index.js.map +1 -1
  104. package/dist/extension/react.js +8 -3
  105. package/dist/extension/react.js.map +1 -1
  106. package/dist/extension/subs/index.d.ts +2 -2
  107. package/dist/extension/subs/index.js.map +1 -1
  108. package/dist/extension/test-utils.js +11 -3
  109. package/dist/extension/test-utils.js.map +1 -1
  110. package/dist/fan-out/index.d.ts +239 -0
  111. package/dist/fan-out/index.js +5 -0
  112. package/dist/fan-out/index.js.map +1 -0
  113. package/dist/idempotency/adapter.d.ts +62 -0
  114. package/dist/idempotency/adapter.js +4 -0
  115. package/dist/idempotency/adapter.js.map +1 -0
  116. package/dist/idempotency/index.js +1 -56
  117. package/dist/idempotency/index.js.map +1 -1
  118. package/dist/idempotent-intake/index.d.ts +186 -0
  119. package/dist/idempotent-intake/index.js +7 -0
  120. package/dist/idempotent-intake/index.js.map +1 -0
  121. package/dist/index.d.ts +341 -191
  122. package/dist/index.js +2 -1
  123. package/dist/llm-call/index.d.ts +309 -0
  124. package/dist/llm-call/index.js +14 -0
  125. package/dist/llm-call/index.js.map +1 -0
  126. package/dist/machine-viz/index.d.ts +4 -4
  127. package/dist/machine-viz/index.js +5 -9
  128. package/dist/machine-viz/index.js.map +1 -1
  129. package/dist/mem/index.d.ts +1 -0
  130. package/dist/monitored-run/index.d.ts +253 -0
  131. package/dist/monitored-run/index.js +10 -0
  132. package/dist/monitored-run/index.js.map +1 -0
  133. package/dist/node/index.d.ts +5 -5
  134. package/dist/node/index.js.map +1 -1
  135. package/dist/paginated-walk/index.d.ts +234 -0
  136. package/dist/paginated-walk/index.js +15 -0
  137. package/dist/paginated-walk/index.js.map +1 -0
  138. package/dist/paginator/index.d.ts +271 -0
  139. package/dist/paginator/index.js +3 -0
  140. package/dist/paginator/index.js.map +1 -0
  141. package/dist/parity/index.d.ts +92 -0
  142. package/dist/parity/index.js +78 -0
  143. package/dist/parity/index.js.map +1 -0
  144. package/dist/pbt/arbitraries/index.d.ts +1 -1
  145. package/dist/pbt/arbitraries/index.js +1 -1
  146. package/dist/pbt/index.d.ts +15 -8
  147. package/dist/pbt/index.js +6 -11
  148. package/dist/pbt/index.js.map +1 -1
  149. package/dist/pbt/runners/index.d.ts +1 -1
  150. package/dist/pbt/runners/index.js +3 -2
  151. package/dist/poller/index.d.ts +288 -0
  152. package/dist/poller/index.js +79 -0
  153. package/dist/poller/index.js.map +1 -0
  154. package/dist/prediction/index.d.ts +130 -0
  155. package/dist/prediction/index.js +4 -0
  156. package/dist/prediction/index.js.map +1 -0
  157. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  158. package/dist/pure/index.d.ts +2 -0
  159. package/dist/pure/index.js +4 -0
  160. package/dist/pure/index.js.map +1 -0
  161. package/dist/raft/demo.d.ts +105 -0
  162. package/dist/raft/demo.js +172 -0
  163. package/dist/raft/demo.js.map +1 -0
  164. package/dist/raft/index.d.ts +548 -0
  165. package/dist/raft/index.js +7 -0
  166. package/dist/raft/index.js.map +1 -0
  167. package/dist/raft/sim.d.ts +222 -0
  168. package/dist/raft/sim.js +8 -0
  169. package/dist/raft/sim.js.map +1 -0
  170. package/dist/raft/timeline.d.ts +46 -0
  171. package/dist/raft/timeline.js +62 -0
  172. package/dist/raft/timeline.js.map +1 -0
  173. package/dist/rate-limit/index.js +1 -43
  174. package/dist/rate-limit/index.js.map +1 -1
  175. package/dist/react/index.d.ts +2 -1
  176. package/dist/react/index.js +43 -20
  177. package/dist/react/index.js.map +1 -1
  178. package/dist/reconciler/index.d.ts +192 -0
  179. package/dist/reconciler/index.js +166 -0
  180. package/dist/reconciler/index.js.map +1 -0
  181. package/dist/recorder/index.d.ts +7 -3
  182. package/dist/recorder/index.js +1 -155
  183. package/dist/recorder/index.js.map +1 -1
  184. package/dist/resilient-call/index.d.ts +294 -0
  185. package/dist/resilient-call/index.js +13 -0
  186. package/dist/resilient-call/index.js.map +1 -0
  187. package/dist/retry-backoff/index.d.ts +42 -5
  188. package/dist/retry-backoff/index.js +1 -35
  189. package/dist/retry-backoff/index.js.map +1 -1
  190. package/dist/retry-to-success/index.d.ts +109 -0
  191. package/dist/retry-to-success/index.js +35 -0
  192. package/dist/retry-to-success/index.js.map +1 -0
  193. package/dist/saga/index.d.ts +238 -0
  194. package/dist/saga/index.js +150 -0
  195. package/dist/saga/index.js.map +1 -0
  196. package/dist/snapshot/index.d.ts +334 -0
  197. package/dist/snapshot/index.js +5 -0
  198. package/dist/snapshot/index.js.map +1 -0
  199. package/dist/subs/index.d.ts +48 -10
  200. package/dist/subs/index.js +1 -1
  201. package/dist/testing/index.d.ts +107 -2
  202. package/dist/testing/index.js +204 -6
  203. package/dist/testing/index.js.map +1 -1
  204. package/dist/throttle/index.d.ts +9 -0
  205. package/dist/throttle/index.js +1 -52
  206. package/dist/throttle/index.js.map +1 -1
  207. package/dist/throttled-input/index.d.ts +380 -0
  208. package/dist/throttled-input/index.js +88 -0
  209. package/dist/throttled-input/index.js.map +1 -0
  210. package/dist/token-refresh/index.d.ts +215 -0
  211. package/dist/token-refresh/index.js +6 -0
  212. package/dist/token-refresh/index.js.map +1 -0
  213. package/dist/trace-replay/index.d.ts +11 -2
  214. package/dist/trace-replay/index.js +3 -55
  215. package/dist/trace-replay/index.js.map +1 -1
  216. package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
  217. package/dist/with-deadline/index.d.ts +196 -0
  218. package/dist/with-deadline/index.js +111 -0
  219. package/dist/with-deadline/index.js.map +1 -0
  220. package/dist/with-resilience/index.d.ts +205 -0
  221. package/dist/with-resilience/index.js +251 -0
  222. package/dist/with-resilience/index.js.map +1 -0
  223. package/dist/with-telemetry/index.d.ts +149 -0
  224. package/dist/with-telemetry/index.js +70 -0
  225. package/dist/with-telemetry/index.js.map +1 -0
  226. package/dist/work-queue/adapter.d.ts +82 -0
  227. package/dist/work-queue/adapter.js +4 -0
  228. package/dist/work-queue/adapter.js.map +1 -0
  229. package/dist/work-queue/index.d.ts +25 -11
  230. package/dist/work-queue/index.js +1 -46
  231. package/dist/work-queue/index.js.map +1 -1
  232. package/dist/work-queue/ops.d.ts +69 -0
  233. package/dist/work-queue/ops.js +3 -0
  234. package/dist/work-queue/ops.js.map +1 -0
  235. package/dist/workflow/index.d.ts +450 -0
  236. package/dist/workflow/index.js +225 -0
  237. package/dist/workflow/index.js.map +1 -0
  238. package/package.json +168 -25
  239. package/dist/chunk-CMG2KMWX.js.map +0 -1
  240. package/dist/chunk-PYBQREFU.js.map +0 -1
  241. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  242. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  243. 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"}
@@ -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 (unlike tea-do's `DoSubscribeCtx<M>`):
103
- * the registry holds raw `WebSocket`s, and each Sub variant carries its own
104
- * Msg-typed callbacks. Trip-wire: if a future node Sub holds a Msg-typed
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 {
@@ -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":"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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 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"}