@demlik/tea 0.1.1 → 0.2.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 (150) hide show
  1. package/dist/agent/index.d.ts +2 -2
  2. package/dist/agent/index.js +8 -8
  3. package/dist/authed-call/index.d.ts +3 -3
  4. package/dist/authed-call/index.js +5 -5
  5. package/dist/await-terminal/index.d.ts +1 -1
  6. package/dist/await-terminal/index.js +2 -2
  7. package/dist/batch-window/index.d.ts +2 -2
  8. package/dist/batch-window/index.js +4 -4
  9. package/dist/cache/index.d.ts +2 -2
  10. package/dist/{chunk-EIZJTYKR.js → chunk-5KVHID6A.js} +4 -4
  11. package/dist/{chunk-EIZJTYKR.js.map → chunk-5KVHID6A.js.map} +1 -1
  12. package/dist/{chunk-RVJFGRGK.js → chunk-6GIGQT6C.js} +4 -4
  13. package/dist/chunk-6GIGQT6C.js.map +1 -0
  14. package/dist/{chunk-FH5RUIIL.js → chunk-6OUDPSSG.js} +3 -3
  15. package/dist/{chunk-FH5RUIIL.js.map → chunk-6OUDPSSG.js.map} +1 -1
  16. package/dist/{chunk-2WZHS4P2.js → chunk-6VDCTR7N.js} +25 -40
  17. package/dist/chunk-6VDCTR7N.js.map +1 -0
  18. package/dist/{chunk-BNJYE4AU.js → chunk-DYRKDCJI.js} +46 -16
  19. package/dist/chunk-DYRKDCJI.js.map +1 -0
  20. package/dist/{chunk-RGTPPF4U.js → chunk-EGWTT5XJ.js} +3 -3
  21. package/dist/{chunk-RGTPPF4U.js.map → chunk-EGWTT5XJ.js.map} +1 -1
  22. package/dist/{chunk-A56GNPI7.js → chunk-EMJTPHI7.js} +5 -23
  23. package/dist/chunk-EMJTPHI7.js.map +1 -0
  24. package/dist/{chunk-IFBQJYGI.js → chunk-FOGL3TC7.js} +3 -3
  25. package/dist/{chunk-IFBQJYGI.js.map → chunk-FOGL3TC7.js.map} +1 -1
  26. package/dist/{chunk-RVU3RBE6.js → chunk-G2U7DJHL.js} +6 -4
  27. package/dist/chunk-G2U7DJHL.js.map +1 -0
  28. package/dist/{chunk-WUV3PSCZ.js → chunk-HCEVHFJ7.js} +2 -2
  29. package/dist/chunk-HCEVHFJ7.js.map +1 -0
  30. package/dist/{chunk-EDGDR5XX.js → chunk-IK4S5DJN.js} +3 -3
  31. package/dist/{chunk-EDGDR5XX.js.map → chunk-IK4S5DJN.js.map} +1 -1
  32. package/dist/{chunk-EQRUAP2A.js → chunk-J6527VRQ.js} +2 -2
  33. package/dist/chunk-J6527VRQ.js.map +1 -0
  34. package/dist/{chunk-TAO4YOLO.js → chunk-M6UZT7HS.js} +3 -3
  35. package/dist/{chunk-TAO4YOLO.js.map → chunk-M6UZT7HS.js.map} +1 -1
  36. package/dist/{chunk-QGZW4W3E.js → chunk-SKAMZKTE.js} +3 -3
  37. package/dist/{chunk-QGZW4W3E.js.map → chunk-SKAMZKTE.js.map} +1 -1
  38. package/dist/{chunk-TC4HWABA.js → chunk-VFGKTGGC.js} +7 -7
  39. package/dist/{chunk-TC4HWABA.js.map → chunk-VFGKTGGC.js.map} +1 -1
  40. package/dist/{chunk-D5EQDLDV.js → chunk-ZDRN3YCE.js} +4 -4
  41. package/dist/{chunk-D5EQDLDV.js.map → chunk-ZDRN3YCE.js.map} +1 -1
  42. package/dist/{core-CymXZCeY.d.ts → core-BmlJOvxM.d.ts} +20 -1
  43. package/dist/deadline/index.d.ts +2 -2
  44. package/dist/deadline/index.js +3 -3
  45. package/dist/debounce/index.d.ts +6 -0
  46. package/dist/debounce/index.js +1 -1
  47. package/dist/do/index.d.ts +422 -315
  48. package/dist/do/index.js +173 -149
  49. package/dist/do/index.js.map +1 -1
  50. package/dist/{durable-effects-2MDxDs45.d.ts → durable-effects-9LxFpmh3.d.ts} +1 -1
  51. package/dist/extension/index.d.ts +1 -1
  52. package/dist/extension/index.js +3 -3
  53. package/dist/extension/react.js +3 -3
  54. package/dist/extension/subs/index.d.ts +2 -2
  55. package/dist/fan-out/index.d.ts +1 -1
  56. package/dist/idempotent-intake/index.d.ts +1 -1
  57. package/dist/index.d.ts +20 -3
  58. package/dist/index.js +2 -2
  59. package/dist/llm-call/index.d.ts +2 -2
  60. package/dist/llm-call/index.js +5 -5
  61. package/dist/machine-viz/index.d.ts +4 -4
  62. package/dist/machine-viz/index.js +4 -8
  63. package/dist/machine-viz/index.js.map +1 -1
  64. package/dist/mem/index.d.ts +1 -1
  65. package/dist/monitored-run/index.d.ts +2 -2
  66. package/dist/monitored-run/index.js +5 -5
  67. package/dist/node/index.d.ts +1 -1
  68. package/dist/paginated-walk/index.d.ts +2 -2
  69. package/dist/paginated-walk/index.js +5 -5
  70. package/dist/parity/index.d.ts +10 -4
  71. package/dist/parity/index.js +6 -5
  72. package/dist/parity/index.js.map +1 -1
  73. package/dist/pbt/arbitraries/index.d.ts +1 -1
  74. package/dist/pbt/index.d.ts +15 -8
  75. package/dist/pbt/index.js +5 -11
  76. package/dist/pbt/index.js.map +1 -1
  77. package/dist/pbt/runners/index.d.ts +1 -1
  78. package/dist/pbt/runners/index.js +3 -3
  79. package/dist/poller/index.d.ts +2 -2
  80. package/dist/poller/index.js +3 -3
  81. package/dist/prediction/index.d.ts +23 -2
  82. package/dist/prediction/index.js +2 -2
  83. package/dist/pure/index.d.ts +2 -2
  84. package/dist/pure/index.js +2 -2
  85. package/dist/react/index.d.ts +1 -1
  86. package/dist/react/index.js +8 -4
  87. package/dist/react/index.js.map +1 -1
  88. package/dist/reconciler/index.d.ts +2 -2
  89. package/dist/reconciler/index.js +5 -5
  90. package/dist/recorder/index.d.ts +1 -1
  91. package/dist/resilient-call/index.d.ts +45 -2
  92. package/dist/resilient-call/index.js +4 -4
  93. package/dist/retry-to-success/index.d.ts +109 -0
  94. package/dist/retry-to-success/index.js +35 -0
  95. package/dist/retry-to-success/index.js.map +1 -0
  96. package/dist/saga/index.d.ts +14 -1
  97. package/dist/saga/index.js.map +1 -1
  98. package/dist/snapshot/index.d.ts +1 -1
  99. package/dist/snapshot/index.js +3 -3
  100. package/dist/subs/index.d.ts +2 -2
  101. package/dist/testing/index.d.ts +1 -1
  102. package/dist/testing/index.js +2 -2
  103. package/dist/throttle/index.d.ts +9 -0
  104. package/dist/throttle/index.js +1 -1
  105. package/dist/throttled-input/index.d.ts +10 -2
  106. package/dist/throttled-input/index.js +6 -6
  107. package/dist/throttled-input/index.js.map +1 -1
  108. package/dist/token-refresh/index.d.ts +1 -1
  109. package/dist/token-refresh/index.js +3 -3
  110. package/dist/trace-replay/index.d.ts +1 -1
  111. package/dist/trace-replay/index.js +3 -3
  112. package/dist/{types-N9LZbdnr.d.ts → types-BoUWgwSm.d.ts} +1 -1
  113. package/dist/with-deadline/index.d.ts +1 -1
  114. package/dist/with-deadline/index.js +5 -24
  115. package/dist/with-deadline/index.js.map +1 -1
  116. package/dist/with-resilience/index.d.ts +2 -2
  117. package/dist/with-resilience/index.js +13 -36
  118. package/dist/with-resilience/index.js.map +1 -1
  119. package/dist/with-telemetry/index.d.ts +1 -1
  120. package/dist/with-telemetry/index.js +4 -29
  121. package/dist/with-telemetry/index.js.map +1 -1
  122. package/dist/work-queue/adapter.d.ts +1 -1
  123. package/dist/work-queue/index.d.ts +1 -1
  124. package/dist/work-queue/ops.d.ts +1 -1
  125. package/dist/workflow/index.d.ts +16 -10
  126. package/dist/workflow/index.js.map +1 -1
  127. package/package.json +8 -20
  128. package/dist/chunk-2WZHS4P2.js.map +0 -1
  129. package/dist/chunk-7VEG4KHB.js +0 -202
  130. package/dist/chunk-7VEG4KHB.js.map +0 -1
  131. package/dist/chunk-A56GNPI7.js.map +0 -1
  132. package/dist/chunk-BNJYE4AU.js.map +0 -1
  133. package/dist/chunk-EQRUAP2A.js.map +0 -1
  134. package/dist/chunk-RVJFGRGK.js.map +0 -1
  135. package/dist/chunk-RVU3RBE6.js.map +0 -1
  136. package/dist/chunk-WUV3PSCZ.js.map +0 -1
  137. package/dist/chunk-XRXKH7KA.js +0 -268
  138. package/dist/chunk-XRXKH7KA.js.map +0 -1
  139. package/dist/raft/demo.d.ts +0 -105
  140. package/dist/raft/demo.js +0 -172
  141. package/dist/raft/demo.js.map +0 -1
  142. package/dist/raft/index.d.ts +0 -548
  143. package/dist/raft/index.js +0 -7
  144. package/dist/raft/index.js.map +0 -1
  145. package/dist/raft/sim.d.ts +0 -222
  146. package/dist/raft/sim.js +0 -8
  147. package/dist/raft/sim.js.map +0 -1
  148. package/dist/raft/timeline.d.ts +0 -46
  149. package/dist/raft/timeline.js +0 -62
  150. package/dist/raft/timeline.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { DeadlineSub } from '../deadline/index.js';
2
2
  import { IdempotencyStore } from '../idempotency/index.js';
3
- import { C as Cmd, S as Sub } from '../core-CymXZCeY.js';
3
+ import { C as Cmd, S as Sub } from '../core-BmlJOvxM.js';
4
4
  import { RetryPolicy, RetryState } from '../retry-backoff/index.js';
5
- import '../types-N9LZbdnr.js';
5
+ import '../types-BoUWgwSm.js';
6
6
 
7
7
  /**
8
8
  * @demlik/tea/poller — "poll a source every N ms until a predicate holds, with
@@ -1,10 +1,10 @@
1
- import { deadlineSub } from '../chunk-QGZW4W3E.js';
1
+ import { deadlineSub } from '../chunk-SKAMZKTE.js';
2
2
  import { asRng, defaultRetryPolicy, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-5VNWZADP.js';
3
3
  import { idempotencyMemory } from '../chunk-BYQUGDD3.js';
4
4
  import '../chunk-K575PT4S.js';
5
5
  import '../chunk-2YFLOCWX.js';
6
- import '../chunk-2WZHS4P2.js';
7
- import '../chunk-BNJYE4AU.js';
6
+ import '../chunk-6VDCTR7N.js';
7
+ import '../chunk-DYRKDCJI.js';
8
8
 
9
9
  // src/poller/index.ts
10
10
  function pollerSubId(atMs) {
@@ -1,4 +1,4 @@
1
- import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
2
2
 
3
3
  /**
4
4
  * @demlik/tea/prediction — the client-prediction ack primitive (epic #186, facet 2).
@@ -56,6 +56,27 @@ interface Ack {
56
56
  }
57
57
  /** Construct an `Ack` for a last-applied sequence number. */
58
58
  declare const ack: (lastAppliedSeq: Seq) => Ack;
59
+ /**
60
+ * The "nothing applied yet" cursor — the ack value for a server that has applied
61
+ * no client input at all. Because `nextSeq` mints **0-based** seqs and
62
+ * `partitionByAck` acks **inclusively** (`seq <= lastAppliedSeq`), the pre-first
63
+ * cursor must sit strictly *below* seq 0: `lastAppliedSeq = 0` would falsely ack
64
+ * the seq-0 input before the server ever saw it. `-1` is "before any seq", and
65
+ * `partitionByAck(buffer, NO_ACK)` therefore leaves the whole buffer PENDING.
66
+ *
67
+ * This names the boundary value on the seam so no consumer has to re-discover it
68
+ * as a magic number: it is the primitive's own design (0-based mint + inclusive
69
+ * partition) that creates the trap, so it is the primitive's job to name the
70
+ * escape. Pair with {@link initAck} when an {@link Ack} is wanted instead of the
71
+ * bare `Seq`.
72
+ */
73
+ declare const NO_ACK: Seq;
74
+ /**
75
+ * The `Ack` for a server that has applied nothing yet — `ack(NO_ACK)`. The `Ack`
76
+ * counterpart to the {@link NO_ACK} `Seq` sentinel, for a consumer that carries
77
+ * an `Ack` rather than a bare `lastAppliedSeq`.
78
+ */
79
+ declare const initAck: () => Ack;
59
80
  /** The result of splitting a seq-tagged buffer against an ack. */
60
81
  interface AckPartition<T> {
61
82
  /** Inputs the authoritative side has applied (`seq <= lastAppliedSeq`). */
@@ -106,4 +127,4 @@ declare function reconcile<S, M extends {
106
127
  type: string;
107
128
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, authoritativeState: S, lastAppliedSeq: Ack | Seq, pending: readonly SeqTagged<M>[]): S;
108
129
 
109
- export { type Ack, type AckPartition, type Seq, type SeqTagged, ack, nextSeq, partitionByAck, reconcile, tagSeq };
130
+ export { type Ack, type AckPartition, NO_ACK, type Seq, type SeqTagged, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq };
@@ -1,4 +1,4 @@
1
- export { ack, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-RVU3RBE6.js';
2
- import '../chunk-BNJYE4AU.js';
1
+ export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-G2U7DJHL.js';
2
+ import '../chunk-DYRKDCJI.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,2 +1,2 @@
1
- export { Ack, AckPartition, Seq, SeqTagged, ack, nextSeq, partitionByAck, reconcile, tagSeq } from '../prediction/index.js';
2
- export { C as Cmd, b as ContextFree, I as Interpret, M as Machine, N as NoCtx, P as Port, c as PortEmitter, R as Reducer, S as Sub, a as SubId, d as Subscribe, e as SyncReturn, T as Transitions, U as UpdateForm, f as detectUpdateForm, g as foldMsgs, h as formOf, s as subId } from '../core-CymXZCeY.js';
1
+ export { Ack, AckPartition, NO_ACK, Seq, SeqTagged, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../prediction/index.js';
2
+ export { C as Cmd, b as ContextFree, I as Interpret, M as Machine, j as NoCellError, N as NoCtx, P as Port, c as PortEmitter, R as Reducer, S as Sub, a as SubId, d as Subscribe, e as SyncReturn, T as Transitions, U as UpdateForm, g as detectUpdateForm, h as foldMsgs, i as formOf, s as subId } from '../core-BmlJOvxM.js';
@@ -1,4 +1,4 @@
1
- export { ack, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-RVU3RBE6.js';
2
- export { Cmd, detectUpdateForm, foldMsgs, formOf, subId } from '../chunk-BNJYE4AU.js';
1
+ export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-G2U7DJHL.js';
2
+ export { Cmd, NoCellError, detectUpdateForm, foldMsgs, formOf, subId } from '../chunk-DYRKDCJI.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
1
  import { Store, Runtime } from '../index.js';
2
- import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
2
+ import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
3
3
 
4
4
  /**
5
5
  * @demlik/tea/react — React host adapter for `@demlik/tea`.
@@ -1,6 +1,6 @@
1
- import { run } from '../chunk-2WZHS4P2.js';
2
- import '../chunk-BNJYE4AU.js';
3
- import { useMemo, useRef, useSyncExternalStore, useEffect } from 'react';
1
+ import { run } from '../chunk-6VDCTR7N.js';
2
+ import '../chunk-DYRKDCJI.js';
3
+ import { useMemo, useRef, useReducer, useSyncExternalStore, useEffect } from 'react';
4
4
 
5
5
  function useMachine(machine, opts) {
6
6
  const ctx = opts.ctx;
@@ -10,6 +10,7 @@ function useMachine(machine, opts) {
10
10
  [machine, ctx, store]
11
11
  );
12
12
  const readyRef = useRef(null);
13
+ const [, markBooted] = useReducer((tick) => tick + 1, 0);
13
14
  const preliminaryState = useMemo(
14
15
  () => machine.init(null, ctx)[0],
15
16
  [machine, ctx]
@@ -24,7 +25,10 @@ function useMachine(machine, opts) {
24
25
  let live = true;
25
26
  booting.ready.then(
26
27
  (r) => {
27
- if (live) readyRef.current = r;
28
+ if (live) {
29
+ readyRef.current = r;
30
+ markBooted();
31
+ }
28
32
  },
29
33
  () => {
30
34
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.ts"],"names":[],"mappings":";;;;AA+DO,SAAS,UAAA,CAOd,SACA,IAAA,EACgC;AAMhC,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,QAAQ,IAAA,CAAK,KAAA;AAMnB,EAAA,MAAM,OAAA,GAAU,OAAA;AAAA,IACd,MAAM,GAAA,CAAI,OAAA,EAAS,EAAE,GAAA,EAAK,OAAO,CAAA;AAAA,IACjC,CAAC,OAAA,EAAS,GAAA,EAAK,KAAK;AAAA,GACtB;AAQA,EAAA,MAAM,QAAA,GAAW,OAA6B,IAAI,CAAA;AAalD,EAAA,MAAM,gBAAA,GAAmB,OAAA;AAAA,IACvB,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAA,EAAM,GAAG,EAAE,CAAC,CAAA;AAAA,IAC/B,CAAC,SAAS,GAAG;AAAA,GACf;AAMA,EAAA,MAAM,WAAA,GAAc,MAClB,QAAA,CAAS,OAAA,KAAY,OAAO,QAAA,CAAS,OAAA,CAAQ,UAAS,GAAI,gBAAA;AAE5D,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,OAAA,CAAQ,SAAA;AAAA,IACR,WAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,IAAA,GAAO,IAAA;AAOX,IAAA,OAAA,CAAQ,KAAA,CAAM,IAAA;AAAA,MACZ,CAAC,CAAA,KAAM;AACL,QAAA,IAAI,IAAA,WAAe,OAAA,GAAU,CAAA;AAAA,MAC/B,CAAA;AAAA,MACA,MAAM;AAAA,MAAC;AAAA,KACT;AACA,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,GAAO,KAAA;AACP,MAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AAInB,MAAA,OAAA,CAAQ,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IAC/B,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,CAAC,KAAA,EAAO,OAAA,CAAQ,QAAQ,CAAA;AACjC;AAeO,SAAS,WACd,OAAA,EACgC;AAChC,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,OAAA,CAAQ,SAAA;AAAA,IACR,OAAA,CAAQ,QAAA;AAAA,IACR,OAAA,CAAQ;AAAA,GACV;AACA,EAAA,OAAO,CAAC,KAAA,EAAO,OAAA,CAAQ,QAAQ,CAAA;AACjC","file":"index.js","sourcesContent":["/**\n * @demlik/tea/react — React host adapter for `@demlik/tea`.\n *\n * The thinnest possible adapter that makes a tea machine consumable from a\n * React component. Three rules (PRD §\"React adapter\"):\n *\n * 1. The runtime is the source of truth, NOT React state. We subscribe via\n * `useSyncExternalStore(runtime.subscribe, runtime.getState,\n * runtime.getState)`. React renders are driven by runtime state changes;\n * React never owns the state.\n * 2. The runtime is memoized per component-mount, not per render. The deps\n * array is `[machine, opts.ctx, opts.store]` — IDENTITY, not value. See\n * `README.md` for the ctx-identity footgun + fix.\n * 3. Concurrent React is the design target. `useSyncExternalStore` is the\n * React-blessed primitive for tearing-free external state.\n *\n * Client-only in v1 — components that call `useMachine` MUST be `\"use client\"`.\n * Server Components consume data via Relay/RSC as today; tea machines own\n * client-side stateful flows. No RSC streaming, no server-side state\n * hydration. Crossing this boundary is a v2 concern.\n */\n\n\"use client\";\n\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport {\n type BootingRuntime,\n type Cmd,\n type Machine,\n type Runtime,\n run,\n type Store,\n type Sub,\n} from \"../index\";\n\n/**\n * Options passed to `useMachine`. The shape is intentionally minimal — `ctx`\n * is required (every machine has one), `store` is optional (omit it for\n * volatile-state machines).\n *\n * **Identity matters.** The runtime is memoized on `[machine, opts.ctx,\n * opts.store]`. A new `ctx` reference rebuilds the runtime. Always `useMemo`\n * (or otherwise stabilize) the ctx at the call site. See README.\n */\nexport interface UseMachineOpts<S, Ctx> {\n ctx: Ctx;\n store?: Store<S>;\n}\n\n/**\n * Build and own a `Runtime<S, M>` for the lifetime of the component mount.\n *\n * - Builds via `useMemo(() => run(machine, opts), [machine, opts.ctx,\n * opts.store])` — runtime is recreated when ANY of the three identities\n * change.\n * - Subscribes via `useSyncExternalStore(runtime.subscribe, runtime.getState,\n * runtime.getState)` — tearing-free under React 18 concurrent rendering.\n * - On unmount (or any dep change), `useEffect` cleanup calls\n * `runtime.stop()` — drains the queue, runs every active sub cleanup,\n * flushes state.\n *\n * Returns `[state, dispatch]` shaped like `useReducer`.\n */\nexport function useMachine<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n opts: UseMachineOpts<S, Ctx>,\n): [S, (msg: M) => Promise<void>] {\n // Deps are literal: machine identity, ctx identity, store identity. NOT\n // `[opts]` (would rebuild every render — callers pass fresh objects).\n // We pass explicit fields to `run()` so the closure captures only the\n // deps in the array — keeps biome's `useExhaustiveDependencies` happy\n // without lying about what the memo actually depends on.\n const ctx = opts.ctx;\n const store = opts.store;\n // `run()` returns a `BootingRuntime<S, M>` SYNCHRONOUSLY (issue #45) — exactly\n // what `useMemo` needs. We never `await` here: awaiting would force an async\n // memo, a null first-commit state, and a resolved-flag dance. Instead we hold\n // the booting handle and capture the booted `Runtime` (the only thing with a\n // total `getState`) once `ready` resolves.\n const booting = useMemo<BootingRuntime<S, M>>(\n () => run(machine, { ctx, store }),\n [machine, ctx, store],\n );\n\n // Captures the booted `Runtime` once `ready` resolves. Until then it is\n // `null` and `getSnapshot` serves `preliminaryState`. A ref (not state)\n // because the booted runtime arrives via a subscription notification, not a\n // render: `subscribe` fires the boot fanout, React re-reads `getSnapshot`,\n // and by then `ready` has populated the ref. Reset whenever the booting\n // handle identity changes (a new machine/ctx/store mount).\n const readyRef = useRef<Runtime<S, M> | null>(null);\n\n // Preliminary state computed sync from `machine.init(null, ctx)`. This is\n // the snapshot React sees BEFORE the runtime finishes booting (matters\n // only when `opts.store` is present — without a store, the substrate\n // already sets state synchronously inside `run()`, so the boot fanout that\n // captures `readyRef` lands on the same microtask as the first commit).\n //\n // We deliberately recompute on each render but cache by `[machine,\n // opts.ctx]` — `init` is pure by convention, so cost is negligible. The\n // cached snapshot is also identity-stable across rerenders when the deps\n // don't change, which `useSyncExternalStore` rewards (no spurious\n // re-renders).\n const preliminaryState = useMemo<S>(\n () => machine.init(null, ctx)[0],\n [machine, ctx],\n );\n\n // `getSnapshot` reads the booted runtime if `ready` has resolved (the ref is\n // populated), else falls back to the preliminary state. No try/catch dance\n // anymore — pre-boot there is simply no booted `Runtime` to read from, so\n // the fallback is structural, not exception-driven.\n const getSnapshot = (): S =>\n readyRef.current !== null ? readyRef.current.getState() : preliminaryState;\n\n const state = useSyncExternalStore(\n booting.subscribe,\n getSnapshot,\n getSnapshot,\n );\n\n useEffect(() => {\n let live = true;\n // Capture the booted runtime when `ready` resolves. The boot fanout\n // (`fireListeners`) runs as part of boot, so the `useSyncExternalStore`\n // subscriber is notified right after this ref is set — React re-reads\n // `getSnapshot` and swaps preliminary → real state with no extra render\n // plumbing. Boot rejections are swallowed: a failed boot surfaces on every\n // `dispatch`, which is where the consumer handles it.\n booting.ready.then(\n (r) => {\n if (live) readyRef.current = r;\n },\n () => {},\n );\n return () => {\n live = false;\n readyRef.current = null;\n // Fire-and-forget — `stop()` is async but `useEffect` cleanup is sync.\n // The runtime swallows in-flight rejections internally, so this never\n // rejects in practice; we still attach `.catch` for safety.\n booting.stop().catch(() => {});\n };\n }, [booting]);\n\n return [state, booting.dispatch];\n}\n\n/**\n * Lower-level escape hatch: consume an externally-built `Runtime<S, M>`.\n *\n * Use this when the runtime is owned by something OTHER than the component —\n * a parent component, a test harness, a singleton. **The component does NOT\n * call `runtime.stop()` on unmount** — the caller owns the lifecycle.\n *\n * Primary use case in this repo: component tests. Build a runtime with\n * `@demlik/tea/mem` and pre-seed it via `replay`, then mount the component with\n * `useRuntime(testRuntime)` to render in any phase.\n *\n * Returns `[state, dispatch]` — same shape as `useMachine`.\n */\nexport function useRuntime<S, M extends { type: string }>(\n runtime: Runtime<S, M>,\n): [S, (msg: M) => Promise<void>] {\n const state = useSyncExternalStore(\n runtime.subscribe,\n runtime.getState,\n runtime.getState,\n );\n return [state, runtime.dispatch];\n}\n"]}
1
+ {"version":3,"sources":["../../src/react/index.ts"],"names":[],"mappings":";;;;AAqEO,SAAS,UAAA,CAOd,SACA,IAAA,EACgC;AAMhC,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,QAAQ,IAAA,CAAK,KAAA;AAMnB,EAAA,MAAM,OAAA,GAAU,OAAA;AAAA,IACd,MAAM,GAAA,CAAI,OAAA,EAAS,EAAE,GAAA,EAAK,OAAO,CAAA;AAAA,IACjC,CAAC,OAAA,EAAS,GAAA,EAAK,KAAK;AAAA,GACtB;AAeA,EAAA,MAAM,QAAA,GAAW,OAA6B,IAAI,CAAA;AAClD,EAAA,MAAM,GAAG,UAAU,CAAA,GAAI,WAAW,CAAC,IAAA,KAAiB,IAAA,GAAO,CAAA,EAAG,CAAC,CAAA;AAa/D,EAAA,MAAM,gBAAA,GAAmB,OAAA;AAAA,IACvB,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAA,EAAM,GAAG,EAAE,CAAC,CAAA;AAAA,IAC/B,CAAC,SAAS,GAAG;AAAA,GACf;AAMA,EAAA,MAAM,WAAA,GAAc,MAClB,QAAA,CAAS,OAAA,KAAY,OAAO,QAAA,CAAS,OAAA,CAAQ,UAAS,GAAI,gBAAA;AAE5D,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,OAAA,CAAQ,SAAA;AAAA,IACR,WAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,IAAA,GAAO,IAAA;AAMX,IAAA,OAAA,CAAQ,KAAA,CAAM,IAAA;AAAA,MACZ,CAAC,CAAA,KAAM;AACL,QAAA,IAAI,IAAA,EAAM;AACR,UAAA,QAAA,CAAS,OAAA,GAAU,CAAA;AACnB,UAAA,UAAA,EAAW;AAAA,QACb;AAAA,MACF,CAAA;AAAA,MACA,MAAM;AAAA,MAAC;AAAA,KACT;AACA,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,GAAO,KAAA;AACP,MAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AAInB,MAAA,OAAA,CAAQ,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IAC/B,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,CAAC,KAAA,EAAO,OAAA,CAAQ,QAAQ,CAAA;AACjC;AAeO,SAAS,WACd,OAAA,EACgC;AAChC,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,OAAA,CAAQ,SAAA;AAAA,IACR,OAAA,CAAQ,QAAA;AAAA,IACR,OAAA,CAAQ;AAAA,GACV;AACA,EAAA,OAAO,CAAC,KAAA,EAAO,OAAA,CAAQ,QAAQ,CAAA;AACjC","file":"index.js","sourcesContent":["/**\n * @demlik/tea/react — React host adapter for `@demlik/tea`.\n *\n * The thinnest possible adapter that makes a tea machine consumable from a\n * React component. Three rules (PRD §\"React adapter\"):\n *\n * 1. The runtime is the source of truth, NOT React state. We subscribe via\n * `useSyncExternalStore(runtime.subscribe, runtime.getState,\n * runtime.getState)`. React renders are driven by runtime state changes;\n * React never owns the state.\n * 2. The runtime is memoized per component-mount, not per render. The deps\n * array is `[machine, opts.ctx, opts.store]` — IDENTITY, not value. See\n * `README.md` for the ctx-identity footgun + fix.\n * 3. Concurrent React is the design target. `useSyncExternalStore` is the\n * React-blessed primitive for tearing-free external state.\n *\n * Client-only in v1 — components that call `useMachine` MUST be `\"use client\"`.\n * Server Components consume data via Relay/RSC as today; tea machines own\n * client-side stateful flows. No RSC streaming, no server-side state\n * hydration. Crossing this boundary is a v2 concern.\n */\n\n\"use client\";\n\nimport {\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport {\n type BootingRuntime,\n type Cmd,\n type Machine,\n type Runtime,\n run,\n type Store,\n type Sub,\n} from \"../index\";\n\n/**\n * Options passed to `useMachine`. The shape is intentionally minimal — `ctx`\n * is required (every machine has one), `store` is optional (omit it for\n * volatile-state machines).\n *\n * **Identity matters.** The runtime is memoized on `[machine, opts.ctx,\n * opts.store]`. A new `ctx` reference rebuilds the runtime. Always `useMemo`\n * (or otherwise stabilize) the ctx at the call site. See README.\n */\nexport interface UseMachineOpts<S, Ctx> {\n ctx: Ctx;\n store?: Store<S>;\n}\n\n/**\n * Build and own a `Runtime<S, M>` for the lifetime of the component mount.\n *\n * - Builds via `useMemo(() => run(machine, opts), [machine, opts.ctx,\n * opts.store])` — runtime is recreated when ANY of the three identities\n * change.\n * - Subscribes via `useSyncExternalStore(runtime.subscribe, runtime.getState,\n * runtime.getState)` — tearing-free under React 18 concurrent rendering.\n * - On unmount (or any dep change), `useEffect` cleanup calls\n * `runtime.stop()` — drains the queue, runs every active sub cleanup,\n * flushes state.\n *\n * Returns `[state, dispatch]` shaped like `useReducer`.\n */\nexport function useMachine<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n opts: UseMachineOpts<S, Ctx>,\n): [S, (msg: M) => Promise<void>] {\n // Deps are literal: machine identity, ctx identity, store identity. NOT\n // `[opts]` (would rebuild every render — callers pass fresh objects).\n // We pass explicit fields to `run()` so the closure captures only the\n // deps in the array — keeps biome's `useExhaustiveDependencies` happy\n // without lying about what the memo actually depends on.\n const ctx = opts.ctx;\n const store = opts.store;\n // `run()` returns a `BootingRuntime<S, M>` SYNCHRONOUSLY (issue #45) — exactly\n // what `useMemo` needs. We never `await` here: awaiting would force an async\n // memo, a null first-commit state, and a resolved-flag dance. Instead we hold\n // the booting handle and capture the booted `Runtime` (the only thing with a\n // total `getState`) once `ready` resolves.\n const booting = useMemo<BootingRuntime<S, M>>(\n () => run(machine, { ctx, store }),\n [machine, ctx, store],\n );\n\n // Captures the booted `Runtime` once `ready` resolves. Until then it is\n // `null` and `getSnapshot` serves `preliminaryState`. Reset whenever the\n // booting handle identity changes (a new machine/ctx/store mount).\n //\n // The boot race the paired `bootedTick` bump exists for: the substrate's\n // boot fanout fires BEFORE `ready` resolves (fireListeners runs inside the\n // boot step; `ready` chains off the boot promise), so at notification time\n // this ref is still null and `getSnapshot` returns the same\n // `preliminaryState` reference — React sees an unchanged snapshot and\n // skips the re-render. Nothing re-notifies after the ref is set, so a\n // store-backed mount would stay stuck on the preliminary state until the\n // next unrelated transition. Bumping a reducer when we capture the booted\n // runtime forces the render that swaps preliminary → real state.\n const readyRef = useRef<Runtime<S, M> | null>(null);\n const [, markBooted] = useReducer((tick: number) => tick + 1, 0);\n\n // Preliminary state computed sync from `machine.init(null, ctx)`. This is\n // the snapshot React sees BEFORE the runtime finishes booting (matters\n // only when `opts.store` is present — without a store, the substrate\n // already sets state synchronously inside `run()`, so the boot fanout that\n // captures `readyRef` lands on the same microtask as the first commit).\n //\n // We deliberately recompute on each render but cache by `[machine,\n // opts.ctx]` — `init` is pure by convention, so cost is negligible. The\n // cached snapshot is also identity-stable across rerenders when the deps\n // don't change, which `useSyncExternalStore` rewards (no spurious\n // re-renders).\n const preliminaryState = useMemo<S>(\n () => machine.init(null, ctx)[0],\n [machine, ctx],\n );\n\n // `getSnapshot` reads the booted runtime if `ready` has resolved (the ref is\n // populated), else falls back to the preliminary state. No try/catch dance\n // anymore — pre-boot there is simply no booted `Runtime` to read from, so\n // the fallback is structural, not exception-driven.\n const getSnapshot = (): S =>\n readyRef.current !== null ? readyRef.current.getState() : preliminaryState;\n\n const state = useSyncExternalStore(\n booting.subscribe,\n getSnapshot,\n getSnapshot,\n );\n\n useEffect(() => {\n let live = true;\n // Capture the booted runtime when `ready` resolves, then force the\n // render that swaps preliminary → real state (see the boot-race note on\n // `readyRef` — the boot fanout fires too early to do it for us). Boot\n // rejections are swallowed: a failed boot surfaces on every `dispatch`,\n // which is where the consumer handles it.\n booting.ready.then(\n (r) => {\n if (live) {\n readyRef.current = r;\n markBooted();\n }\n },\n () => {},\n );\n return () => {\n live = false;\n readyRef.current = null;\n // Fire-and-forget — `stop()` is async but `useEffect` cleanup is sync.\n // The runtime swallows in-flight rejections internally, so this never\n // rejects in practice; we still attach `.catch` for safety.\n booting.stop().catch(() => {});\n };\n }, [booting]);\n\n return [state, booting.dispatch];\n}\n\n/**\n * Lower-level escape hatch: consume an externally-built `Runtime<S, M>`.\n *\n * Use this when the runtime is owned by something OTHER than the component —\n * a parent component, a test harness, a singleton. **The component does NOT\n * call `runtime.stop()` on unmount** — the caller owns the lifecycle.\n *\n * Primary use case in this repo: component tests. Build a runtime with\n * `@demlik/tea/mem` and pre-seed it via `replay`, then mount the component with\n * `useRuntime(testRuntime)` to render in any phase.\n *\n * Returns `[state, dispatch]` — same shape as `useMachine`.\n */\nexport function useRuntime<S, M extends { type: string }>(\n runtime: Runtime<S, M>,\n): [S, (msg: M) => Promise<void>] {\n const state = useSyncExternalStore(\n runtime.subscribe,\n runtime.getState,\n runtime.getState,\n );\n return [state, runtime.dispatch];\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { CircuitConfig, RateLimitConfig, DeadlineConfig, ResilientPorts, RunCmd, FailMsg, SucceedMsg } from '../resilient-call/index.js';
2
2
  import { TtlCache } from '../cache/index.js';
3
- import { C as Cmd } from '../core-CymXZCeY.js';
3
+ import { C as Cmd } from '../core-BmlJOvxM.js';
4
4
  import { PaginatedWalkState, FetchPageCmd, PageOkMsg, PageErrMsg, PaginatedWalkTimerMsg } from '../paginated-walk/index.js';
5
5
  import { RetryPolicy } from '../retry-backoff/index.js';
6
6
  import { DeadlineSub } from '../deadline/index.js';
@@ -8,7 +8,7 @@ export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
8
8
  import '../circuit-breaker/index.js';
9
9
  import '../protocol-B5YmJr0d.js';
10
10
  import '../rate-limit/index.js';
11
- import '../types-N9LZbdnr.js';
11
+ import '../types-BoUWgwSm.js';
12
12
  import '../paginator/index.js';
13
13
 
14
14
  /**
@@ -1,16 +1,16 @@
1
- import { createPaginatedWalk, PAGE_KEY } from '../chunk-IFBQJYGI.js';
1
+ import { createPaginatedWalk, PAGE_KEY } from '../chunk-FOGL3TC7.js';
2
2
  import '../chunk-SINXE47Y.js';
3
- import '../chunk-RVJFGRGK.js';
3
+ import '../chunk-6GIGQT6C.js';
4
4
  import '../chunk-JELSZN25.js';
5
- export { deadlineSub, subscribeDeadline } from '../chunk-QGZW4W3E.js';
5
+ export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
6
6
  import '../chunk-5VNWZADP.js';
7
7
  import '../chunk-2YOE5PIS.js';
8
8
  import '../chunk-SC3FAFZN.js';
9
9
  import { initCache, get, set } from '../chunk-IN33Q4VJ.js';
10
10
  import '../chunk-NSDFA6M6.js';
11
11
  import '../chunk-2YFLOCWX.js';
12
- import '../chunk-2WZHS4P2.js';
13
- import '../chunk-BNJYE4AU.js';
12
+ import '../chunk-6VDCTR7N.js';
13
+ import '../chunk-DYRKDCJI.js';
14
14
 
15
15
  // src/reconciler/index.ts
16
16
  function createReconciler(config, rng = Math.random) {
@@ -1,5 +1,5 @@
1
1
  import { BootingRuntime } from '../index.js';
2
- import '../core-CymXZCeY.js';
2
+ import '../core-BmlJOvxM.js';
3
3
 
4
4
  /**
5
5
  * A recorded run, sufficient to reproduce it via `../trace-replay`.
@@ -2,12 +2,48 @@ import { TtlCache } from '../cache/index.js';
2
2
  import { CircuitState } from '../circuit-breaker/index.js';
3
3
  import { DeadlineExceeded, DeadlineSub } from '../deadline/index.js';
4
4
  export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
5
- import { C as Cmd } from '../core-CymXZCeY.js';
5
+ import { C as Cmd } from '../core-BmlJOvxM.js';
6
6
  import { M as MsgType } from '../protocol-B5YmJr0d.js';
7
7
  import { TokenBucket } from '../rate-limit/index.js';
8
8
  import { RetryState, RetryPolicy } from '../retry-backoff/index.js';
9
- import '../types-N9LZbdnr.js';
9
+ import '../types-BoUWgwSm.js';
10
10
 
11
+ /**
12
+ * @deprecated `@demlik/tea/resilient-call` is deprecated in favor of
13
+ * `@demlik/tea/with-resilience` — the higher-order `withResilience(base,
14
+ * config)` wrapper (TEA-idiomatic: it wraps any existing machine instead of
15
+ * forcing a bespoke embedded slice; epic consolidation verdict, survivor
16
+ * `with-resilience`). The APIs are NOT drop-in; migration map:
17
+ *
18
+ * - `createResilientCall(config, rng)` + hand-wiring `init` / the verbs /
19
+ * `subs` / `handlers` into your machine → `withResilience(base, { target,
20
+ * ...bricks }, rng)`. Nothing is wired by hand anymore: the wrapper
21
+ * intercepts the `target` base Cmd and routes it through the gate.
22
+ * - `ResilientConfig` bricks (`retry` / `circuit` / `rateLimit` / `cache` /
23
+ * `deadline`) → unchanged; `ResilienceConfig` extends the same knob and
24
+ * adds `target` (required), `keyOf?` (per-call key — was `attempt`'s `key`
25
+ * argument), and `at?` (time-as-data reader — REQUIRED with any
26
+ * time-sensitive brick; `withResilience` throws at construction without it).
27
+ * - The slice at your chosen Model field + `liftResilience` → the wrapper
28
+ * owns `model.$resilience`; there is nothing to lift.
29
+ * - Verbs `attempt` / `succeed` / `fail` / `onTimer` → internal to the
30
+ * wrapper; results route as `$resilience:ok` / `$resilience:err` /
31
+ * `$resilience:timer` Msgs through the merged `update` (the old
32
+ * `resilient_run` / `resilient_ok` / `resilient_err` vocabulary is renamed
33
+ * into the `$resilience:` namespace).
34
+ * - `settleFailed` → NO equivalent. It exists for embedded-slice consumers
35
+ * (`authed-call`, `paginated-walk`) that settle a call without touching the
36
+ * breaker; the wrapper exposes no caller-facing settle verb.
37
+ * - `handlers(ports)` / `subs(state)` / the `subscribeDeadline` re-export →
38
+ * handled by the wrapper; run the returned machine like any other.
39
+ *
40
+ * Removal window ("deprecate, don't delete"): the `./resilient-call` subpath
41
+ * export survives one minor release after this deprecation ships, then is
42
+ * removed. Internal sibling modules (`with-resilience` itself, `agent`,
43
+ * `llm-call`, `authed-call`, `reconciler`, `paginated-walk`) intentionally keep
44
+ * importing `../resilient-call` — the deprecation targets the PUBLISHED export,
45
+ * not the module's internal role.
46
+ */
11
47
  /**
12
48
  * @demlik/tea/resilient-call — the ROOT composition: a single resilience knob
13
49
  * that bundles cache → circuit-breaker → rate-limit → exponential-backoff retry
@@ -225,6 +261,9 @@ interface ResilientPorts<I, R> {
225
261
  *
226
262
  * Returns the uniform L2 knob contract. `I` is the port input type, `R` the
227
263
  * port result type.
264
+ *
265
+ * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — not
266
+ * drop-in; see the migration map at the top of this module.
228
267
  */
229
268
  declare function createResilientCall<I, R>(config: ResilientConfig, rng?: () => number): {
230
269
  init: () => ResilientState<I, R>;
@@ -243,6 +282,10 @@ declare function createResilientCall<I, R>(config: ResilientConfig, rng?: () =>
243
282
  * slice lives at `state.resilience`. A convenience for the common single-slice
244
283
  * host; consumers with a differently-named field spread by hand. Pure — a thin
245
284
  * record rebuild, no clock / RNG.
285
+ *
286
+ * @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — the
287
+ * wrapper owns `model.$resilience`, so there is nothing to lift; see the
288
+ * migration map at the top of this module.
246
289
  */
247
290
  declare function liftResilience<S extends {
248
291
  resilience: ResilientState<I, R>;
@@ -1,13 +1,13 @@
1
- export { createResilientCall, liftResilience } from '../chunk-RVJFGRGK.js';
1
+ export { createResilientCall, liftResilience } from '../chunk-6GIGQT6C.js';
2
2
  import '../chunk-JELSZN25.js';
3
- export { deadlineSub, subscribeDeadline } from '../chunk-QGZW4W3E.js';
3
+ export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
4
4
  import '../chunk-5VNWZADP.js';
5
5
  import '../chunk-2YOE5PIS.js';
6
6
  import '../chunk-SC3FAFZN.js';
7
7
  import '../chunk-IN33Q4VJ.js';
8
8
  import '../chunk-NSDFA6M6.js';
9
9
  import '../chunk-2YFLOCWX.js';
10
- import '../chunk-2WZHS4P2.js';
11
- import '../chunk-BNJYE4AU.js';
10
+ import '../chunk-6VDCTR7N.js';
11
+ import '../chunk-DYRKDCJI.js';
12
12
  //# sourceMappingURL=index.js.map
13
13
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,109 @@
1
+ import { Rng, RetryPolicy } from '../retry-backoff/index.js';
2
+
3
+ /**
4
+ * @demlik/tea/retry-to-success — run a fallible async port through BOUNDED
5
+ * retry + exponential backoff to success-or-terminal-give-up, and hand the
6
+ * caller a `Promise<R>` that resolves on the first success or REJECTS with a
7
+ * checkable {@link RetryExhaustedError} the moment the retry bound is hit.
8
+ *
9
+ * This is the retry-side parallel to `./await-terminal`. `runToTerminal` gives a
10
+ * caller with a one-shot decision *machine* a fire-and-await `Promise<State>`
11
+ * without wiring `observe`; this gives a caller with a one-shot fallible *port*
12
+ * a fire-and-await `Promise<R>` without either hand-folding the
13
+ * `./retry-backoff` primitives (`recordFailure` / `shouldRetry` / `nextDelayMs`)
14
+ * into their own reducer each time, or standing up the full `./resilient-call`
15
+ * verb contract (`init` + `attempt`/`succeed`/`fail`/`onTimer` + `subs` +
16
+ * `handlers`) when they want retry only and none of cache / circuit / rate-limit
17
+ * / deadline.
18
+ *
19
+ * It adds NO new runtime capability and NO new backoff math — the whole retry
20
+ * decision is `./retry-backoff` exactly as `resilient-call`'s `backoff` verb
21
+ * wires it:
22
+ * - `initRetry()` seeds the per-call attempt counter.
23
+ * - `recordFailure(retry, error)` folds each thrown failure (attempt++).
24
+ * - `shouldRetry(retry, policy)` is the terminal-at-`maxAttempts` bound — the
25
+ * `maxAttempts`-th recorded failure refuses another attempt (attempts
26
+ * `0 .. maxAttempts-1` run; the port is invoked at most `maxAttempts` times).
27
+ * - `nextDelayMs(retry, policy, rng)` is the exp+jitter+cap delay before the
28
+ * next attempt, computed on the incremented state — same call order as
29
+ * `resilient-call`.
30
+ *
31
+ * Where the clock / RNG live: injected, never read on the module's own behalf,
32
+ * mirroring `retry-backoff`'s purity discipline. `rng` is an {@link Rng} (branded
33
+ * `[0, 1)` via `asRng` at the caller boundary) threaded straight into
34
+ * `nextDelayMs`; the wait itself is a `sleep(ms)` port defaulting to a real
35
+ * `setTimeout`, injectable so a test can drive the backoff schedule
36
+ * deterministically without touching wall-clock timers.
37
+ *
38
+ * Exhaustion is NEVER a silent swallow: at the bound the returned promise
39
+ * rejects with {@link RetryExhaustedError} (carrying the attempt count and the
40
+ * final failure), the retry-side dialect of `await-terminal`'s
41
+ * `TerminalTimeoutError` — a distinct `instanceof`- and `_tag`-checkable
42
+ * rejection.
43
+ *
44
+ * NOT a substrate primitive: it depends on nothing from `../index`, only on the
45
+ * leaf `../retry-backoff` utility. Callers reach it via the
46
+ * `@demlik/tea/retry-to-success` subpath.
47
+ */
48
+
49
+ /**
50
+ * Raised when `retryToSuccess` exhausts the retry bound without a success — the
51
+ * `maxAttempts`-th recorded failure refuses another attempt. Follows the tea
52
+ * error dialect (`override readonly name`) and carries a `_tag` discriminant, so
53
+ * a caller can branch on it by `instanceof` or on the tag. It carries the number
54
+ * of attempts made and the final failure (`lastError`) so the terminal give-up
55
+ * is a distinct, checkable rejection — never a silent resolve, and never a
56
+ * swallowed error.
57
+ */
58
+ declare class RetryExhaustedError extends Error {
59
+ /** How many times the port was invoked before giving up (= `policy.maxAttempts`). */
60
+ readonly attempts: number;
61
+ /** The failure the final attempt threw — carried, never interpreted. */
62
+ readonly lastError: unknown;
63
+ readonly name = "RetryExhaustedError";
64
+ readonly _tag: "RetryExhaustedError";
65
+ constructor(
66
+ /** How many times the port was invoked before giving up (= `policy.maxAttempts`). */
67
+ attempts: number,
68
+ /** The failure the final attempt threw — carried, never interpreted. */
69
+ lastError: unknown);
70
+ }
71
+ /** Options for {@link retryToSuccess}. */
72
+ interface RetryToSuccessOptions {
73
+ /**
74
+ * The `[0, 1)` jitter source threaded into `nextDelayMs`, branded via
75
+ * `asRng` at construction (a raw `() => number` is rejected by the type).
76
+ * Injected for deterministic backoff in tests; defaults to `defaultRng`
77
+ * (`Math.random`), read only at the delay boundary — never inside the loop's
78
+ * decision.
79
+ */
80
+ readonly rng?: Rng;
81
+ /**
82
+ * The wait between attempts, given the backoff delay in ms. Injectable so a
83
+ * test can drive the retry schedule without wall-clock timers (and assert the
84
+ * exact delays); defaults to a real `setTimeout`. The delay value is always
85
+ * `./retry-backoff`'s `nextDelayMs` — this port only realizes the wait.
86
+ */
87
+ readonly sleep?: (ms: number) => Promise<void>;
88
+ }
89
+ /**
90
+ * Fire-and-await bounded retry: invoke the fallible `port`, and on each thrown
91
+ * failure fold it through `./retry-backoff` — record it, and if the policy
92
+ * still permits an attempt, back off `nextDelayMs` and retry; otherwise REJECT
93
+ * with {@link RetryExhaustedError}. Resolves with the port's value on the first
94
+ * success.
95
+ *
96
+ * The port is invoked at most `policy.maxAttempts` times (attempts
97
+ * `0 .. maxAttempts-1`); the `maxAttempts`-th failure is terminal. Every failure
98
+ * is either retried or surfaced on the rejection's `lastError` — none is
99
+ * swallowed.
100
+ *
101
+ * @param port the fallible work — throws on failure, resolves with `R` on success.
102
+ * @param policy the bounded-retry curve (`baseMs` / `factor` / `capMs` /
103
+ * `maxAttempts` / `jitter`) from `./retry-backoff`.
104
+ * @param opts optional injected `rng` (jitter) and `sleep` (the wait) — see
105
+ * {@link RetryToSuccessOptions}.
106
+ */
107
+ declare function retryToSuccess<R>(port: () => Promise<R>, policy: RetryPolicy, opts?: RetryToSuccessOptions): Promise<R>;
108
+
109
+ export { RetryExhaustedError, type RetryToSuccessOptions, retryToSuccess };
@@ -0,0 +1,35 @@
1
+ import { defaultRng, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-5VNWZADP.js';
2
+
3
+ // src/retry-to-success/index.ts
4
+ var RetryExhaustedError = class extends Error {
5
+ constructor(attempts, lastError) {
6
+ super(
7
+ `@demlik/tea: retryToSuccess gave up after ${attempts} attempt(s) \u2014 the bounded retry reached maxAttempts without a success. See .lastError for the final failure.`
8
+ );
9
+ this.attempts = attempts;
10
+ this.lastError = lastError;
11
+ }
12
+ name = "RetryExhaustedError";
13
+ _tag = "RetryExhaustedError";
14
+ };
15
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ async function retryToSuccess(port, policy, opts = {}) {
17
+ const rng = opts.rng ?? defaultRng;
18
+ const sleep = opts.sleep ?? defaultSleep;
19
+ let retry = initRetry();
20
+ for (; ; ) {
21
+ try {
22
+ return await port();
23
+ } catch (error) {
24
+ retry = recordFailure(retry, error);
25
+ if (!shouldRetry(retry, policy)) {
26
+ throw new RetryExhaustedError(retry.attempt, error);
27
+ }
28
+ await sleep(nextDelayMs(retry, policy, rng));
29
+ }
30
+ }
31
+ }
32
+
33
+ export { RetryExhaustedError, retryToSuccess };
34
+ //# sourceMappingURL=index.js.map
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/retry-to-success/index.ts"],"names":[],"mappings":";;;AAiEO,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EAG7C,WAAA,CAEkB,UAEA,SAAA,EAChB;AACA,IAAA,KAAA;AAAA,MACE,6CAA6C,QAAQ,CAAA,iHAAA;AAAA,KAGvD;AARgB,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAEA,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AAAA,EAOlB;AAAA,EAbkB,IAAA,GAAO,qBAAA;AAAA,EAChB,IAAA,GAAO,qBAAA;AAalB;AAGA,IAAM,YAAA,GAAe,CAAC,EAAA,KACpB,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAuClD,eAAsB,cAAA,CACpB,IAAA,EACA,MAAA,EACA,IAAA,GAA8B,EAAC,EACnB;AACZ,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,IAAO,UAAA;AACxB,EAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,IAAS,YAAA;AAI5B,EAAA,IAAI,QAAQ,SAAA,EAAU;AACtB,EAAA,WAAS;AACP,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,IAAA,EAAK;AAAA,IACpB,SAAS,KAAA,EAAO;AAId,MAAA,KAAA,GAAQ,aAAA,CAAc,OAAO,KAAK,CAAA;AAClC,MAAA,IAAI,CAAC,WAAA,CAAY,KAAA,EAAO,MAAM,CAAA,EAAG;AAG/B,QAAA,MAAM,IAAI,mBAAA,CAAoB,KAAA,CAAM,OAAA,EAAS,KAAK,CAAA;AAAA,MACpD;AACA,MAAA,MAAM,KAAA,CAAM,WAAA,CAAY,KAAA,EAAO,MAAA,EAAQ,GAAG,CAAC,CAAA;AAAA,IAC7C;AAAA,EACF;AACF","file":"index.js","sourcesContent":["/**\n * @demlik/tea/retry-to-success — run a fallible async port through BOUNDED\n * retry + exponential backoff to success-or-terminal-give-up, and hand the\n * caller a `Promise<R>` that resolves on the first success or REJECTS with a\n * checkable {@link RetryExhaustedError} the moment the retry bound is hit.\n *\n * This is the retry-side parallel to `./await-terminal`. `runToTerminal` gives a\n * caller with a one-shot decision *machine* a fire-and-await `Promise<State>`\n * without wiring `observe`; this gives a caller with a one-shot fallible *port*\n * a fire-and-await `Promise<R>` without either hand-folding the\n * `./retry-backoff` primitives (`recordFailure` / `shouldRetry` / `nextDelayMs`)\n * into their own reducer each time, or standing up the full `./resilient-call`\n * verb contract (`init` + `attempt`/`succeed`/`fail`/`onTimer` + `subs` +\n * `handlers`) when they want retry only and none of cache / circuit / rate-limit\n * / deadline.\n *\n * It adds NO new runtime capability and NO new backoff math — the whole retry\n * decision is `./retry-backoff` exactly as `resilient-call`'s `backoff` verb\n * wires it:\n * - `initRetry()` seeds the per-call attempt counter.\n * - `recordFailure(retry, error)` folds each thrown failure (attempt++).\n * - `shouldRetry(retry, policy)` is the terminal-at-`maxAttempts` bound — the\n * `maxAttempts`-th recorded failure refuses another attempt (attempts\n * `0 .. maxAttempts-1` run; the port is invoked at most `maxAttempts` times).\n * - `nextDelayMs(retry, policy, rng)` is the exp+jitter+cap delay before the\n * next attempt, computed on the incremented state — same call order as\n * `resilient-call`.\n *\n * Where the clock / RNG live: injected, never read on the module's own behalf,\n * mirroring `retry-backoff`'s purity discipline. `rng` is an {@link Rng} (branded\n * `[0, 1)` via `asRng` at the caller boundary) threaded straight into\n * `nextDelayMs`; the wait itself is a `sleep(ms)` port defaulting to a real\n * `setTimeout`, injectable so a test can drive the backoff schedule\n * deterministically without touching wall-clock timers.\n *\n * Exhaustion is NEVER a silent swallow: at the bound the returned promise\n * rejects with {@link RetryExhaustedError} (carrying the attempt count and the\n * final failure), the retry-side dialect of `await-terminal`'s\n * `TerminalTimeoutError` — a distinct `instanceof`- and `_tag`-checkable\n * rejection.\n *\n * NOT a substrate primitive: it depends on nothing from `../index`, only on the\n * leaf `../retry-backoff` utility. Callers reach it via the\n * `@demlik/tea/retry-to-success` subpath.\n */\n\nimport {\n defaultRng,\n initRetry,\n nextDelayMs,\n type RetryPolicy,\n type Rng,\n recordFailure,\n shouldRetry,\n} from \"../retry-backoff\";\n\n/**\n * Raised when `retryToSuccess` exhausts the retry bound without a success — the\n * `maxAttempts`-th recorded failure refuses another attempt. Follows the tea\n * error dialect (`override readonly name`) and carries a `_tag` discriminant, so\n * a caller can branch on it by `instanceof` or on the tag. It carries the number\n * of attempts made and the final failure (`lastError`) so the terminal give-up\n * is a distinct, checkable rejection — never a silent resolve, and never a\n * swallowed error.\n */\nexport class RetryExhaustedError extends Error {\n override readonly name = \"RetryExhaustedError\";\n readonly _tag = \"RetryExhaustedError\" as const;\n constructor(\n /** How many times the port was invoked before giving up (= `policy.maxAttempts`). */\n public readonly attempts: number,\n /** The failure the final attempt threw — carried, never interpreted. */\n public readonly lastError: unknown,\n ) {\n super(\n `@demlik/tea: retryToSuccess gave up after ${attempts} attempt(s) — the ` +\n `bounded retry reached maxAttempts without a success. See .lastError ` +\n `for the final failure.`,\n );\n }\n}\n\n/** Default wait: a real `setTimeout(ms)`, the sole clock read this module makes. */\nconst defaultSleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/** Options for {@link retryToSuccess}. */\nexport interface RetryToSuccessOptions {\n /**\n * The `[0, 1)` jitter source threaded into `nextDelayMs`, branded via\n * `asRng` at construction (a raw `() => number` is rejected by the type).\n * Injected for deterministic backoff in tests; defaults to `defaultRng`\n * (`Math.random`), read only at the delay boundary — never inside the loop's\n * decision.\n */\n readonly rng?: Rng;\n /**\n * The wait between attempts, given the backoff delay in ms. Injectable so a\n * test can drive the retry schedule without wall-clock timers (and assert the\n * exact delays); defaults to a real `setTimeout`. The delay value is always\n * `./retry-backoff`'s `nextDelayMs` — this port only realizes the wait.\n */\n readonly sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Fire-and-await bounded retry: invoke the fallible `port`, and on each thrown\n * failure fold it through `./retry-backoff` — record it, and if the policy\n * still permits an attempt, back off `nextDelayMs` and retry; otherwise REJECT\n * with {@link RetryExhaustedError}. Resolves with the port's value on the first\n * success.\n *\n * The port is invoked at most `policy.maxAttempts` times (attempts\n * `0 .. maxAttempts-1`); the `maxAttempts`-th failure is terminal. Every failure\n * is either retried or surfaced on the rejection's `lastError` — none is\n * swallowed.\n *\n * @param port the fallible work — throws on failure, resolves with `R` on success.\n * @param policy the bounded-retry curve (`baseMs` / `factor` / `capMs` /\n * `maxAttempts` / `jitter`) from `./retry-backoff`.\n * @param opts optional injected `rng` (jitter) and `sleep` (the wait) — see\n * {@link RetryToSuccessOptions}.\n */\nexport async function retryToSuccess<R>(\n port: () => Promise<R>,\n policy: RetryPolicy,\n opts: RetryToSuccessOptions = {},\n): Promise<R> {\n const rng = opts.rng ?? defaultRng;\n const sleep = opts.sleep ?? defaultSleep;\n\n // The retry counter folds over `./retry-backoff` exactly as resilient-call's\n // `backoff` verb does — no local attempt bookkeeping, no local backoff math.\n let retry = initRetry();\n for (;;) {\n try {\n return await port();\n } catch (error) {\n // Fold the failure first (attempt++), then consult the bound on the\n // incremented state — same order as resilient-call so the semantics of\n // `maxAttempts` are identical across both surfaces.\n retry = recordFailure(retry, error);\n if (!shouldRetry(retry, policy)) {\n // Terminal give-up at the bound: reject, never swallow. `retry.attempt`\n // is the number of failures = the number of port invocations made.\n throw new RetryExhaustedError(retry.attempt, error);\n }\n await sleep(nextDelayMs(retry, policy, rng));\n }\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { C as Cmd } from '../core-CymXZCeY.js';
1
+ import { C as Cmd } from '../core-BmlJOvxM.js';
2
2
  import { QueueItem } from '../work-queue/index.js';
3
3
  import '../index.js';
4
4
 
@@ -12,6 +12,19 @@ import '../index.js';
12
12
  * the saga drives `do` forward one step at a time, and on the first failure it
13
13
  * pivots and drives `undo` backward over exactly the steps that completed.
14
14
  *
15
+ * **Boundary with `@demlik/tea/workflow` — the compensation test.** Both drive
16
+ * an ordered multi-step transaction with reverse-order compensation; the line
17
+ * between them is whether every step carries a true inverse. Here
18
+ * `SagaStep<D, U>` REQUIRES both `do` and `undo` — a step without its inverse
19
+ * is unrepresentable, so full reversibility is a type-level guarantee.
20
+ * Workflow's `WorkflowStep.compensation` is OPTIONAL — an irreversible step (a
21
+ * sent email) is skipped during the unwind. Pick by the test: **every step
22
+ * must be reversible → saga; some steps are irreversible but the run must
23
+ * finish → workflow.** Cross to workflow when the transaction includes steps
24
+ * that can't be undone (or when you want each activity recorded as a durable
25
+ * owed effect on the `../do` ledger); stay here when the type system should
26
+ * enforce that every step can be undone.
27
+ *
15
28
  * The recipe (which bricks it composes):
16
29
  * - `../work-queue/adapter` — the saga's forward progress IS a work-queue
17
30
  * lifecycle. Each step's record carries the same `{ id, status, input }`