@demlik/tea 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +49 -0
  2. package/dist/agent/index.d.ts +16 -770
  3. package/dist/agent/index.js +18 -16
  4. package/dist/authed-call/index.d.ts +3 -3
  5. package/dist/authed-call/index.js +22 -36
  6. package/dist/authed-call/index.js.map +1 -1
  7. package/dist/await-terminal/index.d.ts +2 -2
  8. package/dist/await-terminal/index.js +2 -2
  9. package/dist/batch-window/index.d.ts +3 -3
  10. package/dist/batch-window/index.js +3 -3
  11. package/dist/cache/index.d.ts +2 -2
  12. package/dist/cache/index.js +5 -2
  13. package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
  14. package/dist/chunk-25LBDCBC.js.map +1 -0
  15. package/dist/{chunk-IK4S5DJN.js → chunk-2AHAR7CX.js} +7 -14
  16. package/dist/chunk-2AHAR7CX.js.map +1 -0
  17. package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
  18. package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
  19. package/dist/{chunk-VFGKTGGC.js → chunk-5W7CNAHB.js} +191 -178
  20. package/dist/chunk-5W7CNAHB.js.map +1 -0
  21. package/dist/{chunk-6GIGQT6C.js → chunk-B5RAZVXW.js} +12 -11
  22. package/dist/chunk-B5RAZVXW.js.map +1 -0
  23. package/dist/{chunk-6VDCTR7N.js → chunk-D4NJBQAH.js} +258 -151
  24. package/dist/chunk-D4NJBQAH.js.map +1 -0
  25. package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
  26. package/dist/chunk-D736QUGF.js.map +1 -0
  27. package/dist/{chunk-EGWTT5XJ.js → chunk-ET6QQNSQ.js} +76 -95
  28. package/dist/chunk-ET6QQNSQ.js.map +1 -0
  29. package/dist/chunk-EWJP3YYP.js +21 -0
  30. package/dist/chunk-EWJP3YYP.js.map +1 -0
  31. package/dist/{chunk-M6UZT7HS.js → chunk-FCKP6N7Y.js} +6 -5
  32. package/dist/chunk-FCKP6N7Y.js.map +1 -0
  33. package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
  34. package/dist/chunk-FV3Z5MBE.js.map +1 -0
  35. package/dist/chunk-G5E74IJB.js +8 -0
  36. package/dist/chunk-G5E74IJB.js.map +1 -0
  37. package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
  38. package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
  39. package/dist/{chunk-FOGL3TC7.js → chunk-JXYYRKD6.js} +8 -10
  40. package/dist/chunk-JXYYRKD6.js.map +1 -0
  41. package/dist/{chunk-ZDRN3YCE.js → chunk-LJJV6ZD5.js} +17 -24
  42. package/dist/chunk-LJJV6ZD5.js.map +1 -0
  43. package/dist/chunk-LYLDCIPH.js +365 -0
  44. package/dist/chunk-LYLDCIPH.js.map +1 -0
  45. package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
  46. package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
  47. package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
  48. package/dist/chunk-MUUROGIH.js.map +1 -0
  49. package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
  50. package/dist/chunk-PBDYW2TU.js.map +1 -0
  51. package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
  52. package/dist/chunk-Q2UM7BXU.js.map +1 -0
  53. package/dist/chunk-RMX4EOQK.js +26 -0
  54. package/dist/chunk-RMX4EOQK.js.map +1 -0
  55. package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
  56. package/dist/chunk-S2EGRPCM.js.map +1 -0
  57. package/dist/{chunk-5KVHID6A.js → chunk-SPWVPIFQ.js} +4 -4
  58. package/dist/{chunk-5KVHID6A.js.map → chunk-SPWVPIFQ.js.map} +1 -1
  59. package/dist/chunk-SSHDBOXF.js +71 -0
  60. package/dist/chunk-SSHDBOXF.js.map +1 -0
  61. package/dist/{chunk-6OUDPSSG.js → chunk-TWDHNIY6.js} +3 -3
  62. package/dist/{chunk-6OUDPSSG.js.map → chunk-TWDHNIY6.js.map} +1 -1
  63. package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
  64. package/dist/chunk-UF4MZSLT.js.map +1 -0
  65. package/dist/chunk-ZJ5AOYBH.js +8 -0
  66. package/dist/chunk-ZJ5AOYBH.js.map +1 -0
  67. package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
  68. package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
  69. package/dist/circuit-breaker/index.d.ts +1 -0
  70. package/dist/circuit-breaker/index.js +1 -1
  71. package/dist/{core-BmlJOvxM.d.ts → core-42kapW0n.d.ts} +96 -6
  72. package/dist/deadline/index.d.ts +77 -11
  73. package/dist/deadline/index.js +3 -3
  74. package/dist/devtools/index.js +8 -28
  75. package/dist/devtools/index.js.map +1 -1
  76. package/dist/do/index.d.ts +52 -24
  77. package/dist/do/index.js +94 -92
  78. package/dist/do/index.js.map +1 -1
  79. package/dist/{durable-effects-9LxFpmh3.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
  80. package/dist/extension/index.d.ts +25 -7
  81. package/dist/extension/index.js +4 -3
  82. package/dist/extension/index.js.map +1 -1
  83. package/dist/extension/react.js +4 -3
  84. package/dist/extension/react.js.map +1 -1
  85. package/dist/extension/subs/index.d.ts +2 -2
  86. package/dist/extension/subs/index.js +7 -10
  87. package/dist/extension/subs/index.js.map +1 -1
  88. package/dist/fan-out/index.d.ts +10 -10
  89. package/dist/fan-out/index.js +3 -3
  90. package/dist/idempotent-intake/index.d.ts +2 -2
  91. package/dist/idempotent-intake/index.js +3 -3
  92. package/dist/index.d.ts +50 -489
  93. package/dist/index.js +2 -2
  94. package/dist/llm-call/index.d.ts +3 -2
  95. package/dist/llm-call/index.js +12 -10
  96. package/dist/machine-B_BF4jij.d.ts +804 -0
  97. package/dist/machine-viz/index.d.ts +1 -1
  98. package/dist/machine-viz/index.js +2 -2
  99. package/dist/mem/index.d.ts +3 -2
  100. package/dist/mem/index.js.map +1 -1
  101. package/dist/monitored-run/index.d.ts +54 -30
  102. package/dist/monitored-run/index.js +7 -7
  103. package/dist/node/index.d.ts +3 -2
  104. package/dist/node/index.js +2 -3
  105. package/dist/node/index.js.map +1 -1
  106. package/dist/paginated-walk/index.d.ts +2 -2
  107. package/dist/paginated-walk/index.js +11 -10
  108. package/dist/parity/index.d.ts +6 -5
  109. package/dist/parity/index.js +5 -4
  110. package/dist/parity/index.js.map +1 -1
  111. package/dist/pbt/arbitraries/index.d.ts +1 -1
  112. package/dist/pbt/index.d.ts +1 -1
  113. package/dist/pbt/index.js +3 -3
  114. package/dist/pbt/index.js.map +1 -1
  115. package/dist/pbt/runners/index.d.ts +1 -1
  116. package/dist/pbt/runners/index.js +3 -3
  117. package/dist/poller/index.d.ts +59 -17
  118. package/dist/poller/index.js +56 -20
  119. package/dist/poller/index.js.map +1 -1
  120. package/dist/prediction/index.d.ts +1 -1
  121. package/dist/prediction/index.js +2 -2
  122. package/dist/pure/index.d.ts +1 -1
  123. package/dist/pure/index.js +2 -2
  124. package/dist/rate-limit/index.d.ts +2 -2
  125. package/dist/rate-limit/index.js +1 -1
  126. package/dist/react/index.d.ts +10 -3
  127. package/dist/react/index.js +2 -2
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/reconciler/index.d.ts +2 -2
  130. package/dist/reconciler/index.js +13 -15
  131. package/dist/reconciler/index.js.map +1 -1
  132. package/dist/recorder/index.d.ts +20 -20
  133. package/dist/recorder/index.js +1 -1
  134. package/dist/resilient-call/index.d.ts +31 -6
  135. package/dist/resilient-call/index.js +10 -9
  136. package/dist/retry-backoff/index.d.ts +191 -14
  137. package/dist/retry-backoff/index.js +1 -1
  138. package/dist/retry-to-success/index.js +1 -1
  139. package/dist/runtime-types-CukV8kMX.d.ts +406 -0
  140. package/dist/saga/index.d.ts +3 -2
  141. package/dist/saga/index.js +2 -2
  142. package/dist/saga/index.js.map +1 -1
  143. package/dist/snapshot/index.d.ts +1 -1
  144. package/dist/snapshot/index.js +3 -3
  145. package/dist/subs/index.d.ts +372 -4
  146. package/dist/subs/index.js +4 -1
  147. package/dist/testing/index.d.ts +2 -2
  148. package/dist/testing/index.js +2 -2
  149. package/dist/throttled-input/index.d.ts +2 -2
  150. package/dist/throttled-input/index.js +14 -13
  151. package/dist/throttled-input/index.js.map +1 -1
  152. package/dist/token-refresh/index.d.ts +1 -1
  153. package/dist/token-refresh/index.js +3 -3
  154. package/dist/trace-replay/index.d.ts +12 -7
  155. package/dist/trace-replay/index.js +4 -3
  156. package/dist/{types-BoUWgwSm.d.ts → types-KbL3W1mj.d.ts} +1 -1
  157. package/dist/with-deadline/index.d.ts +1 -1
  158. package/dist/with-deadline/index.js +2 -2
  159. package/dist/with-resilience/index.d.ts +21 -2
  160. package/dist/with-resilience/index.js +12 -23
  161. package/dist/with-resilience/index.js.map +1 -1
  162. package/dist/with-telemetry/index.d.ts +1 -1
  163. package/dist/with-telemetry/index.js +2 -2
  164. package/dist/work-queue/adapter.d.ts +2 -2
  165. package/dist/work-queue/adapter.js +2 -2
  166. package/dist/work-queue/index.d.ts +3 -2
  167. package/dist/work-queue/index.js +1 -1
  168. package/dist/work-queue/index.js.map +1 -1
  169. package/dist/work-queue/ops.d.ts +2 -2
  170. package/dist/work-queue/ops.js +1 -1
  171. package/dist/workflow/index.d.ts +51 -19
  172. package/dist/workflow/index.js +71 -52
  173. package/dist/workflow/index.js.map +1 -1
  174. package/package.json +4 -1
  175. package/dist/chunk-2YOE5PIS.js.map +0 -1
  176. package/dist/chunk-3545BOYL.js.map +0 -1
  177. package/dist/chunk-5VNWZADP.js.map +0 -1
  178. package/dist/chunk-6GIGQT6C.js.map +0 -1
  179. package/dist/chunk-6VDCTR7N.js.map +0 -1
  180. package/dist/chunk-DYRKDCJI.js.map +0 -1
  181. package/dist/chunk-EGWTT5XJ.js.map +0 -1
  182. package/dist/chunk-EMJTPHI7.js +0 -91
  183. package/dist/chunk-EMJTPHI7.js.map +0 -1
  184. package/dist/chunk-FOGL3TC7.js.map +0 -1
  185. package/dist/chunk-IK4S5DJN.js.map +0 -1
  186. package/dist/chunk-M6UZT7HS.js.map +0 -1
  187. package/dist/chunk-NSDFA6M6.js +0 -198
  188. package/dist/chunk-NSDFA6M6.js.map +0 -1
  189. package/dist/chunk-SC3FAFZN.js.map +0 -1
  190. package/dist/chunk-SKAMZKTE.js +0 -20
  191. package/dist/chunk-SKAMZKTE.js.map +0 -1
  192. package/dist/chunk-TGG26XMK.js.map +0 -1
  193. package/dist/chunk-UV6UWGEO.js.map +0 -1
  194. package/dist/chunk-UYWUVEP6.js.map +0 -1
  195. package/dist/chunk-VFGKTGGC.js.map +0 -1
  196. package/dist/chunk-ZDRN3YCE.js.map +0 -1
@@ -0,0 +1,406 @@
1
+ import { P as Port, C as Cmd, S as Sub, M as Machine, T as Transitions, R as Reducer, I as InterpretDetached, b as PortEmitter } from './core-42kapW0n.js';
2
+
3
+ /**
4
+ * @demlik/tea runtime interface surface + pure helpers — the public types,
5
+ * interfaces, and construction/composition helpers the runtime (`./run`)
6
+ * implements against: `Store`, the error-sink and supervision contracts, the
7
+ * `RuntimeRef` / `BootingRuntime` / `Runtime` handle hierarchy, `definePort`, the
8
+ * identity-typed constructors `defineMachine` / `asReducer`, and the pure tools
9
+ * `replay` (compose without running) and `tryInterpret` (Railway).
10
+ */
11
+
12
+ declare const ReducerBrand: unique symbol;
13
+ type Branded<T> = T & {
14
+ readonly [ReducerBrand]: true;
15
+ };
16
+ /**
17
+ * Compile-time exhaustiveness assertion for default branches of switches over
18
+ * discriminated unions narrowed by hand (Sub handlers, message-bridge
19
+ * dispatchers, port fanouts) — TS narrows the operand to `never` only if every
20
+ * variant is covered, so adding one produces a compile error at the `absurd(x)`
21
+ * site. Strengthens invariant 7 (identity is explicit).
22
+ *
23
+ * @example
24
+ * switch (msg.type) {
25
+ * case "a": return ...;
26
+ * case "b": return ...;
27
+ * default: return absurd(msg); // compile error if a "c" is added
28
+ * }
29
+ */
30
+ declare function absurd(x: never): never;
31
+ declare const noop: <S, M, C extends Cmd>(state: S, _msg: M) => readonly [S, readonly C[]];
32
+ /**
33
+ * Thrown by `definePort` when a name has already been registered in the current
34
+ * process. Symmetric with `SubIdCollisionError` (thrown by `reconcileSubs`) —
35
+ * both mechanize invariant 7 (identity is explicit) at the runtime layer the
36
+ * type system cannot reach (string names compared at runtime).
37
+ */
38
+ declare class PortNameCollisionError extends Error {
39
+ readonly name = "PortNameCollisionError";
40
+ readonly _tag: "PortNameCollisionError";
41
+ constructor(portName: string);
42
+ }
43
+ /**
44
+ * Thrown by `reconcileSubs` when, within ONE desired subscription set, two subs
45
+ * share an `id` but declare different `type`s — a silent bug class the type
46
+ * system cannot reach (ids are strings compared at runtime). The symmetric twin
47
+ * of `PortNameCollisionError`: both mechanize invariant 7 (identity is explicit)
48
+ * at the runtime layer. Same id across transitions is the no-churn case and does
49
+ * NOT throw; only a within-set type conflict does.
50
+ */
51
+ declare class SubIdCollisionError extends Error {
52
+ readonly name = "SubIdCollisionError";
53
+ readonly _tag: "SubIdCollisionError";
54
+ constructor(id: string, declaredType: string, conflictingType: string);
55
+ }
56
+ /**
57
+ * Test-only escape hatch — clears the module-level port-name registry so the
58
+ * collision assert in `definePort` doesn't false-positive across vitest's
59
+ * non-isolated runs. Production code MUST NOT call this.
60
+ *
61
+ * @internal test-only
62
+ */
63
+ declare function __resetPortRegistry(): void;
64
+ /**
65
+ * Which otherwise-unattributable runtime path produced an error. Every fanout is
66
+ * throw-isolated so one bad consumer never strands its siblings, and the
67
+ * isolated throw routes to the `OnError` sink rather than a bare `console.error`.
68
+ *
69
+ * - `"follow-up"` — a follow-up Msg an interpret handler returned rejected when
70
+ * re-dispatched (the original dispatcher already resolved, so no caller).
71
+ * A rejection caused by `stop()`'s own teardown is NOT this phase — see
72
+ * `"discard"`.
73
+ * - `"stop-save"` — the final `store.save(state)` inside `stop()` threw
74
+ * (`stop()` resolves regardless, so without the sink this was silent loss).
75
+ * - `"reduce"` — the pure `update` (reducer) threw synchronously; the configured
76
+ * `Supervision` strategy decides what happens next, but the throw is surfaced
77
+ * here as data first.
78
+ * - `"listener"` — a `runtime.subscribe(...)` listener threw during fanout.
79
+ * - `"observer"` — a `runtime.observe(...)` observer threw during fanout.
80
+ * - `"event"` — the `events` projector or an `on(type, ...)` handler threw.
81
+ * - `"boot"` — an `onBoot` handler threw (boot fanout, or a late registration).
82
+ * - `"port-emit"` — a `subscribePort` listener threw during a port emission.
83
+ * - `"sub-cleanup"` — a subscription's cleanup threw (reconcile-removal or
84
+ * `stop()` teardown).
85
+ * - `"discard"` — work lost to a teardown the host asked for. Two witnesses,
86
+ * both carrying a `RuntimeDiscardNotice`: `stop()` was called while interpret
87
+ * handlers were still awaiting (`RuntimeDiscardedError`), and a Msg that
88
+ * arrived DURING `stop()`'s drain and so could not be delivered
89
+ * (`DispatchDiscardedError`). The one phase that is a LIFECYCLE report rather
90
+ * than a failure: legal, merely lossy.
91
+ *
92
+ * The phase never decides fatality — the ERROR CLASS does (`RuntimeDiscardNotice`
93
+ * warns, everything else rethrows). A phase is attached by the report site, so a
94
+ * consumer sink that throws while handling a `"discard"` report inherits that
95
+ * phase; keying the warn-only path on it would make the sink's own defect vanish.
96
+ */
97
+ type RuntimeErrorPhase = "follow-up" | "stop-save" | "reduce" | "listener" | "observer" | "event" | "boot" | "port-emit" | "sub-cleanup" | "discard";
98
+ /** Context handed to an `OnError` sink alongside the error itself. */
99
+ interface RuntimeErrorContext {
100
+ readonly phase: RuntimeErrorPhase;
101
+ }
102
+ /**
103
+ * Sink for runtime failures that have no caller to reject at. Configured via
104
+ * `run(machine, { onError })`. Should be total — a throw inside the sink is
105
+ * itself surfaced via the default sink, so it can never re-introduce a silent
106
+ * failure.
107
+ */
108
+ type OnError = (error: unknown, context: RuntimeErrorContext) => void;
109
+ /** The three declared reducer-throw supervision strategies. */
110
+ type SupervisionStrategy = "stop" | "escalate" | "restart";
111
+ /**
112
+ * Declared supervision policy for a reducer (`update`) throw, at
113
+ * `run(machine, { supervision })`. The bare-string shorthands (`"stop"` /
114
+ * `"escalate"`) are accepted for the two strategies that need no host data;
115
+ * `restart` MUST be the object form because it carries the `rehydrate` callback,
116
+ * which MUST return a valid `S` synchronously (the reducer is synchronous, so
117
+ * recovery is too — no suspension across the single-writer slot).
118
+ */
119
+ type Supervision<S, M extends {
120
+ type: string;
121
+ }> = "stop" | "escalate" | {
122
+ readonly strategy: "stop";
123
+ } | {
124
+ readonly strategy: "escalate";
125
+ } | {
126
+ readonly strategy: "restart";
127
+ /**
128
+ * Host-provided rehydration to last-known-good state. Invoked when the
129
+ * reducer throws; its return value becomes the new state and the transition
130
+ * continues from there. Receives the pre-throw `state`, the `msg` that
131
+ * triggered the throw, and the thrown `error` so the host can route by
132
+ * cause.
133
+ */
134
+ readonly rehydrate: (state: S, msg: M, error: unknown) => S;
135
+ };
136
+ /**
137
+ * Raised by `idle()` when the quiescence wait hits its iteration cap without the
138
+ * dispatch tail stabilizing — `idle()` REJECTS rather than silently resolving,
139
+ * so a livelocking machine surfaces instead of masquerading as quiescent.
140
+ * Mechanizes invariant 6 at the runtime layer, the role `PortNameCollisionError`
141
+ * plays for invariant 7.
142
+ */
143
+ declare class QuiescenceTimeoutError extends Error {
144
+ readonly iterations: number;
145
+ readonly name = "QuiescenceTimeoutError";
146
+ readonly _tag: "QuiescenceTimeoutError";
147
+ constructor(iterations: number);
148
+ }
149
+ /**
150
+ * Base of the LOSSY-BUT-LEGAL teardown facts: work the host discarded by letting
151
+ * go of a runtime that still had something outstanding. Reported under
152
+ * `phase: "discard"`, never raised.
153
+ *
154
+ * It exists so "is this fatal?" has ONE definition — `error instanceof
155
+ * RuntimeDiscardNotice` — that the default sink can ask about any error from any
156
+ * site. The phase cannot answer it: a report site attaches the phase, so a
157
+ * consumer sink that throws while handling a `"discard"` report carries that
158
+ * phase into the sink-failure path, and a phase-keyed warn branch would swallow
159
+ * the sink's own defect. Sub-classing is how a new teardown fact opts into the
160
+ * warn-only treatment; nothing else can.
161
+ */
162
+ declare abstract class RuntimeDiscardNotice extends Error {
163
+ }
164
+ /**
165
+ * Reported to the `OnError` sink under `phase: "discard"` when `stop()` is
166
+ * called while `interpret` handlers are still awaiting. The host is dropping
167
+ * its only handle on that work: `stop()` drains the tail, but the listeners,
168
+ * observers and event handlers that would have consumed the resulting
169
+ * transitions are gone, so the Cmds' results reach nobody.
170
+ *
171
+ * The motivating shape is `@demlik/tea/react`'s `useMachine`, which memoizes on
172
+ * `[machine, ctx, store]` — a ctx re-derived mid-flight replaces the runtime,
173
+ * and the in-flight mutation's response arrives for a runtime the UI no longer
174
+ * renders, silently rewinding the visible state. Never a throw: tearing a
175
+ * runtime down mid-flight is legal (a navigation does it every time), so this
176
+ * is surfaced as data, not raised. Mechanizes invariant 6 (no silent failures)
177
+ * at the teardown seam, the role `QuiescenceTimeoutError` plays for `idle()`.
178
+ */
179
+ declare class RuntimeDiscardedError extends RuntimeDiscardNotice {
180
+ readonly pendingCmds: number;
181
+ readonly name = "RuntimeDiscardedError";
182
+ readonly _tag: "RuntimeDiscardedError";
183
+ constructor(pendingCmds: number);
184
+ }
185
+ /**
186
+ * The rejection of a dispatch that arrived DURING `stop()`'s drain — an in-flight
187
+ * interpret handler's follow-up Msg, a detached handler's terminal Msg, or a Sub
188
+ * that is still live because subs are torn down only after the drain. The stop
189
+ * barrier is absolute (the Msg is refused, never folded), so this names the loss.
190
+ *
191
+ * It is the SECOND witness of `RuntimeDiscardedError`'s fact, and it is why the
192
+ * drain window is a distinct runtime state rather than a plain `stopped` flag:
193
+ * a Msg refused inside the window was discarded BY the teardown, while the same
194
+ * refusal after `stop()` has returned is a consumer dispatching into a runtime it
195
+ * already retired — a real error, and still loud (`phase: "follow-up"`, or the
196
+ * caller's own rejection). The distinction is the state at refusal time, never
197
+ * the message text.
198
+ */
199
+ declare class DispatchDiscardedError extends RuntimeDiscardNotice {
200
+ readonly msgType: string;
201
+ readonly name = "DispatchDiscardedError";
202
+ readonly _tag: "DispatchDiscardedError";
203
+ constructor(msgType: string);
204
+ }
205
+ /**
206
+ * Define a typed port. `name` is metadata (devtools / debug logs); port identity
207
+ * is by reference. Each definePort call must use a unique `name` — a second call
208
+ * with the same name throws `PortNameCollisionError`. Use ports for "data leaving
209
+ * the runtime selectively" (cursor announcements, DOM-mutation broadcasts,
210
+ * telemetry) that should NOT be folded into State. Strengthens invariant 7.
211
+ */
212
+ declare function definePort<T>(name: string): Port<T>;
213
+ interface Store<S> {
214
+ load(): Promise<unknown>;
215
+ save(state: S): Promise<void>;
216
+ migrate(raw: unknown): S | null;
217
+ }
218
+ interface Schema<S> {
219
+ safeParse(raw: unknown): {
220
+ success: true;
221
+ data: S;
222
+ } | {
223
+ success: false;
224
+ };
225
+ }
226
+ /**
227
+ * Build a `Store.migrate` from a schema (job 1) and an optional thin `upcast`
228
+ * (job 2). `upcast` maps a recognized-but-OLD raw shape forward into the shape
229
+ * the schema validates; it defaults to identity (no version migration yet).
230
+ *
231
+ * Never throws — a shape the schema rejects returns `null`, the substrate's
232
+ * fresh-boot path, per the `Store.migrate` contract. An `upcast` that itself
233
+ * throws on a corrupt blob is caught and collapses to `null` (same posture).
234
+ */
235
+ declare function schemaMigrate<S>(schema: Schema<S>, upcast?: (raw: unknown) => unknown): (raw: unknown) => S | null;
236
+ type DispatchSettle = "quiescent" | "once";
237
+ interface RuntimeRef<M extends {
238
+ type: string;
239
+ }> {
240
+ /**
241
+ * Put a Msg in the runtime's inbox and resolve once processed. Runs to
242
+ * QUIESCENCE by default: settles only after the dispatched Msg AND every
243
+ * transitive interpret follow-up has drained. Pass `{ settle: "once" }` (or
244
+ * call `dispatchOnce`) for the single-step case.
245
+ *
246
+ * Rejection ordering: the ONE dispatched transition's own failure surfaces
247
+ * first, here. If it succeeds but the follow-up chain never stabilizes, the
248
+ * quiescent drain rejects with `QuiescenceTimeoutError`. Follow-up Msg
249
+ * rejections themselves route to the `onError` sink, never here.
250
+ */
251
+ dispatch(msg: M, opts?: {
252
+ readonly settle?: DispatchSettle;
253
+ }): Promise<void>;
254
+ /**
255
+ * Dispatch `msg` and resolve after exactly ONE transition's effects settle,
256
+ * WITHOUT draining the follow-up chain — equivalent to
257
+ * `dispatch(msg, { settle: "once" })`. Prefer plain `dispatch` unless you will
258
+ * `await runtime.idle()` yourself; the un-awaited follow-up chain is the
259
+ * footgun the quiescent default removed.
260
+ */
261
+ dispatchOnce(msg: M): Promise<void>;
262
+ }
263
+ interface BootingRuntime<S, M extends {
264
+ type: string;
265
+ }, E extends {
266
+ type: string;
267
+ } = never> extends RuntimeRef<M> {
268
+ dispatch(msg: M, opts?: {
269
+ readonly settle?: DispatchSettle;
270
+ }): Promise<void>;
271
+ dispatchOnce(msg: M): Promise<void>;
272
+ subscribe(listener: () => void): () => void;
273
+ observe(observer: (msg: M, state: S) => void): () => void;
274
+ /**
275
+ * Subscribe to the INITIAL State — the boot transition. Fires exactly once:
276
+ * immediately if boot already completed (so a late subscriber never misses
277
+ * it), otherwise on the boot fanout. Returns a cleanup (a no-op once fired).
278
+ */
279
+ onBoot(handler: (state: S) => void): () => void;
280
+ /**
281
+ * Subscribe to a SEMANTIC event of `type`. The handler receives exactly the
282
+ * `E` member whose `type` matches `K`, so a consumer never touches another
283
+ * event's shape and never references the machine's PRIVATE Msg names. Multiple
284
+ * handlers per type; fanout is synchronous and throw-isolated. Fires only when
285
+ * the machine's `events` projector (wired on `run`) emits that type.
286
+ */
287
+ on<K extends E["type"]>(type: K, handler: (event: Extract<E, {
288
+ type: K;
289
+ }>) => void): () => void;
290
+ /**
291
+ * Subscribe to a typed Port. Multiple listeners per port; fanout is synchronous
292
+ * and isolated. Ports are for "data leaving the runtime selectively" (see
293
+ * `definePort`) — distinct from `observe` (every transition) and State.
294
+ */
295
+ subscribePort<T>(port: Port<T>, listener: (value: T) => void): () => void;
296
+ /**
297
+ * Emit a value on a Port from OUTSIDE an interpret handler — same synchronous
298
+ * fanout as `ctx.emit`. The canonical use is `observe`-driven emission (one
299
+ * runtime publishing a state-derived signal another subscribes to via
300
+ * `subscribePort`) without spawning a "tell the outside world" Cmd per
301
+ * transition.
302
+ */
303
+ emitPort<T>(port: Port<T>, value: T): void;
304
+ /**
305
+ * Resolves to the booted `Runtime<S, M>` after boot completes (or rejects with
306
+ * the boot error). This is the ONLY way to obtain a `Runtime` — and a
307
+ * `Runtime` is the only handle whose `getState()` is total. Awaiting `ready` is
308
+ * the single gate between "boot in flight" and "State exists":
309
+ *
310
+ * ```ts
311
+ * const runtime = await run(machine, opts).ready;
312
+ * runtime.getState(); // total — never throws-before-boot
313
+ * ```
314
+ *
315
+ * Idempotent — the same settled promise, resolving to the same `Runtime`, comes
316
+ * back on every read. Expresses canon §2.3 (boot is a named, awaitable moment).
317
+ */
318
+ ready: Promise<Runtime<S, M, E>>;
319
+ stop(): Promise<void>;
320
+ }
321
+ interface Runtime<S, M extends {
322
+ type: string;
323
+ }, E extends {
324
+ type: string;
325
+ } = never> extends BootingRuntime<S, M, E> {
326
+ /**
327
+ * The current State. TOTAL — never throws. Obtaining a `Runtime` requires
328
+ * awaiting `ready`, which only resolves AFTER boot has run `init` and set the
329
+ * initial State.
330
+ */
331
+ getState(): S;
332
+ /** Resolves to this same `Runtime` once boot completes. Idempotent. */
333
+ ready: Promise<Runtime<S, M, E>>;
334
+ /**
335
+ * Resolves once the runtime has reached QUIESCENCE — every dispatched Msg AND
336
+ * every transitive interpret follow-up has been processed, with no further step
337
+ * pending on the tail. Since plain `dispatch` already runs to quiescence, this
338
+ * is mainly for follow-ups left by a `dispatchOnce` single step, or a chain
339
+ * kicked off by a Sub / boot `init` cmds with no `dispatch` to await.
340
+ *
341
+ * Idempotent and re-entrant-safe. Tail rejections are NOT surfaced here (a
342
+ * failing dispatch surfaces on its OWN promise; follow-up rejections route to
343
+ * `onError`). The one rejection `idle()` produces is `QuiescenceTimeoutError`
344
+ * on hitting the iteration cap — so a livelock stays distinguishable from a
345
+ * genuine quiesce (invariant 6). A poll is still correct when waiting on an
346
+ * EXTERNAL event the runtime cannot enqueue itself.
347
+ */
348
+ idle(): Promise<void>;
349
+ /**
350
+ * The terminal State of the run, or `undefined` while in flight — "terminal"
351
+ * per the `terminal` predicate passed to `run()` (no predicate → never
352
+ * terminal → always `undefined`). The first-class result read: the run's
353
+ * product off the State the machine already owns, NOT scraped off the `observe`
354
+ * firehose by matching an internal Msg name. Total — never throws.
355
+ */
356
+ result(): S | undefined;
357
+ /**
358
+ * Resolves with the terminal State the first time the run reaches one (per the
359
+ * `terminal` predicate). If ALREADY terminal, resolves immediately; otherwise
360
+ * on the transition that first makes `terminal` hold. The awaitable companion
361
+ * to `result()`. With no predicate this never resolves. Idempotent and
362
+ * multi-caller safe.
363
+ */
364
+ done(): Promise<S>;
365
+ }
366
+ declare function defineMachine<S, M extends {
367
+ type: string;
368
+ }, C extends Cmd, U extends Sub, Ctx>(m: Omit<Machine<S, M, C, U, Ctx>, "update"> & {
369
+ update: [S] extends [{
370
+ type: string;
371
+ }] ? Transitions<S, M, C> : never;
372
+ }): Machine<S, M, C, U, Ctx>;
373
+ declare function defineMachine<S, M extends {
374
+ type: string;
375
+ }, C extends Cmd, U extends Sub, Ctx>(m: Omit<Machine<S, M, C, U, Ctx>, "update"> & {
376
+ update: Reducer<S, M, C>;
377
+ }): Machine<S, M, C, U, Ctx>;
378
+ declare function asReducer<S, M extends {
379
+ type: string;
380
+ }, C extends Cmd>(reducer: Reducer<S, M, C>): Branded<Reducer<S, M, C>>;
381
+ type CtxArg<Ctx> = [Record<never, never>] extends [Ctx] ? {
382
+ ctx?: Ctx;
383
+ } : {
384
+ ctx: Ctx;
385
+ };
386
+ declare function replay<S, M extends {
387
+ type: string;
388
+ }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, opts: {
389
+ msgs: readonly M[];
390
+ ctx: Ctx;
391
+ loaded?: S | null;
392
+ }): {
393
+ state: S;
394
+ cmds: C[];
395
+ subs: U[];
396
+ depSubs: {
397
+ index: number;
398
+ id: string;
399
+ }[];
400
+ };
401
+ declare function wrapDetached<C extends Cmd, M extends {
402
+ type: string;
403
+ }, Allowed extends M, Ctx>(handler: InterpretDetached<C, Allowed, Ctx>): (cmd: C, ctx: Ctx & PortEmitter, dispatch?: (msg: M) => void) => Promise<void>;
404
+ declare function tryInterpret<C extends Cmd, Ok, M, Ctx>(work: (cmd: C, ctx: Ctx) => Promise<Ok>, onOk: (value: Ok, cmd: C) => M, onErr: (error: unknown, cmd: C) => M): (cmd: C, ctx: Ctx) => Promise<M>;
405
+
406
+ export { type BootingRuntime as B, type CtxArg as C, DispatchDiscardedError as D, type OnError as O, PortNameCollisionError as P, QuiescenceTimeoutError as Q, type Runtime as R, type Store as S, __resetPortRegistry as _, type Supervision as a, type Branded as b, absurd as c, SubIdCollisionError as d, type RuntimeErrorPhase as e, type RuntimeErrorContext as f, type SupervisionStrategy as g, RuntimeDiscardNotice as h, RuntimeDiscardedError as i, definePort as j, type Schema as k, type DispatchSettle as l, type RuntimeRef as m, noop as n, defineMachine as o, asReducer as p, replay as r, schemaMigrate as s, tryInterpret as t, wrapDetached as w };
@@ -1,8 +1,9 @@
1
- import { C as Cmd } from '../core-BmlJOvxM.js';
1
+ import { C as Cmd } from '../core-42kapW0n.js';
2
2
  import { QueueItem } from '../work-queue/index.js';
3
- import '../index.js';
3
+ import '../runtime-types-CukV8kMX.js';
4
4
 
5
5
  /**
6
+ * @packageDocumentation
6
7
  * @demlik/tea/saga — a forward-then-compensate transaction over an ordered list
7
8
  * of reversible steps.
8
9
  *
@@ -1,5 +1,5 @@
1
- import { queueAdapter } from '../chunk-HJO6VNQI.js';
2
- import '../chunk-TGG26XMK.js';
1
+ import { queueAdapter } from '../chunk-MU6MCIAC.js';
2
+ import '../chunk-FV3Z5MBE.js';
3
3
 
4
4
  // src/saga/index.ts
5
5
  function isCommitted(state) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/saga/index.ts"],"names":[],"mappings":";;;;AAuMO,SAAS,YAAe,KAAA,EAA8B;AAC3D,EAAA,OAAO,MAAM,KAAA,KAAU,WAAA;AACzB;AAGO,SAAS,UAAa,KAAA,EAA8B;AACzD,EAAA,OAAO,MAAM,KAAA,KAAU,SAAA;AACzB;AAOO,SAAS,qBAAwB,KAAA,EAA8B;AACpE,EAAA,OAAO,MAAM,KAAA,KAAU,qBAAA;AACzB;AAQO,SAAS,UAAa,KAAA,EAA8B;AACzD,EAAA,OACE,MAAM,KAAA,KAAU,WAAA,IAChB,MAAM,KAAA,KAAU,SAAA,IAChB,MAAM,KAAA,KAAU,qBAAA;AAEpB;AASO,SAAS,QAAA,GAAqC;AACnD,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,CAAA;AAAA,IACV,MAAM,EAAC;AAAA,IACP,YAAA,EAAc,KAAA;AAAA,IACd,KAAA,EAAO,MAAA;AAAA,IACP,OAAO;AAAC,GACV;AACF;AAcO,SAAS,WACd,MAAA,EACA;AAIA,EAAA,MAAM,KAAK,YAAA,EAAqB;AAahC,EAAA,SAAS,WAAA,GAAmC;AAC1C,IAAA,IAAI,QAA6B,EAAC;AAClC,IAAA,MAAA,CAAO,KAAA,CAAM,OAAA,CAAQ,CAAC,KAAA,EAAO,KAAA,KAAU;AACrC,MAAA,KAAA,GAAQ,EAAA,CAAG,QAAQ,KAAA,EAAO,KAAA,EAAO,GAAG,MAAA,CAAO,KAAK,CAAC,CAAA,CAAE,IAAA;AAAA,IACrD,CAAC,CAAA;AACD,IAAA,OAAO,KAAA;AAAA,EACT;AASA,EAAA,SAAS,SAAA,CACP,KAAA,EACA,KAAA,EACA,MAAA,EACqB;AACrB,IAAA,OAAO,GAAG,KAAA,CAAM,KAAA,EAAO,OAAO,KAAK,CAAA,EAAG,CAAC,IAAA,MAAU;AAAA,MAC/C,GAAG,IAAA;AAAA,MACH;AAAA,MACA,CAAA,CAAE,IAAA;AAAA,EACN;AAOA,EAAA,SAAS,OAAO,KAAA,EAA2C;AACzD,IAAA,OAAO,MAAA,CAAO,MAAM,KAAK,CAAA;AAAA,EAC3B;AAGA,EAAA,SAAS,SAAS,IAAA,EAA6C;AAC7D,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,MAAA,GAAS,CAAC,CAAA;AAAA,EAC7B;AAYA,EAAA,SAAS,MACP,KAAA,EAC4C;AAC5C,IAAA,IAAI,MAAM,KAAA,KAAU,MAAA,SAAe,CAAC,KAAA,EAAO,EAAE,CAAA;AAE7C,IAAA,MAAM,QAAQ,WAAA,EAAY;AAC1B,IAAA,MAAM,KAAA,GAAQ,OAAO,CAAC,CAAA;AACtB,IAAA,IAAI,UAAU,MAAA,EAAW;AAEvB,MAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,OAAO,WAAA,EAAa,KAAA,EAAM,EAAG,EAAE,CAAA;AAAA,IACrD;AAEA,IAAA,MAAM,IAAA,GAA0B;AAAA,MAC9B,GAAG,KAAA;AAAA,MACH,QAAA,EAAU,CAAA;AAAA,MACV,KAAA,EAAO,SAAA;AAAA,MACP,KAAA,EAAO,SAAA,CAAU,KAAA,EAAO,CAAA,EAAG,SAAS;AAAA,KACtC;AACA,IAAA,OAAO,CAAC,IAAA,EAAM,CAAC,KAAA,CAAM,EAAE,CAAC,CAAA;AAAA,EAC1B;AAiBA,EAAA,SAAS,OACP,KAAA,EAC4C;AAC5C,IAAA,IAAI,MAAM,KAAA,KAAU,SAAA,SAAkB,CAAC,KAAA,EAAO,EAAE,CAAA;AAEhD,IAAA,MAAM,YAAY,KAAA,CAAM,QAAA;AACxB,IAAA,MAAM,SAAA,GAAY,SAAA,CAAU,KAAA,CAAM,KAAA,EAAO,WAAW,MAAM,CAAA;AAC1D,IAAA,MAAM,IAAA,GAAO,CAAC,GAAG,KAAA,CAAM,MAAM,SAAS,CAAA;AACtC,IAAA,MAAM,eAAe,SAAA,GAAY,CAAA;AACjC,IAAA,MAAM,QAAA,GAAW,OAAO,YAAY,CAAA;AAEpC,IAAA,IAAI,aAAa,MAAA,EAAW;AAG1B,MAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,IAAA,EAAM,KAAA,EAAO,SAAA,EAAW,KAAA,EAAO,WAAA,EAAY,EAAG,EAAE,CAAA;AAAA,IACtE;AAEA,IAAA,MAAM,IAAA,GAA0B;AAAA,MAC9B,GAAG,KAAA;AAAA,MACH,QAAA,EAAU,YAAA;AAAA,MACV,IAAA;AAAA,MACA,KAAA,EAAO,SAAA,CAAU,SAAA,EAAW,YAAA,EAAc,SAAS;AAAA,KACrD;AACA,IAAA,OAAO,CAAC,IAAA,EAAM,CAAC,QAAA,CAAS,EAAE,CAAC,CAAA;AAAA,EAC7B;AAmBA,EAAA,SAAS,OAAA,CACP,OACA,KAAA,EAC4C;AAC5C,IAAA,IAAI,MAAM,KAAA,KAAU,SAAA,SAAkB,CAAC,KAAA,EAAO,EAAE,CAAA;AAEhD,IAAA,MAAM,SAAS,KAAA,CAAM,QAAA;AACrB,IAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,KAAA,CAAM,KAAA,EAAO,QAAQ,QAAQ,CAAA;AAOrD,IAAA,MAAM,SAAA,GAAY,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA;AACrC,IAAA,MAAM,QAAA,GAAW,SAAA,KAAc,MAAA,GAAY,MAAA,GAAY,OAAO,SAAS,CAAA;AACvE,IAAA,IAAI,SAAA,KAAc,MAAA,IAAa,QAAA,KAAa,MAAA,EAAW;AACrD,MAAA,OAAO;AAAA,QACL,EAAE,GAAG,KAAA,EAAO,KAAA,EAAO,OAAO,YAAA,EAAc,IAAA,EAAM,OAAO,SAAA,EAAU;AAAA,QAC/D;AAAC,OACH;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAA0B;AAAA,MAC9B,GAAG,KAAA;AAAA,MACH,KAAA;AAAA,MACA,KAAA;AAAA,MACA,YAAA,EAAc,IAAA;AAAA,MACd,KAAA,EAAO,cAAA;AAAA,MACP,QAAA,EAAU;AAAA,KACZ;AACA,IAAA,OAAO,CAAC,IAAA,EAAM,CAAC,QAAA,CAAS,IAAI,CAAC,CAAA;AAAA,EAC/B;AAiBA,EAAA,SAAS,OACP,KAAA,EAC4C;AAC5C,IAAA,IAAI,MAAM,KAAA,KAAU,cAAA,SAAuB,CAAC,KAAA,EAAO,EAAE,CAAA;AAMrD,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA;AAClC,IAAA,IAAI,WAAW,MAAA,EAAW,OAAO,CAAC,KAAA,EAAO,EAAE,CAAA;AAC3C,IAAA,MAAM,SAAA,GAAY,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,GAAG,EAAE,CAAA;AACxC,IAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,KAAA,CAAM,KAAA,EAAO,QAAQ,WAAW,CAAA;AAKxD,IAAA,MAAM,QAAA,GAAW,SAAS,SAAS,CAAA;AACnC,IAAA,MAAM,QAAA,GAAW,QAAA,KAAa,MAAA,GAAY,MAAA,GAAY,OAAO,QAAQ,CAAA;AACrE,IAAA,IAAI,QAAA,KAAa,MAAA,IAAa,QAAA,KAAa,MAAA,EAAW;AAEpD,MAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,IAAA,EAAM,SAAA,EAAW,KAAA,EAAO,KAAA,EAAO,SAAA,EAAU,EAAG,EAAE,CAAA;AAAA,IACpE;AAEA,IAAA,MAAM,IAAA,GAA0B;AAAA,MAC9B,GAAG,KAAA;AAAA,MACH,IAAA,EAAM,SAAA;AAAA,MACN,KAAA;AAAA,MACA,QAAA,EAAU;AAAA,KACZ;AACA,IAAA,OAAO,CAAC,IAAA,EAAM,CAAC,QAAA,CAAS,IAAI,CAAC,CAAA;AAAA,EAC/B;AAuBA,EAAA,SAAS,OAAA,CACP,OACA,KAAA,EAC4C;AAC5C,IAAA,IAAI,MAAM,KAAA,KAAU,cAAA,SAAuB,CAAC,KAAA,EAAO,EAAE,CAAA;AAKrD,IAAA,MAAM,QAAQ,SAAA,CAAU,KAAA,CAAM,KAAA,EAAO,KAAA,CAAM,UAAU,QAAQ,CAAA;AAC7D,IAAA,OAAO;AAAA,MACL;AAAA,QACE,GAAG,KAAA;AAAA,QACH,KAAA;AAAA,QACA,iBAAA,EAAmB,KAAA;AAAA,QACnB,KAAA,EAAO;AAAA,OACT;AAAA,MACA;AAAC,KACH;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAA;AAAA,IACN,KAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,WAAA;AAAA,IACA,SAAA;AAAA,IACA,oBAAA;AAAA,IACA;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * @demlik/tea/saga — a forward-then-compensate transaction over an ordered list\n * of reversible steps.\n *\n * The Level-2 combinator for \"run these N steps in order; if any step fails,\n * undo the steps that already succeeded, in reverse.\" It is the distributed-\n * transaction / compensation knob: each step is a `{ do, undo }` pair of Cmds,\n * the saga drives `do` forward one step at a time, and on the first failure it\n * pivots and drives `undo` backward over exactly the steps that completed.\n *\n * **Boundary with `@demlik/tea/workflow` — the compensation test.** Both drive\n * an ordered multi-step transaction with reverse-order compensation; the line\n * between them is whether every step carries a true inverse. Here\n * `SagaStep<D, U>` REQUIRES both `do` and `undo` — a step without its inverse\n * is unrepresentable, so full reversibility is a type-level guarantee.\n * Workflow's `WorkflowStep.compensation` is OPTIONAL — an irreversible step (a\n * sent email) is skipped during the unwind. Pick by the test: **every step\n * must be reversible → saga; some steps are irreversible but the run must\n * finish → workflow.** Cross to workflow when the transaction includes steps\n * that can't be undone (or when you want each activity recorded as a durable\n * owed effect on the `../do` ledger); stay here when the type system should\n * enforce that every step can be undone.\n *\n * The recipe (which bricks it composes):\n * - `../work-queue/adapter` — the saga's forward progress IS a work-queue\n * lifecycle. Each step's record carries the same `{ id, status, input }`\n * vocabulary (`pending → running → done | failed | cancelled`), so the step\n * taxonomy is the substrate's status enum rather than a parallel invention.\n * We reuse the pure `QueueItem` / `QueueItemStatus` types AND delegate every\n * status flip to the `QueueAdapter` verbs: `wq.enqueue` stamps each fresh\n * ledger record, and `wq.patch` performs every\n * `pending → running → done | failed | cancelled` transition. The status\n * transitions are NOT re-rolled here — they live in exactly one place\n * behind the adapter, per the package's one-shape rule. The async\n * `createQueue(store)` adapter is NOT used: its `load → mutate → save` reads\n * the clock and lives at an I/O boundary, while a combinator's verbs must be\n * pure (invariant 2). We bind the same ops to the in-Model ledger instead —\n * durable + replayable, the two non-negotiables every knob preserves.\n *\n * The compensation log is the spec's `done: StepId[]` — the indices of the\n * steps that completed forward, in the order they completed. On `stepErr` the\n * saga walks that log in reverse, emitting one `undo` per `undoOk`, so the\n * unwind is the exact mirror of the wind. If an `undo` ITSELF fails, `undoErr`\n * pivots to the terminal `compensation_failed` phase: a failed compensation is\n * a real-world fact (a refund that bounced, a hold that won't release) that the\n * consumer must see and reconcile by hand — never a saga wedged in\n * `compensating` forever.\n *\n * Typical wiring (combinator form — splice the hooks into your machine):\n *\n * const saga = createSaga<BookFlight | ChargeCard | ..., Cancel | Refund | ...>({\n * steps: [\n * { do: { type: \"book_flight\" }, undo: { type: \"cancel_flight\" } },\n * { do: { type: \"charge_card\" }, undo: { type: \"refund_card\" } },\n * { do: { type: \"reserve_hotel\" },undo: { type: \"release_hotel\" } },\n * ],\n * });\n *\n * // in `update`:\n * begin: (s) => lift(s, saga.start(s.saga)),\n * step_ok: (s) => lift(s, saga.stepOk(s.saga)),\n * step_err: (s, m) => lift(s, saga.stepErr(s.saga, m.error)),\n * undo_ok: (s) => lift(s, saga.undoOk(s.saga)),\n * undo_err: (s, m) => lift(s, saga.undoErr(s.saga, m.error)),\n *\n * Every verb is PURE: it returns a NEW slice plus the Cmds to perform (the next\n * `do` going forward, or the next `undo` rolling back). The reducer never reads\n * a clock or RNG — step identity is positional (the index into `config.steps`),\n * so progress survives a round-trip through the durable slice and resumes\n * exactly where it left off after a DO eviction.\n *\n * There is no `subs` and no `handlers`: a saga runs no timers, and the `do` /\n * `undo` Cmds are fully caller-defined (the consumer's own `interpret` performs\n * each effect and routes Ok/Err back to the `stepOk` / `stepErr` / `undoOk`\n * verbs — the same shape `resilient-fetch` uses with `tryInterpret`). The saga\n * owns the ORDER; the consumer owns the I/O. Pure-ops module, like\n * `retry-backoff`.\n *\n * NOT a substrate primitive: it depends only on `../work-queue`'s pure types\n * and `../index`'s `Cmd` type. Consumers reach it via the `@demlik/tea/saga`\n * subpath.\n */\n\nimport type { Cmd } from \"../index\";\nimport type { QueueItem, QueueItemStatus } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A step's stable identity. Positional: the 0-based index into\n * `config.steps`. Positional ids are PURE (no clock, no RNG) and survive a\n * round-trip through the durable slice, so `done` can be replayed to resume an\n * interrupted saga exactly. The compensation log is a list of these.\n */\nexport type StepId = number;\n\n/**\n * One step of the saga: the forward effect and its compensating inverse, both\n * as data (plain Cmds). `do` runs while winding forward; `undo` runs while\n * unwinding after a failure. The pair is the unit of reversibility — a step\n * that completed (`do` succeeded) is exactly a step whose `undo` must run if a\n * later step fails.\n */\nexport interface SagaStep<D extends Cmd, U extends Cmd> {\n /** The forward effect, performed while the saga winds forward. */\n readonly do: D;\n /** The compensating inverse, performed (in reverse) while the saga unwinds. */\n readonly undo: U;\n}\n\n/**\n * The knob. The ordered list of reversible steps. The list is fixed for the\n * life of one saga instance (a saga drives a known transaction); per-run inputs\n * ride on the consumer's Msgs, not here.\n */\nexport interface SagaConfig<D extends Cmd, U extends Cmd> {\n /** The steps, in forward order. `do` runs `0 → n-1`; `undo` runs the completed prefix `n-1 → 0`. */\n readonly steps: readonly SagaStep<D, U>[];\n}\n\n/**\n * The lifecycle phase of a saga, narrowed at the type level so a consumer can\n * branch on `state.phase` (and a Transitions-table reducer can key on it).\n *\n * - `\"idle\"` — created, not started. No step has run.\n * - `\"running\"` — winding forward; `position` is the in-flight step.\n * - `\"compensating\"` — a step failed; unwinding `done` in reverse.\n * - `\"committed\"` — every step completed forward. Terminal success.\n * - `\"aborted\"` — compensation finished (every completed step undone, or\n * the first step itself failed with nothing to undo).\n * Terminal failure, fully unwound.\n * - `\"compensation_failed\"` — an `undo` itself failed mid-rollback. Terminal\n * failure, PARTIALLY unwound: the steps still in `done`\n * were never compensated and must be reconciled by hand.\n * Distinguished from `aborted` precisely so a stuck\n * rollback is visible state, not a saga wedged in\n * `compensating` forever.\n */\nexport type SagaPhase =\n | \"idle\"\n | \"running\"\n | \"compensating\"\n | \"committed\"\n | \"aborted\"\n | \"compensation_failed\";\n\n/**\n * The slice this knob owns.\n *\n * The spec's three fields are load-bearing:\n * - `position` — the index of the step currently in flight while `running`,\n * or the index being compensated while `compensating`. Survives eviction,\n * so `boot` resumes mid-saga from the durable slice.\n * - `done` — the compensation log: the ids of steps that completed forward,\n * in completion order. `undo` walks this in reverse. This is the single\n * source of truth for what must be unwound.\n * - `compensating` — the direction bit. `false` while winding forward, `true`\n * once a failure has pivoted the saga into rollback.\n *\n * Plus:\n * - `phase` — the derived lifecycle tag (see `SagaPhase`). Kept in the slice\n * (not recomputed) so a consumer's view and Transitions reducer can key on\n * `state.phase` directly, and the terminal states (`committed` / `aborted`)\n * are explicit facts rather than \"position past the end\" arithmetic.\n * - `error` — the forward failure that triggered compensation, carried\n * forward verbatim (`unknown`) for the consumer to read, never interpreted\n * (same discipline as `retry-backoff`'s `lastError` and `fan-out`'s\n * `error`).\n * - `compensationError` — the failure of an `undo` itself, set only when the\n * saga reaches `compensation_failed`. Kept distinct from `error` (the\n * forward failure) so a consumer reconciling a stuck rollback can read both\n * \"what broke the transaction\" and \"which compensation then bounced\".\n * Carried verbatim, never interpreted, same as `error`.\n * - `items` — the work-queue ledger, one `QueueItem` per step, keyed by the\n * step's positional id. The single source of truth for each step's status;\n * `phase` / `position` / `done` are the projection the verbs keep in\n * lockstep with it.\n *\n * Every field is `readonly` and JSON-serializable (plain numbers, arrays, and\n * the work-queue's plain `QueueItem` records), so the slice round-trips through\n * a consumer's `Store<S>` unchanged — durable across DO eviction.\n */\nexport interface SagaState<I = StepId> {\n /** The step in flight (`running`) or being compensated (`compensating`). */\n readonly position: number;\n /** The compensation log: ids of completed-forward steps, in completion order. */\n readonly done: readonly StepId[];\n /** Direction bit: `false` winding forward, `true` unwinding. */\n readonly compensating: boolean;\n /** The derived lifecycle tag (see `SagaPhase`). */\n readonly phase: SagaPhase;\n /** The forward failure that triggered compensation, if any. Carried, never interpreted. */\n readonly error?: unknown;\n /** The failure of an `undo` itself (only when `compensation_failed`). Carried, never interpreted. */\n readonly compensationError?: unknown;\n /** The work-queue ledger: one record per step, keyed by positional id. */\n readonly items: readonly QueueItem<I>[];\n}\n\n/** Whether the saga has terminated successfully (every step committed). */\nexport function isCommitted<I>(state: SagaState<I>): boolean {\n return state.phase === \"committed\";\n}\n\n/** Whether the saga has terminated in failure, fully unwound (every completed step undone). */\nexport function isAborted<I>(state: SagaState<I>): boolean {\n return state.phase === \"aborted\";\n}\n\n/**\n * Whether the saga has terminated in failure with an UNFINISHED rollback: an\n * `undo` itself failed, so some completed steps were never compensated and need\n * hand reconciliation. Terminal, but distinct from `aborted` (fully unwound).\n */\nexport function isCompensationFailed<I>(state: SagaState<I>): boolean {\n return state.phase === \"compensation_failed\";\n}\n\n/**\n * Whether the saga has reached a terminal phase (success, fully-unwound\n * failure, or compensation-failed). Derived — reads the slice, never mutates.\n * Consumers branch their own phase off this; no further verb has any effect\n * once it holds (the verbs guard on phase and no-op).\n */\nexport function isSettled<I>(state: SagaState<I>): boolean {\n return (\n state.phase === \"committed\" ||\n state.phase === \"aborted\" ||\n state.phase === \"compensation_failed\"\n );\n}\n\n/**\n * The starting slice: idle, nothing run, winding-forward direction. `items` is\n * empty until `start` stamps one ledger record per configured step — the slice\n * is generic in the ledger payload `I`, defaulting to the positional `StepId`\n * since a saga's \"input\" per step is just its index (the consumer's real inputs\n * ride on its own Msgs).\n */\nexport function initSaga<I = StepId>(): SagaState<I> {\n return {\n position: 0,\n done: [],\n compensating: false,\n phase: \"idle\",\n items: [],\n };\n}\n\n/**\n * Build the saga knob from `config`. Returns the uniform combinator contract:\n * `init` for the owned slice and the five lifecycle verbs (`start`, `stepOk`,\n * `stepErr`, `undoOk`, `undoErr`).\n *\n * No `subs` and no `handlers` — a saga runs no timers, and each step's `do` /\n * `undo` effect is owned by the consumer's `interpret` (the saga emits the Cmd;\n * the consumer performs it and routes the outcome back to a verb). The saga's\n * sole job is the ORDER: which Cmd to emit next, and when to pivot from winding\n * to unwinding (and when a failed `undo` ends the rollback). Pure-ops module —\n * matches `retry-backoff`'s shape.\n */\nexport function createSaga<D extends Cmd, U extends Cmd>(\n config: SagaConfig<D, U>,\n) {\n // The ledger seam, bound once. The saga delegates ledger construction to\n // `wq.enqueue` and every status flip to `wq.patch`, so the `QueueItem<StepId>`\n // shape stays insulated behind the verbs.\n const wq = queueAdapter<StepId>();\n\n /**\n * One ledger record per configured step, keyed by its positional id. Each\n * record is stamped by the `QueueAdapter`'s `enqueue` (the saga delegates\n * ledger construction to the verb rather than building records inline).\n *\n * The positional id is stringified — `QueueItem.id` is a string, the saga's\n * step identity is the index; `String(index)` keeps both the ledger's\n * string-id contract and the positional identity. `enqueuedAt` is `0`: saga\n * verbs are pure and the saga makes no timestamp-based decision (ordering is\n * positional), so there is no clock to read here.\n */\n function freshLedger(): QueueItem<StepId>[] {\n let items: QueueItem<StepId>[] = [];\n config.steps.forEach((_step, index) => {\n items = wq.enqueue(items, index, 0, String(index)).next;\n });\n return items;\n }\n\n /**\n * Flip the ledger record for step `index` to `status`, delegating the\n * transition to the `QueueAdapter`'s `patch` (keyed by the record's string\n * id, `String(index)`) so the status flip is NOT re-rolled here. PURE —\n * `patch` returns a new array; an unknown id returns a copy unchanged (the\n * verbs guard against out-of-range, but the helper is total).\n */\n function setStatus(\n items: readonly QueueItem<StepId>[],\n index: number,\n status: QueueItemStatus,\n ): QueueItem<StepId>[] {\n return wq.patch(items, String(index), (item) => ({\n ...item,\n status,\n })).next;\n }\n\n /**\n * The step at `index`, or `undefined` if out of range. Centralizes the bounds\n * check so the verbs read a step through a narrowing guard instead of a\n * non-null assertion: `const step = stepAt(i); if (step === undefined) …`.\n */\n function stepAt(index: number): SagaStep<D, U> | undefined {\n return config.steps[index];\n }\n\n /** The most-recently-completed step id (tail of `done`), or `undefined` if the log is empty. */\n function lastDone(done: readonly StepId[]): StepId | undefined {\n return done[done.length - 1];\n }\n\n /**\n * Begin the saga: stamp a `pending` ledger record per step, mark step 0\n * `running`, and emit step 0's `do` Cmd. PURE.\n *\n * A saga with zero configured steps commits immediately (nothing to do, so\n * the empty transaction trivially succeeds) and emits no Cmd. Calling `start`\n * on an already-started saga (phase ≠ `\"idle\"`) is a no-op: the slice is\n * returned unchanged with no Cmds, so a stray re-`start` after a boot/replay\n * can't relaunch step 0 on top of an in-flight saga.\n */\n function start(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"idle\") return [state, []];\n\n const items = freshLedger();\n const first = stepAt(0);\n if (first === undefined) {\n // Empty transaction: trivially committed, no effect.\n return [{ ...state, phase: \"committed\", items }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: 0,\n phase: \"running\",\n items: setStatus(items, 0, \"running\"),\n };\n return [next, [first.do]];\n }\n\n /**\n * Record the in-flight step as committed and advance. PURE.\n *\n * Flips the current step's ledger record `running → done`, appends its id to\n * the compensation log, and:\n * - if a next step exists, marks it `running` and emits its `do` Cmd;\n * - if this was the last step, transitions to `committed` (terminal\n * success) and emits no Cmd.\n *\n * `stepOk` while not `running` (idle, compensating, or already terminal) is a\n * no-op — the runtime's serial dispatch makes a true double-`stepOk`\n * impossible, but a stale success Msg after a boot/replay is real, and\n * swallowing it is the safe direction (the step's outcome is already recorded\n * in the ledger).\n */\n function stepOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const completed = state.position;\n const itemsDone = setStatus(state.items, completed, \"done\");\n const done = [...state.done, completed];\n const nextPosition = completed + 1;\n const nextStep = stepAt(nextPosition);\n\n if (nextStep === undefined) {\n // No next step — the last step committed, so the whole transaction\n // succeeded.\n return [{ ...state, done, items: itemsDone, phase: \"committed\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: nextPosition,\n done,\n items: setStatus(itemsDone, nextPosition, \"running\"),\n };\n return [next, [nextStep.do]];\n }\n\n /**\n * Pivot from winding to unwinding: the in-flight step failed. PURE.\n *\n * Flips the failed step's ledger record `running → failed`, records the\n * `error`, sets the direction bit, and begins compensation by emitting the\n * `undo` of the MOST-recently-completed step (the tail of the compensation\n * log) — rollback is the mirror of progress, so the last step that succeeded\n * is the first to be undone.\n *\n * If nothing completed forward (the first step itself failed), there is\n * nothing to compensate: the saga goes straight to `aborted` (terminal\n * failure) and emits no `undo`.\n *\n * `stepErr` while not `running` is a no-op (same stale-Msg reasoning as\n * `stepOk`): once a saga is already compensating or terminal, a late failure\n * Msg for an old step must not restart or re-pivot the unwind.\n */\n function stepErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const failed = state.position;\n const items = setStatus(state.items, failed, \"failed\");\n\n // Compensate the most-recently-completed step first. `lastDone === undefined`\n // means nothing completed forward (the first step itself failed) — there is\n // nothing to undo, so the saga aborts straight away. The guard narrows\n // `undoIndex` to `StepId` and `undoStep` to a present step without a\n // non-null assertion.\n const undoIndex = lastDone(state.done);\n const undoStep = undoIndex === undefined ? undefined : stepAt(undoIndex);\n if (undoIndex === undefined || undoStep === undefined) {\n return [\n { ...state, items, error, compensating: true, phase: \"aborted\" },\n [],\n ];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n items,\n error,\n compensating: true,\n phase: \"compensating\",\n position: undoIndex,\n };\n return [next, [undoStep.undo]];\n }\n\n /**\n * Continue the rollback: the `undo` for the current compensation position\n * succeeded. PURE.\n *\n * Pops the just-undone step off the tail of the compensation log (flipping\n * its ledger record `done → cancelled` to mark it rolled back), and:\n * - if more completed steps remain, emits the next one's `undo` (continuing\n * in reverse) and moves `position` to it;\n * - if the log is now empty, transitions to `aborted` (terminal failure —\n * the saga is fully unwound) and emits no Cmd.\n *\n * `undoOk` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the forward verbs): an `undo`-success Msg that arrives after the saga has\n * already finished unwinding must not pop a step that isn't there.\n */\n function undoOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The tail of the log is the step we just compensated — mark it cancelled.\n // `undone === undefined` cannot happen while `compensating` (the phase is\n // only entered with a non-empty log), but the guard keeps the lookup total\n // and narrows `undone` to `StepId` without a non-null assertion.\n const undone = lastDone(state.done);\n if (undone === undefined) return [state, []];\n const remaining = state.done.slice(0, -1);\n const items = setStatus(state.items, undone, \"cancelled\");\n\n // The next step to compensate is the new tail of the log. `nextUndo`\n // narrows to `StepId` here and to a present step via `stepAt`, so the\n // continue-vs-terminate branch needs no non-null assertion.\n const nextUndo = lastDone(remaining);\n const nextStep = nextUndo === undefined ? undefined : stepAt(nextUndo);\n if (nextUndo === undefined || nextStep === undefined) {\n // Fully unwound — terminal failure.\n return [{ ...state, done: remaining, items, phase: \"aborted\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n done: remaining,\n items,\n position: nextUndo,\n };\n return [next, [nextStep.undo]];\n }\n\n /**\n * Halt the rollback: the `undo` for the current compensation position itself\n * FAILED. PURE.\n *\n * A failed compensation is a real-world fact — a refund that bounced, a hold\n * that won't release — and it must become visible terminal state, never leave\n * the saga wedged in `compensating` forever waiting for an `undoOk` that will\n * never come. So `undoErr`:\n * - flips the in-flight compensation step's ledger record `done → failed`\n * (its `undo` did not take, so it is NOT `cancelled`);\n * - records the undo failure in `compensationError` (distinct from `error`,\n * the forward failure that started the rollback);\n * - transitions to the terminal `compensation_failed` phase and emits no\n * Cmd. The compensation log is LEFT INTACT (`done` keeps the steps that\n * were never rolled back) so the consumer can see exactly which steps\n * still need hand reconciliation.\n *\n * `undoErr` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the other verbs): an `undo`-failure Msg that arrives after the saga has\n * already settled must not re-pivot a terminal saga.\n */\n function undoErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The step whose `undo` just failed is the current compensation position.\n // Its forward effect stands un-reversed, so its record goes `done → failed`,\n // not `cancelled`.\n const items = setStatus(state.items, state.position, \"failed\");\n return [\n {\n ...state,\n items,\n compensationError: error,\n phase: \"compensation_failed\",\n },\n [],\n ];\n }\n\n return {\n init: initSaga<StepId>,\n start,\n stepOk,\n stepErr,\n undoOk,\n undoErr,\n isCommitted,\n isAborted,\n isCompensationFailed,\n isSettled,\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/saga/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/saga — a forward-then-compensate transaction over an ordered list\n * of reversible steps.\n *\n * The Level-2 combinator for \"run these N steps in order; if any step fails,\n * undo the steps that already succeeded, in reverse.\" It is the distributed-\n * transaction / compensation knob: each step is a `{ do, undo }` pair of Cmds,\n * the saga drives `do` forward one step at a time, and on the first failure it\n * pivots and drives `undo` backward over exactly the steps that completed.\n *\n * **Boundary with `@demlik/tea/workflow` — the compensation test.** Both drive\n * an ordered multi-step transaction with reverse-order compensation; the line\n * between them is whether every step carries a true inverse. Here\n * `SagaStep<D, U>` REQUIRES both `do` and `undo` — a step without its inverse\n * is unrepresentable, so full reversibility is a type-level guarantee.\n * Workflow's `WorkflowStep.compensation` is OPTIONAL — an irreversible step (a\n * sent email) is skipped during the unwind. Pick by the test: **every step\n * must be reversible → saga; some steps are irreversible but the run must\n * finish → workflow.** Cross to workflow when the transaction includes steps\n * that can't be undone (or when you want each activity recorded as a durable\n * owed effect on the `../do` ledger); stay here when the type system should\n * enforce that every step can be undone.\n *\n * The recipe (which bricks it composes):\n * - `../work-queue/adapter` — the saga's forward progress IS a work-queue\n * lifecycle. Each step's record carries the same `{ id, status, input }`\n * vocabulary (`pending → running → done | failed | cancelled`), so the step\n * taxonomy is the substrate's status enum rather than a parallel invention.\n * We reuse the pure `QueueItem` / `QueueItemStatus` types AND delegate every\n * status flip to the `QueueAdapter` verbs: `wq.enqueue` stamps each fresh\n * ledger record, and `wq.patch` performs every\n * `pending → running → done | failed | cancelled` transition. The status\n * transitions are NOT re-rolled here — they live in exactly one place\n * behind the adapter, per the package's one-shape rule. The async\n * `createQueue(store)` adapter is NOT used: its `load → mutate → save` reads\n * the clock and lives at an I/O boundary, while a combinator's verbs must be\n * pure (invariant 2). We bind the same ops to the in-Model ledger instead —\n * durable + replayable, the two non-negotiables every knob preserves.\n *\n * The compensation log is the spec's `done: StepId[]` — the indices of the\n * steps that completed forward, in the order they completed. On `stepErr` the\n * saga walks that log in reverse, emitting one `undo` per `undoOk`, so the\n * unwind is the exact mirror of the wind. If an `undo` ITSELF fails, `undoErr`\n * pivots to the terminal `compensation_failed` phase: a failed compensation is\n * a real-world fact (a refund that bounced, a hold that won't release) that the\n * consumer must see and reconcile by hand — never a saga wedged in\n * `compensating` forever.\n *\n * Typical wiring (combinator form — splice the hooks into your machine):\n *\n * const saga = createSaga<BookFlight | ChargeCard | ..., Cancel | Refund | ...>({\n * steps: [\n * { do: { type: \"book_flight\" }, undo: { type: \"cancel_flight\" } },\n * { do: { type: \"charge_card\" }, undo: { type: \"refund_card\" } },\n * { do: { type: \"reserve_hotel\" },undo: { type: \"release_hotel\" } },\n * ],\n * });\n *\n * // in `update`:\n * begin: (s) => lift(s, saga.start(s.saga)),\n * step_ok: (s) => lift(s, saga.stepOk(s.saga)),\n * step_err: (s, m) => lift(s, saga.stepErr(s.saga, m.error)),\n * undo_ok: (s) => lift(s, saga.undoOk(s.saga)),\n * undo_err: (s, m) => lift(s, saga.undoErr(s.saga, m.error)),\n *\n * Every verb is PURE: it returns a NEW slice plus the Cmds to perform (the next\n * `do` going forward, or the next `undo` rolling back). The reducer never reads\n * a clock or RNG — step identity is positional (the index into `config.steps`),\n * so progress survives a round-trip through the durable slice and resumes\n * exactly where it left off after a DO eviction.\n *\n * There is no `subs` and no `handlers`: a saga runs no timers, and the `do` /\n * `undo` Cmds are fully caller-defined (the consumer's own `interpret` performs\n * each effect and routes Ok/Err back to the `stepOk` / `stepErr` / `undoOk`\n * verbs — the same shape `resilient-fetch` uses with `tryInterpret`). The saga\n * owns the ORDER; the consumer owns the I/O. Pure-ops module, like\n * `retry-backoff`.\n *\n * NOT a substrate primitive: it depends only on `../work-queue`'s pure types\n * and `../index`'s `Cmd` type. Consumers reach it via the `@demlik/tea/saga`\n * subpath.\n */\n\nimport type { Cmd } from \"../index\";\nimport type { QueueItem, QueueItemStatus } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A step's stable identity. Positional: the 0-based index into\n * `config.steps`. Positional ids are PURE (no clock, no RNG) and survive a\n * round-trip through the durable slice, so `done` can be replayed to resume an\n * interrupted saga exactly. The compensation log is a list of these.\n */\nexport type StepId = number;\n\n/**\n * One step of the saga: the forward effect and its compensating inverse, both\n * as data (plain Cmds). `do` runs while winding forward; `undo` runs while\n * unwinding after a failure. The pair is the unit of reversibility — a step\n * that completed (`do` succeeded) is exactly a step whose `undo` must run if a\n * later step fails.\n */\nexport interface SagaStep<D extends Cmd, U extends Cmd> {\n /** The forward effect, performed while the saga winds forward. */\n readonly do: D;\n /** The compensating inverse, performed (in reverse) while the saga unwinds. */\n readonly undo: U;\n}\n\n/**\n * The knob. The ordered list of reversible steps. The list is fixed for the\n * life of one saga instance (a saga drives a known transaction); per-run inputs\n * ride on the consumer's Msgs, not here.\n */\nexport interface SagaConfig<D extends Cmd, U extends Cmd> {\n /** The steps, in forward order. `do` runs `0 → n-1`; `undo` runs the completed prefix `n-1 → 0`. */\n readonly steps: readonly SagaStep<D, U>[];\n}\n\n/**\n * The lifecycle phase of a saga, narrowed at the type level so a consumer can\n * branch on `state.phase` (and a Transitions-table reducer can key on it).\n *\n * - `\"idle\"` — created, not started. No step has run.\n * - `\"running\"` — winding forward; `position` is the in-flight step.\n * - `\"compensating\"` — a step failed; unwinding `done` in reverse.\n * - `\"committed\"` — every step completed forward. Terminal success.\n * - `\"aborted\"` — compensation finished (every completed step undone, or\n * the first step itself failed with nothing to undo).\n * Terminal failure, fully unwound.\n * - `\"compensation_failed\"` — an `undo` itself failed mid-rollback. Terminal\n * failure, PARTIALLY unwound: the steps still in `done`\n * were never compensated and must be reconciled by hand.\n * Distinguished from `aborted` precisely so a stuck\n * rollback is visible state, not a saga wedged in\n * `compensating` forever.\n */\nexport type SagaPhase =\n | \"idle\"\n | \"running\"\n | \"compensating\"\n | \"committed\"\n | \"aborted\"\n | \"compensation_failed\";\n\n/**\n * The slice this knob owns.\n *\n * The spec's three fields are load-bearing:\n * - `position` — the index of the step currently in flight while `running`,\n * or the index being compensated while `compensating`. Survives eviction,\n * so `boot` resumes mid-saga from the durable slice.\n * - `done` — the compensation log: the ids of steps that completed forward,\n * in completion order. `undo` walks this in reverse. This is the single\n * source of truth for what must be unwound.\n * - `compensating` — the direction bit. `false` while winding forward, `true`\n * once a failure has pivoted the saga into rollback.\n *\n * Plus:\n * - `phase` — the derived lifecycle tag (see `SagaPhase`). Kept in the slice\n * (not recomputed) so a consumer's view and Transitions reducer can key on\n * `state.phase` directly, and the terminal states (`committed` / `aborted`)\n * are explicit facts rather than \"position past the end\" arithmetic.\n * - `error` — the forward failure that triggered compensation, carried\n * forward verbatim (`unknown`) for the consumer to read, never interpreted\n * (same discipline as `retry-backoff`'s `lastError` and `fan-out`'s\n * `error`).\n * - `compensationError` — the failure of an `undo` itself, set only when the\n * saga reaches `compensation_failed`. Kept distinct from `error` (the\n * forward failure) so a consumer reconciling a stuck rollback can read both\n * \"what broke the transaction\" and \"which compensation then bounced\".\n * Carried verbatim, never interpreted, same as `error`.\n * - `items` — the work-queue ledger, one `QueueItem` per step, keyed by the\n * step's positional id. The single source of truth for each step's status;\n * `phase` / `position` / `done` are the projection the verbs keep in\n * lockstep with it.\n *\n * Every field is `readonly` and JSON-serializable (plain numbers, arrays, and\n * the work-queue's plain `QueueItem` records), so the slice round-trips through\n * a consumer's `Store<S>` unchanged — durable across DO eviction.\n */\nexport interface SagaState<I = StepId> {\n /** The step in flight (`running`) or being compensated (`compensating`). */\n readonly position: number;\n /** The compensation log: ids of completed-forward steps, in completion order. */\n readonly done: readonly StepId[];\n /** Direction bit: `false` winding forward, `true` unwinding. */\n readonly compensating: boolean;\n /** The derived lifecycle tag (see `SagaPhase`). */\n readonly phase: SagaPhase;\n /** The forward failure that triggered compensation, if any. Carried, never interpreted. */\n readonly error?: unknown;\n /** The failure of an `undo` itself (only when `compensation_failed`). Carried, never interpreted. */\n readonly compensationError?: unknown;\n /** The work-queue ledger: one record per step, keyed by positional id. */\n readonly items: readonly QueueItem<I>[];\n}\n\n/** Whether the saga has terminated successfully (every step committed). */\nexport function isCommitted<I>(state: SagaState<I>): boolean {\n return state.phase === \"committed\";\n}\n\n/** Whether the saga has terminated in failure, fully unwound (every completed step undone). */\nexport function isAborted<I>(state: SagaState<I>): boolean {\n return state.phase === \"aborted\";\n}\n\n/**\n * Whether the saga has terminated in failure with an UNFINISHED rollback: an\n * `undo` itself failed, so some completed steps were never compensated and need\n * hand reconciliation. Terminal, but distinct from `aborted` (fully unwound).\n */\nexport function isCompensationFailed<I>(state: SagaState<I>): boolean {\n return state.phase === \"compensation_failed\";\n}\n\n/**\n * Whether the saga has reached a terminal phase (success, fully-unwound\n * failure, or compensation-failed). Derived — reads the slice, never mutates.\n * Consumers branch their own phase off this; no further verb has any effect\n * once it holds (the verbs guard on phase and no-op).\n */\nexport function isSettled<I>(state: SagaState<I>): boolean {\n return (\n state.phase === \"committed\" ||\n state.phase === \"aborted\" ||\n state.phase === \"compensation_failed\"\n );\n}\n\n/**\n * The starting slice: idle, nothing run, winding-forward direction. `items` is\n * empty until `start` stamps one ledger record per configured step — the slice\n * is generic in the ledger payload `I`, defaulting to the positional `StepId`\n * since a saga's \"input\" per step is just its index (the consumer's real inputs\n * ride on its own Msgs).\n */\nexport function initSaga<I = StepId>(): SagaState<I> {\n return {\n position: 0,\n done: [],\n compensating: false,\n phase: \"idle\",\n items: [],\n };\n}\n\n/**\n * Build the saga knob from `config`. Returns the uniform combinator contract:\n * `init` for the owned slice and the five lifecycle verbs (`start`, `stepOk`,\n * `stepErr`, `undoOk`, `undoErr`).\n *\n * No `subs` and no `handlers` — a saga runs no timers, and each step's `do` /\n * `undo` effect is owned by the consumer's `interpret` (the saga emits the Cmd;\n * the consumer performs it and routes the outcome back to a verb). The saga's\n * sole job is the ORDER: which Cmd to emit next, and when to pivot from winding\n * to unwinding (and when a failed `undo` ends the rollback). Pure-ops module —\n * matches `retry-backoff`'s shape.\n */\nexport function createSaga<D extends Cmd, U extends Cmd>(\n config: SagaConfig<D, U>,\n) {\n // The ledger seam, bound once. The saga delegates ledger construction to\n // `wq.enqueue` and every status flip to `wq.patch`, so the `QueueItem<StepId>`\n // shape stays insulated behind the verbs.\n const wq = queueAdapter<StepId>();\n\n /**\n * One ledger record per configured step, keyed by its positional id. Each\n * record is stamped by the `QueueAdapter`'s `enqueue` (the saga delegates\n * ledger construction to the verb rather than building records inline).\n *\n * The positional id is stringified — `QueueItem.id` is a string, the saga's\n * step identity is the index; `String(index)` keeps both the ledger's\n * string-id contract and the positional identity. `enqueuedAt` is `0`: saga\n * verbs are pure and the saga makes no timestamp-based decision (ordering is\n * positional), so there is no clock to read here.\n */\n function freshLedger(): QueueItem<StepId>[] {\n let items: QueueItem<StepId>[] = [];\n config.steps.forEach((_step, index) => {\n items = wq.enqueue(items, index, 0, String(index)).next;\n });\n return items;\n }\n\n /**\n * Flip the ledger record for step `index` to `status`, delegating the\n * transition to the `QueueAdapter`'s `patch` (keyed by the record's string\n * id, `String(index)`) so the status flip is NOT re-rolled here. PURE —\n * `patch` returns a new array; an unknown id returns a copy unchanged (the\n * verbs guard against out-of-range, but the helper is total).\n */\n function setStatus(\n items: readonly QueueItem<StepId>[],\n index: number,\n status: QueueItemStatus,\n ): QueueItem<StepId>[] {\n return wq.patch(items, String(index), (item) => ({\n ...item,\n status,\n })).next;\n }\n\n /**\n * The step at `index`, or `undefined` if out of range. Centralizes the bounds\n * check so the verbs read a step through a narrowing guard instead of a\n * non-null assertion: `const step = stepAt(i); if (step === undefined) …`.\n */\n function stepAt(index: number): SagaStep<D, U> | undefined {\n return config.steps[index];\n }\n\n /** The most-recently-completed step id (tail of `done`), or `undefined` if the log is empty. */\n function lastDone(done: readonly StepId[]): StepId | undefined {\n return done[done.length - 1];\n }\n\n /**\n * Begin the saga: stamp a `pending` ledger record per step, mark step 0\n * `running`, and emit step 0's `do` Cmd. PURE.\n *\n * A saga with zero configured steps commits immediately (nothing to do, so\n * the empty transaction trivially succeeds) and emits no Cmd. Calling `start`\n * on an already-started saga (phase ≠ `\"idle\"`) is a no-op: the slice is\n * returned unchanged with no Cmds, so a stray re-`start` after a boot/replay\n * can't relaunch step 0 on top of an in-flight saga.\n */\n function start(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"idle\") return [state, []];\n\n const items = freshLedger();\n const first = stepAt(0);\n if (first === undefined) {\n // Empty transaction: trivially committed, no effect.\n return [{ ...state, phase: \"committed\", items }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: 0,\n phase: \"running\",\n items: setStatus(items, 0, \"running\"),\n };\n return [next, [first.do]];\n }\n\n /**\n * Record the in-flight step as committed and advance. PURE.\n *\n * Flips the current step's ledger record `running → done`, appends its id to\n * the compensation log, and:\n * - if a next step exists, marks it `running` and emits its `do` Cmd;\n * - if this was the last step, transitions to `committed` (terminal\n * success) and emits no Cmd.\n *\n * `stepOk` while not `running` (idle, compensating, or already terminal) is a\n * no-op — the runtime's serial dispatch makes a true double-`stepOk`\n * impossible, but a stale success Msg after a boot/replay is real, and\n * swallowing it is the safe direction (the step's outcome is already recorded\n * in the ledger).\n */\n function stepOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly D[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const completed = state.position;\n const itemsDone = setStatus(state.items, completed, \"done\");\n const done = [...state.done, completed];\n const nextPosition = completed + 1;\n const nextStep = stepAt(nextPosition);\n\n if (nextStep === undefined) {\n // No next step — the last step committed, so the whole transaction\n // succeeded.\n return [{ ...state, done, items: itemsDone, phase: \"committed\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n position: nextPosition,\n done,\n items: setStatus(itemsDone, nextPosition, \"running\"),\n };\n return [next, [nextStep.do]];\n }\n\n /**\n * Pivot from winding to unwinding: the in-flight step failed. PURE.\n *\n * Flips the failed step's ledger record `running → failed`, records the\n * `error`, sets the direction bit, and begins compensation by emitting the\n * `undo` of the MOST-recently-completed step (the tail of the compensation\n * log) — rollback is the mirror of progress, so the last step that succeeded\n * is the first to be undone.\n *\n * If nothing completed forward (the first step itself failed), there is\n * nothing to compensate: the saga goes straight to `aborted` (terminal\n * failure) and emits no `undo`.\n *\n * `stepErr` while not `running` is a no-op (same stale-Msg reasoning as\n * `stepOk`): once a saga is already compensating or terminal, a late failure\n * Msg for an old step must not restart or re-pivot the unwind.\n */\n function stepErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"running\") return [state, []];\n\n const failed = state.position;\n const items = setStatus(state.items, failed, \"failed\");\n\n // Compensate the most-recently-completed step first. `lastDone === undefined`\n // means nothing completed forward (the first step itself failed) — there is\n // nothing to undo, so the saga aborts straight away. The guard narrows\n // `undoIndex` to `StepId` and `undoStep` to a present step without a\n // non-null assertion.\n const undoIndex = lastDone(state.done);\n const undoStep = undoIndex === undefined ? undefined : stepAt(undoIndex);\n if (undoIndex === undefined || undoStep === undefined) {\n return [\n { ...state, items, error, compensating: true, phase: \"aborted\" },\n [],\n ];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n items,\n error,\n compensating: true,\n phase: \"compensating\",\n position: undoIndex,\n };\n return [next, [undoStep.undo]];\n }\n\n /**\n * Continue the rollback: the `undo` for the current compensation position\n * succeeded. PURE.\n *\n * Pops the just-undone step off the tail of the compensation log (flipping\n * its ledger record `done → cancelled` to mark it rolled back), and:\n * - if more completed steps remain, emits the next one's `undo` (continuing\n * in reverse) and moves `position` to it;\n * - if the log is now empty, transitions to `aborted` (terminal failure —\n * the saga is fully unwound) and emits no Cmd.\n *\n * `undoOk` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the forward verbs): an `undo`-success Msg that arrives after the saga has\n * already finished unwinding must not pop a step that isn't there.\n */\n function undoOk(\n state: SagaState<StepId>,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The tail of the log is the step we just compensated — mark it cancelled.\n // `undone === undefined` cannot happen while `compensating` (the phase is\n // only entered with a non-empty log), but the guard keeps the lookup total\n // and narrows `undone` to `StepId` without a non-null assertion.\n const undone = lastDone(state.done);\n if (undone === undefined) return [state, []];\n const remaining = state.done.slice(0, -1);\n const items = setStatus(state.items, undone, \"cancelled\");\n\n // The next step to compensate is the new tail of the log. `nextUndo`\n // narrows to `StepId` here and to a present step via `stepAt`, so the\n // continue-vs-terminate branch needs no non-null assertion.\n const nextUndo = lastDone(remaining);\n const nextStep = nextUndo === undefined ? undefined : stepAt(nextUndo);\n if (nextUndo === undefined || nextStep === undefined) {\n // Fully unwound — terminal failure.\n return [{ ...state, done: remaining, items, phase: \"aborted\" }, []];\n }\n\n const next: SagaState<StepId> = {\n ...state,\n done: remaining,\n items,\n position: nextUndo,\n };\n return [next, [nextStep.undo]];\n }\n\n /**\n * Halt the rollback: the `undo` for the current compensation position itself\n * FAILED. PURE.\n *\n * A failed compensation is a real-world fact — a refund that bounced, a hold\n * that won't release — and it must become visible terminal state, never leave\n * the saga wedged in `compensating` forever waiting for an `undoOk` that will\n * never come. So `undoErr`:\n * - flips the in-flight compensation step's ledger record `done → failed`\n * (its `undo` did not take, so it is NOT `cancelled`);\n * - records the undo failure in `compensationError` (distinct from `error`,\n * the forward failure that started the rollback);\n * - transitions to the terminal `compensation_failed` phase and emits no\n * Cmd. The compensation log is LEFT INTACT (`done` keeps the steps that\n * were never rolled back) so the consumer can see exactly which steps\n * still need hand reconciliation.\n *\n * `undoErr` while not `compensating` is a no-op (same stale-Msg reasoning as\n * the other verbs): an `undo`-failure Msg that arrives after the saga has\n * already settled must not re-pivot a terminal saga.\n */\n function undoErr(\n state: SagaState<StepId>,\n error: unknown,\n ): readonly [SagaState<StepId>, readonly U[]] {\n if (state.phase !== \"compensating\") return [state, []];\n\n // The step whose `undo` just failed is the current compensation position.\n // Its forward effect stands un-reversed, so its record goes `done → failed`,\n // not `cancelled`.\n const items = setStatus(state.items, state.position, \"failed\");\n return [\n {\n ...state,\n items,\n compensationError: error,\n phase: \"compensation_failed\",\n },\n [],\n ];\n }\n\n return {\n init: initSaga<StepId>,\n start,\n stepOk,\n stepErr,\n undoOk,\n undoErr,\n isCommitted,\n isAborted,\n isCompensationFailed,\n isSettled,\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { C as Cmd, I as Interpret } from '../core-BmlJOvxM.js';
1
+ import { C as Cmd, c as Interpret } from '../core-42kapW0n.js';
2
2
 
3
3
  /**
4
4
  * @demlik/tea/snapshot — periodic state checkpoint to a host store (R2/KV-shaped).
@@ -1,5 +1,5 @@
1
- export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved } from '../chunk-5KVHID6A.js';
2
- import '../chunk-6VDCTR7N.js';
3
- import '../chunk-DYRKDCJI.js';
1
+ export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved } from '../chunk-SPWVPIFQ.js';
2
+ import '../chunk-D4NJBQAH.js';
3
+ import '../chunk-PBDYW2TU.js';
4
4
  //# sourceMappingURL=index.js.map
5
5
  //# sourceMappingURL=index.js.map