@demlik/tea 0.19.0 → 0.21.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 (120) hide show
  1. package/README.md +36 -20
  2. package/dist/agent/index.d.ts +48 -17
  3. package/dist/agent/index.js +10 -10
  4. package/dist/{chunk-IHUNKHDX.js → chunk-4I2LCQKB.js} +3 -3
  5. package/dist/{chunk-IHUNKHDX.js.map → chunk-4I2LCQKB.js.map} +1 -1
  6. package/dist/{chunk-XVIKSRCK.js → chunk-6NIQIUGR.js} +4 -4
  7. package/dist/{chunk-XVIKSRCK.js.map → chunk-6NIQIUGR.js.map} +1 -1
  8. package/dist/{chunk-JVKU2UQC.js → chunk-7Z2IDZ7R.js} +3 -3
  9. package/dist/{chunk-JVKU2UQC.js.map → chunk-7Z2IDZ7R.js.map} +1 -1
  10. package/dist/{chunk-H3MWNH7A.js → chunk-BFSZ4L6Y.js} +4 -4
  11. package/dist/{chunk-H3MWNH7A.js.map → chunk-BFSZ4L6Y.js.map} +1 -1
  12. package/dist/{chunk-H2ELKHI6.js → chunk-EB5OMOXX.js} +4 -4
  13. package/dist/chunk-EB5OMOXX.js.map +1 -0
  14. package/dist/{chunk-AIIXV4XB.js → chunk-GAVLND5C.js} +19 -4
  15. package/dist/chunk-GAVLND5C.js.map +1 -0
  16. package/dist/{chunk-I7E7AD2S.js → chunk-HU3Y2AZY.js} +4 -4
  17. package/dist/chunk-HU3Y2AZY.js.map +1 -0
  18. package/dist/{chunk-WTSOW2MD.js → chunk-MBQR3LFW.js} +5 -5
  19. package/dist/{chunk-WTSOW2MD.js.map → chunk-MBQR3LFW.js.map} +1 -1
  20. package/dist/{chunk-C3G2QFEZ.js → chunk-QIE4Y6BK.js} +4 -4
  21. package/dist/{chunk-C3G2QFEZ.js.map → chunk-QIE4Y6BK.js.map} +1 -1
  22. package/dist/{chunk-BRX3243I.js → chunk-TMYUDZLE.js} +3 -3
  23. package/dist/{chunk-BRX3243I.js.map → chunk-TMYUDZLE.js.map} +1 -1
  24. package/dist/{chunk-FNGZMVIT.js → chunk-VHKDUBXC.js} +3 -3
  25. package/dist/chunk-VHKDUBXC.js.map +1 -0
  26. package/dist/{chunk-AHW2CLPT.js → chunk-VM7PMFV5.js} +70 -47
  27. package/dist/chunk-VM7PMFV5.js.map +1 -0
  28. package/dist/{chunk-PCJA2HTZ.js → chunk-W6XWI6QE.js} +20 -20
  29. package/dist/chunk-W6XWI6QE.js.map +1 -0
  30. package/dist/{chunk-JYNYG5SF.js → chunk-YCJPLPQQ.js} +3 -3
  31. package/dist/chunk-YCJPLPQQ.js.map +1 -0
  32. package/dist/{chunk-YIMOFXST.js → chunk-YDF6P74H.js} +3 -3
  33. package/dist/chunk-YDF6P74H.js.map +1 -0
  34. package/dist/{chunk-JVE2LBFJ.js → chunk-Z7IL735U.js} +3 -3
  35. package/dist/{chunk-JVE2LBFJ.js.map → chunk-Z7IL735U.js.map} +1 -1
  36. package/dist/{chunk-PQSDUX6B.js → chunk-ZV4WGW74.js} +3 -3
  37. package/dist/{chunk-PQSDUX6B.js.map → chunk-ZV4WGW74.js.map} +1 -1
  38. package/dist/{core-Ce9ncpuv.d.ts → core-BKJW1Kmq.d.ts} +154 -2
  39. package/dist/devtools/index.d.ts +17 -0
  40. package/dist/devtools/index.js.map +1 -1
  41. package/dist/do/index.d.ts +17 -15
  42. package/dist/do/index.js +11 -11
  43. package/dist/do/index.js.map +1 -1
  44. package/dist/{drive-loop-CsUsW3Wo.d.ts → drive-loop-Dc6Z8Vbs.d.ts} +1 -1
  45. package/dist/{durable-effects-BmUrRA-h.d.ts → durable-effects-CgrLNccA.d.ts} +1 -1
  46. package/dist/effect/index.d.ts +191 -5
  47. package/dist/effect/index.js +71 -4
  48. package/dist/effect/index.js.map +1 -1
  49. package/dist/extension/index.d.ts +84 -3
  50. package/dist/extension/index.js +2 -2
  51. package/dist/extension/index.js.map +1 -1
  52. package/dist/flow/index.d.ts +33 -13
  53. package/dist/flow/index.js +13 -12
  54. package/dist/flow/index.js.map +1 -1
  55. package/dist/idempotency/index.d.ts +11 -3
  56. package/dist/idempotency/index.js +3 -3
  57. package/dist/{index-Ce5GYAuI.d.ts → index-1hit3dJ9.d.ts} +2 -2
  58. package/dist/{index-2kNxy6az.d.ts → index-B-VC4qjx.d.ts} +16 -4
  59. package/dist/{index-Bf9_bNql.d.ts → index-BjwRcjBi.d.ts} +2 -2
  60. package/dist/{index-Dy3379ve.d.ts → index-CYrUWi1c.d.ts} +4 -4
  61. package/dist/{index-DWdg7R1l.d.ts → index-Cuy1_xTh.d.ts} +5 -1
  62. package/dist/{index-D8sYVzCI.d.ts → index-DB8Yn0-K.d.ts} +6 -1
  63. package/dist/{index-e36uEyl4.d.ts → index-Diyo8WBO.d.ts} +2 -2
  64. package/dist/{index-B1Wzarkl.d.ts → index-Z6Uw7C9t.d.ts} +1 -1
  65. package/dist/index.d.ts +70 -9
  66. package/dist/index.js +2 -2
  67. package/dist/jev/index.d.ts +46 -34
  68. package/dist/jev/index.js +12 -8
  69. package/dist/jev/index.js.map +1 -1
  70. package/dist/{machine-CuojfZup.d.ts → machine-BuPTS1QH.d.ts} +11 -9
  71. package/dist/machine-viz/index.d.ts +2 -1
  72. package/dist/machine-viz/index.js +2 -2
  73. package/dist/machine-viz/index.js.map +1 -1
  74. package/dist/mem/index.d.ts +2 -2
  75. package/dist/mem/index.js +2 -2
  76. package/dist/node/index.d.ts +11 -2
  77. package/dist/node/index.js +2 -2
  78. package/dist/node/index.js.map +1 -1
  79. package/dist/otel/index.d.ts +10 -10
  80. package/dist/otel/index.js +10 -10
  81. package/dist/paginate/index.d.ts +5 -4
  82. package/dist/paginate/index.js +5 -5
  83. package/dist/parity/index.d.ts +3 -3
  84. package/dist/parity/index.js +3 -3
  85. package/dist/parity/index.js.map +1 -1
  86. package/dist/pbt/index.d.ts +1 -1
  87. package/dist/pbt/index.js +2 -2
  88. package/dist/persistence/index.d.ts +5 -5
  89. package/dist/persistence/index.js +4 -4
  90. package/dist/promise/index.d.ts +30 -10
  91. package/dist/promise/index.js +4 -4
  92. package/dist/react/index.d.ts +7 -6
  93. package/dist/react/index.js.map +1 -1
  94. package/dist/resilience/index.d.ts +7 -6
  95. package/dist/resilience/index.js +5 -5
  96. package/dist/resilience/index.js.map +1 -1
  97. package/dist/{index-BnsAqn0-.d.ts → run-D0sxbyno.d.ts} +23 -7
  98. package/dist/{runtime-types-BKg2CGMx.d.ts → runtime-types-BPKp2bGo.d.ts} +120 -9
  99. package/dist/testing/effect/index.d.ts +5 -5
  100. package/dist/testing/effect/index.js +8 -8
  101. package/dist/testing/index.d.ts +2 -2
  102. package/dist/testing/index.js +3 -3
  103. package/dist/testing/promise/index.d.ts +4 -4
  104. package/dist/testing/promise/index.js +5 -5
  105. package/dist/timing/index.d.ts +4 -4
  106. package/dist/timing/index.js +4 -4
  107. package/dist/types-BiXj7k_k.d.ts +9 -0
  108. package/dist/work-queue/index.d.ts +4 -4
  109. package/dist/work-queue/index.js +2 -2
  110. package/dist/work-queue/index.js.map +1 -1
  111. package/package.json +6 -3
  112. package/dist/chunk-AHW2CLPT.js.map +0 -1
  113. package/dist/chunk-AIIXV4XB.js.map +0 -1
  114. package/dist/chunk-FNGZMVIT.js.map +0 -1
  115. package/dist/chunk-H2ELKHI6.js.map +0 -1
  116. package/dist/chunk-I7E7AD2S.js.map +0 -1
  117. package/dist/chunk-JYNYG5SF.js.map +0 -1
  118. package/dist/chunk-PCJA2HTZ.js.map +0 -1
  119. package/dist/chunk-YIMOFXST.js.map +0 -1
  120. package/dist/types-SzNSHKlV.d.ts +0 -5
@@ -1,5 +1,5 @@
1
1
  import * as fc from 'fast-check';
2
- import { C as Cmd, c as Sub, M as Machine, g as Reducer, i as Transitions, U as UpdateForm } from '../core-Ce9ncpuv.js';
2
+ import { C as Cmd, c as Sub, M as Machine, g as Reducer, i as Transitions, U as UpdateForm } from '../core-BKJW1Kmq.js';
3
3
  import '@standard-schema/spec';
4
4
 
5
5
  /**
package/dist/pbt/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import '../chunk-FNGZMVIT.js';
2
- import { replay, applyCell, msgKeysOf } from '../chunk-AIIXV4XB.js';
1
+ import '../chunk-VHKDUBXC.js';
2
+ import { replay, applyCell, msgKeysOf } from '../chunk-GAVLND5C.js';
3
3
  import * as fc from 'fast-check';
4
4
 
5
5
  // src/pbt/arbitraries/ctx.ts
@@ -1,8 +1,8 @@
1
- import { T as Trace } from '../index-B1Wzarkl.js';
2
- export { B as BreadcrumbsOptions, a as Recorder, R as RecorderOptions, b as TraceAttachment, c as TraceBreadcrumb, d as breadcrumbsFromTrace, p as parseJSONL, r as recorder, t as traceAttachment } from '../index-B1Wzarkl.js';
3
- export { c as SnapshotConfig, d as SnapshotFailedMsg, e as SnapshotKnob, f as SnapshotLoadCmd, g as SnapshotLoadFailedMsg, h as SnapshotLoadedMsg, b as SnapshotSavedMsg, a as SnapshotState, S as SnapshotWriteCmd, i as createSnapshot, s as snapshotLoadDef, j as snapshotWriteDef } from '../index-D8sYVzCI.js';
4
- import { C as Cmd, c as Sub, M as Machine } from '../core-Ce9ncpuv.js';
5
- import '../runtime-types-BKg2CGMx.js';
1
+ import { T as Trace } from '../index-Z6Uw7C9t.js';
2
+ export { B as BreadcrumbsOptions, a as Recorder, R as RecorderOptions, b as TraceAttachment, c as TraceBreadcrumb, d as breadcrumbsFromTrace, p as parseJSONL, r as recorder, t as traceAttachment } from '../index-Z6Uw7C9t.js';
3
+ export { c as SnapshotConfig, d as SnapshotFailedMsg, e as SnapshotKnob, f as SnapshotLoadCmd, g as SnapshotLoadFailedMsg, h as SnapshotLoadedMsg, b as SnapshotSavedMsg, a as SnapshotState, S as SnapshotWriteCmd, i as createSnapshot, s as snapshotLoadDef, j as snapshotWriteDef } from '../index-DB8Yn0-K.js';
4
+ import { C as Cmd, c as Sub, M as Machine } from '../core-BKJW1Kmq.js';
5
+ import '../runtime-types-BPKp2bGo.js';
6
6
  import '@standard-schema/spec';
7
7
 
8
8
  /**
@@ -1,8 +1,8 @@
1
- export { breadcrumbsFromTrace, deepEqual, parseJSONL, recorder, replayTrace, traceAttachment } from '../chunk-BRX3243I.js';
1
+ export { breadcrumbsFromTrace, deepEqual, parseJSONL, recorder, replayTrace, traceAttachment } from '../chunk-TMYUDZLE.js';
2
2
  import '../chunk-HWVW4AXC.js';
3
- export { createSnapshot, snapshotLoadDef, snapshotWriteDef } from '../chunk-YIMOFXST.js';
3
+ export { createSnapshot, snapshotLoadDef, snapshotWriteDef } from '../chunk-YDF6P74H.js';
4
4
  import '../chunk-4LPY67E4.js';
5
- import '../chunk-FNGZMVIT.js';
6
- import '../chunk-AIIXV4XB.js';
5
+ import '../chunk-VHKDUBXC.js';
6
+ import '../chunk-GAVLND5C.js';
7
7
  //# sourceMappingURL=index.js.map
8
8
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
- import { C as Cmd, c as Sub, M as Machine, R as RunHandlers } from '../core-Ce9ncpuv.js';
2
- import { B as BootingRuntime, C as CtxArg, S as Store, O as OnError, a as Supervision, T as TelemetrySink } from '../runtime-types-BKg2CGMx.js';
1
+ import { C as Cmd, c as Sub, M as Machine, R as RunHandlers } from '../core-BKJW1Kmq.js';
2
+ import { B as BootingRuntime, C as CtxArg, S as Store, O as OnError, a as Supervision, T as TelemetrySink } from '../runtime-types-BPKp2bGo.js';
3
3
  import '@standard-schema/spec';
4
4
 
5
5
  /**
@@ -12,6 +12,11 @@ import '@standard-schema/spec';
12
12
  * built from `run`'s options in the order both engines share.
13
13
  */
14
14
 
15
+ /**
16
+ * Start a machine on the Promise engine and return its runtime. Pass the Cmd
17
+ * handlers, the Sub runners, `ctx` and an optional `Store`; the runtime is
18
+ * returned at once and every method waits for boot.
19
+ */
15
20
  declare function run<S, M extends {
16
21
  type: string;
17
22
  }, C extends Cmd, U extends Sub, Ctx, E extends {
@@ -51,17 +56,31 @@ declare function run<S, M extends {
51
56
  */
52
57
  telemetry?: TelemetrySink;
53
58
  /**
54
- * How long `stop()` waits for teardown work that returned a Promise (an
55
- * async `release` in `defineManagedResource`, an async Sub cleanup) before
56
- * giving up on it. Defaults to 5_000ms.
59
+ * How long `stop()` waits for work it cannot interrupt before giving up
60
+ * on it. Defaults to 5_000ms. It bounds two waits, one after the other:
61
+ * the queue, where a Cmd handler may still be running, and then the
62
+ * teardown work that returned a Promise (an async `release` in
63
+ * `defineManagedResource`, an async Sub cleanup).
57
64
  *
58
65
  * `stop()` awaits those disposals so a host doing
59
66
  * `await runtime.stop(); env.evict()` cannot drop the isolate mid-release —
60
67
  * the leak the managed-resource battery exists to prevent, relocated to
61
- * shutdown. The bound is what keeps a release that never settles from
62
- * hanging the host: on expiry `stop()` reports a `DisposeTimeoutNotice`
63
- * (warn-only, like every `RuntimeDiscardNotice`) and resolves anyway,
64
- * because `stop()` resolving is a contract.
68
+ * shutdown. The bound is what keeps a handler that never returns, or a
69
+ * release that never settles, from hanging the host: on expiry `stop()`
70
+ * reports a `DrainTimeoutNotice` or a `DisposeTimeoutNotice` (warn-only,
71
+ * like every `RuntimeDiscardNotice`) and resolves anyway, because `stop()`
72
+ * resolving is a contract (ADR 0025). The Subs are still stopped and the
73
+ * final save still runs. A handler `stop()` gave up on keeps running;
74
+ * what it returns later is dropped.
75
+ *
76
+ * tea starts no store call after `stop()` has returned. A store call
77
+ * already under way when `stop()` gave up is not cancelled and can
78
+ * finish after `stop()` has returned. On a plain store that late save
79
+ * can overwrite what a newer run saved; a fenced store refuses it with
80
+ * `StoreConflictError`. So a host that starts a new run on the same
81
+ * store after a `stop()` that gave up needs a fenced store. A run
82
+ * stopped while its `store.load()` was pending saves nothing when the
83
+ * load returns: `ready` resolves with the loaded State, unsaved.
65
84
  */
66
85
  disposeTimeoutMs?: number;
67
86
  /**
@@ -166,7 +185,8 @@ type DriveToDoneOptions<S, M extends {
166
185
  * throws later, since both leave the run with no State to settle on. A State the
167
186
  * caller's `failed` predicate marks rejects on an abort exit exactly as it does
168
187
  * anywhere else. In-flight effects are not recalled — a promise cannot be cancelled —
169
- * so they settle to their own end and `stop()` drains them as it always did;
188
+ * so they settle to their own end and `stop()` drains them, up to
189
+ * `disposeTimeoutMs`;
170
190
  * keeping their results off the public channels is the machine's own business.
171
191
  * Omit the pair and every path here behaves exactly as it did before.
172
192
  *
@@ -1,6 +1,6 @@
1
- export { driveToDone, run } from '../chunk-H2ELKHI6.js';
2
- import '../chunk-AHW2CLPT.js';
3
- import '../chunk-JVE2LBFJ.js';
4
- import '../chunk-AIIXV4XB.js';
1
+ export { driveToDone, run } from '../chunk-EB5OMOXX.js';
2
+ import '../chunk-VM7PMFV5.js';
3
+ import '../chunk-Z7IL735U.js';
4
+ import '../chunk-GAVLND5C.js';
5
5
  //# sourceMappingURL=index.js.map
6
6
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
- import { E as EngineRun, S as Store, b as BootedRunHandle } from '../runtime-types-BKg2CGMx.js';
2
- import { C as Cmd, c as Sub, R as RunHandlers, M as Machine } from '../core-Ce9ncpuv.js';
1
+ import { E as EngineRun, S as Store, b as BootedRunHandle } from '../runtime-types-BPKp2bGo.js';
2
+ import { C as Cmd, c as Sub, R as RunHandlers, M as Machine } from '../core-BKJW1Kmq.js';
3
3
  import '@standard-schema/spec';
4
4
 
5
5
  /**
@@ -68,10 +68,11 @@ type UseMachineOpts<S, M extends {
68
68
  * - Subscribes via `useSyncExternalStore(runtime.subscribe, runtime.getState,
69
69
  * runtime.getState)` — tearing-free under React 18 concurrent rendering.
70
70
  * - On unmount (or any dep change), `useEffect` cleanup calls
71
- * `runtime.stop()` — drains the queue, runs every active sub cleanup,
72
- * flushes state, and — if Cmds were still in flight — reports
73
- * `RuntimeDiscardedError` (`phase: "discard"`) rather than dropping them
74
- * silently.
71
+ * `runtime.stop()` — drains the queue, up to `disposeTimeoutMs`, runs every
72
+ * active sub cleanup, flushes state, and — if Cmds were still in flight —
73
+ * reports `RuntimeDiscardedError` (`phase: "discard"`) rather than dropping
74
+ * them silently. A handler that has not returned by the limit is given up
75
+ * on and reported as a `DrainTimeoutNotice`.
75
76
  *
76
77
  * Returns `[state, dispatch]` shaped like `useReducer`.
77
78
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.ts"],"names":[],"mappings":";;AAqGO,SAAS,UAAA,CAOd,SACA,IAAA,EACgC;AAMhC,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,QAAQ,IAAA,CAAK,KAAA;AAInB,EAAA,MAAM,WAAA,GAAc,OAAkC,IAAI,CAAA;AAC1D,EAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AACtB,EAAA,MAAM,QAAA,GAAW,OAAA;AAAA,IACf,OAAO;AAAA,MACL,SAAA,EAAW,WAAA;AAAA,QACT,MAAO,YAAY,OAAA,CAAmC;AAAA,OACxD;AAAA,MACA,SAAA,EAAW,WAAA,CAAY,MAAM,WAAA,CAAY,QAAQ,SAAS;AAAA,KAG5D,CAAA;AAAA,IACA;AAAC,GACH;AAMA,EAAA,MAAM,OAAA,GAAU,OAAA;AAAA,IACd,MACE,IAAI,OAAA,EAAS;AAAA,MACX,GAAA;AAAA,MACA,KAAA;AAAA,MACA,GAAG;AAAA,KAC2B,CAAA;AAAA,IAClC,CAAC,OAAA,EAAS,GAAA,EAAK,GAAA,EAAK,OAAO,QAAQ;AAAA,GACrC;AAeA,EAAA,MAAM,QAAA,GAAW,OAAqC,IAAI,CAAA;AAC1D,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;AAOA,SAAS,YAAY,OAAA,EAA2C;AAC9D,EAAA,OAAO,IAAI,KAAA;AAAA,IACT,EAAC;AAAA,IACD;AAAA,MACE,KAAK,CAAC,OAAA,EAAS,IAAA,KACZ,OAAA,KAAyD,IAAI;AAAA;AAClE,GACF;AACF;AAgBO,SAAS,WAId,OAAA,EAAmE;AACnE,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 * @packageDocumentation\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 type {\n BootedRunHandle,\n Cmd,\n EngineRun,\n Interpret,\n Machine,\n RunHandle,\n RunHandlers,\n RunOptions,\n Store,\n Sub,\n Subscribe,\n} from \"../index\";\n\n/**\n * Options passed to `useMachine`. `run` is the engine's `run` — `run` from\n * `@demlik/tea/promise`, or any engine's; the hook imports no engine. `ctx`\n * is required (every machine has one), `store` is optional (omit it for\n * volatile-state machines), and the handlers are the ones `run` takes beside\n * the machine: `interpret` (required once the machine emits a Cmd) and the\n * `subscribe` runners (required once the machine declares a Sub type other\n * than the built-in `timer`) — see {@link RunHandlers}. Same requiredness as\n * `run`, because it is the same type.\n *\n * **The handlers are read fresh, never memoized on.** A handler table written\n * inline in the component is a new object every render; keying the runtime on\n * it would reboot the machine on every render. So `useMachine` hands `run` a\n * table that looks each cell up on the latest render's `interpret` /\n * `subscribe` at the moment a Cmd runs or a Sub starts — a handler may close\n * over props and state freely.\n *\n * **Identity matters.** The runtime is memoized on `[machine, opts.run,\n * opts.ctx, opts.store]`. A new `ctx` reference rebuilds the runtime, and so\n * does a new `run` — pass the engine's own function, not an inline wrapper. Always `useMemo`\n * (or otherwise stabilize) the ctx at the call site. See README.\n *\n * Forgetting to is no longer silent: the replaced runtime's `stop()` reports\n * `RuntimeDiscardedError` under `phase: \"discard\"` when Cmds were still in\n * flight, which the substrate's default sink warns about (issue #365).\n */\nexport type UseMachineOpts<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n> = {\n run: EngineRun<S, M, C, U, Ctx>;\n ctx: Ctx;\n store?: Store<S>;\n} & RunHandlers<M, C, U, Ctx>;\n\n/**\n * Build and own a run of `machine` for the lifetime of the component mount, on\n * the engine whose `run` the caller hands in.\n *\n * - Builds via `useMemo(() => opts.run(machine, opts), [machine, opts.run,\n * opts.ctx, opts.store])` — the run is recreated when ANY of those\n * identities 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, and — if Cmds were still in flight — reports\n * `RuntimeDiscardedError` (`phase: \"discard\"`) rather than dropping them\n * silently.\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: NoInfer<UseMachineOpts<S, M, C, U, Ctx>>,\n): [S, (msg: M) => Promise<void>] {\n // Deps are literal: machine, engine, ctx and store identity. NOT `[opts]`\n // (would rebuild every render — callers pass fresh objects). We pass\n // explicit fields to `run()` so the closure captures only the deps in the\n // array — keeps biome's `useExhaustiveDependencies` happy without lying\n // about what the memo actually depends on.\n const run = opts.run;\n const ctx = opts.ctx;\n const store = opts.store;\n // The latest render's handlers, read per call (see `UseMachineOpts`). Written\n // during render on purpose: a Cmd a transition emits runs after the render\n // that produced it, so the table it reads is never older than that render.\n const handlersRef = useRef<RunHandlers<M, C, U, Ctx>>(opts);\n handlersRef.current = opts;\n const handlers = useMemo(\n () => ({\n interpret: latestTable(\n () => (handlersRef.current as { interpret?: object }).interpret,\n ) as Interpret<M, C, Ctx>,\n subscribe: latestTable(() => handlersRef.current.subscribe) as Partial<\n Subscribe<M, U, Ctx>\n >,\n }),\n [],\n );\n // `run()` returns a `RunHandle<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 one (the only thing with a total\n // `getState`) once `ready` resolves.\n const booting = useMemo<RunHandle<S, M>>(\n () =>\n run(machine, {\n ctx,\n store,\n ...handlers,\n } as RunOptions<S, M, C, U, Ctx>),\n [machine, run, ctx, store, handlers],\n );\n\n // Captures the booted handle 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<BootedRunHandle<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 handle 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 * A handler table whose every cell is looked up on `current()` at the moment it\n * is read, so `run` — which reads a cell per Cmd / per Sub start — always sees\n * the latest render's handler without the runtime being rebuilt.\n */\nfunction latestTable(current: () => object | undefined): object {\n return new Proxy(\n {},\n {\n get: (_target, type) =>\n (current() as Record<PropertyKey, unknown> | undefined)?.[type],\n },\n );\n}\n\n/**\n * Lower-level escape hatch: consume an externally-built, booted run — any\n * engine's {@link BootedRunHandle} (the Promise engine's `Runtime` is one).\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<\n S,\n M extends { type: string },\n E extends { type: string } = never,\n>(runtime: BootedRunHandle<S, M, E>): [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":";;AAsGO,SAAS,UAAA,CAOd,SACA,IAAA,EACgC;AAMhC,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,QAAQ,IAAA,CAAK,KAAA;AAInB,EAAA,MAAM,WAAA,GAAc,OAAkC,IAAI,CAAA;AAC1D,EAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AACtB,EAAA,MAAM,QAAA,GAAW,OAAA;AAAA,IACf,OAAO;AAAA,MACL,SAAA,EAAW,WAAA;AAAA,QACT,MAAO,YAAY,OAAA,CAAmC;AAAA,OACxD;AAAA,MACA,SAAA,EAAW,WAAA,CAAY,MAAM,WAAA,CAAY,QAAQ,SAAS;AAAA,KAG5D,CAAA;AAAA,IACA;AAAC,GACH;AAMA,EAAA,MAAM,OAAA,GAAU,OAAA;AAAA,IACd,MACE,IAAI,OAAA,EAAS;AAAA,MACX,GAAA;AAAA,MACA,KAAA;AAAA,MACA,GAAG;AAAA,KAC2B,CAAA;AAAA,IAClC,CAAC,OAAA,EAAS,GAAA,EAAK,GAAA,EAAK,OAAO,QAAQ;AAAA,GACrC;AAeA,EAAA,MAAM,QAAA,GAAW,OAAqC,IAAI,CAAA;AAC1D,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;AAOA,SAAS,YAAY,OAAA,EAA2C;AAC9D,EAAA,OAAO,IAAI,KAAA;AAAA,IACT,EAAC;AAAA,IACD;AAAA,MACE,KAAK,CAAC,OAAA,EAAS,IAAA,KACZ,OAAA,KAAyD,IAAI;AAAA;AAClE,GACF;AACF;AAgBO,SAAS,WAId,OAAA,EAAmE;AACnE,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 * @packageDocumentation\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 type {\n BootedRunHandle,\n Cmd,\n EngineRun,\n Interpret,\n Machine,\n RunHandle,\n RunHandlers,\n RunOptions,\n Store,\n Sub,\n Subscribe,\n} from \"../index\";\n\n/**\n * Options passed to `useMachine`. `run` is the engine's `run` — `run` from\n * `@demlik/tea/promise`, or any engine's; the hook imports no engine. `ctx`\n * is required (every machine has one), `store` is optional (omit it for\n * volatile-state machines), and the handlers are the ones `run` takes beside\n * the machine: `interpret` (required once the machine emits a Cmd) and the\n * `subscribe` runners (required once the machine declares a Sub type other\n * than the built-in `timer`) — see {@link RunHandlers}. Same requiredness as\n * `run`, because it is the same type.\n *\n * **The handlers are read fresh, never memoized on.** A handler table written\n * inline in the component is a new object every render; keying the runtime on\n * it would reboot the machine on every render. So `useMachine` hands `run` a\n * table that looks each cell up on the latest render's `interpret` /\n * `subscribe` at the moment a Cmd runs or a Sub starts — a handler may close\n * over props and state freely.\n *\n * **Identity matters.** The runtime is memoized on `[machine, opts.run,\n * opts.ctx, opts.store]`. A new `ctx` reference rebuilds the runtime, and so\n * does a new `run` — pass the engine's own function, not an inline wrapper. Always `useMemo`\n * (or otherwise stabilize) the ctx at the call site. See README.\n *\n * Forgetting to is no longer silent: the replaced runtime's `stop()` reports\n * `RuntimeDiscardedError` under `phase: \"discard\"` when Cmds were still in\n * flight, which the substrate's default sink warns about (issue #365).\n */\nexport type UseMachineOpts<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n> = {\n run: EngineRun<S, M, C, U, Ctx>;\n ctx: Ctx;\n store?: Store<S>;\n} & RunHandlers<M, C, U, Ctx>;\n\n/**\n * Build and own a run of `machine` for the lifetime of the component mount, on\n * the engine whose `run` the caller hands in.\n *\n * - Builds via `useMemo(() => opts.run(machine, opts), [machine, opts.run,\n * opts.ctx, opts.store])` — the run is recreated when ANY of those\n * identities 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, up to `disposeTimeoutMs`, runs every\n * active sub cleanup, flushes state, and — if Cmds were still in flight —\n * reports `RuntimeDiscardedError` (`phase: \"discard\"`) rather than dropping\n * them silently. A handler that has not returned by the limit is given up\n * on and reported as a `DrainTimeoutNotice`.\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: NoInfer<UseMachineOpts<S, M, C, U, Ctx>>,\n): [S, (msg: M) => Promise<void>] {\n // Deps are literal: machine, engine, ctx and store identity. NOT `[opts]`\n // (would rebuild every render — callers pass fresh objects). We pass\n // explicit fields to `run()` so the closure captures only the deps in the\n // array — keeps biome's `useExhaustiveDependencies` happy without lying\n // about what the memo actually depends on.\n const run = opts.run;\n const ctx = opts.ctx;\n const store = opts.store;\n // The latest render's handlers, read per call (see `UseMachineOpts`). Written\n // during render on purpose: a Cmd a transition emits runs after the render\n // that produced it, so the table it reads is never older than that render.\n const handlersRef = useRef<RunHandlers<M, C, U, Ctx>>(opts);\n handlersRef.current = opts;\n const handlers = useMemo(\n () => ({\n interpret: latestTable(\n () => (handlersRef.current as { interpret?: object }).interpret,\n ) as Interpret<M, C, Ctx>,\n subscribe: latestTable(() => handlersRef.current.subscribe) as Partial<\n Subscribe<M, U, Ctx>\n >,\n }),\n [],\n );\n // `run()` returns a `RunHandle<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 one (the only thing with a total\n // `getState`) once `ready` resolves.\n const booting = useMemo<RunHandle<S, M>>(\n () =>\n run(machine, {\n ctx,\n store,\n ...handlers,\n } as RunOptions<S, M, C, U, Ctx>),\n [machine, run, ctx, store, handlers],\n );\n\n // Captures the booted handle 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<BootedRunHandle<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 handle 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 * A handler table whose every cell is looked up on `current()` at the moment it\n * is read, so `run` — which reads a cell per Cmd / per Sub start — always sees\n * the latest render's handler without the runtime being rebuilt.\n */\nfunction latestTable(current: () => object | undefined): object {\n return new Proxy(\n {},\n {\n get: (_target, type) =>\n (current() as Record<PropertyKey, unknown> | undefined)?.[type],\n },\n );\n}\n\n/**\n * Lower-level escape hatch: consume an externally-built, booted run — any\n * engine's {@link BootedRunHandle} (the Promise engine's `Runtime` is one).\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<\n S,\n M extends { type: string },\n E extends { type: string } = never,\n>(runtime: BootedRunHandle<S, M, E>): [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,12 +1,12 @@
1
- import { a as CmdOf, b as CmdDef, e as SettledErr, f as TaggedError, d as SettledOk, T as TimerDeps, C as Cmd } from '../core-Ce9ncpuv.js';
1
+ import { a as CmdOf, b as CmdDef, e as SettledErr, f as TaggedError, d as SettledOk, T as TimerDeps, C as Cmd } from '../core-BKJW1Kmq.js';
2
2
  import { M as MsgType } from '../protocol-ClRZBhHQ.js';
3
- import { a as DeadlineSub } from '../index-e36uEyl4.js';
4
- export { A as ArmTimer, e as CacheEntry, c as CacheEvictMsg, f as CacheEvictionDeps, C as CacheEvictionSub, D as DeadlineExceeded, g as DeadlineMsgType, h as DeadlineOpts, b as DeadlinesSub, T as TtlCache, i as cacheEvictMsg, j as cacheEvictionSub, k as cacheEvictionSubscribe, d as deadlineExceeded, l as deadlineMsgType, m as deadlineSub, n as deadlines, o as deadlinesSub, p as evictExpired, q as get, r as has, t as initCache, u as nextTimer, v as remove, w as set, x as setTimeoutArmTimer, s as subscribeDeadline, y as subscribeWith } from '../index-e36uEyl4.js';
5
- import { c as ResilientConfig, a as ResilientState, R as RunCmd, S as SucceedMsg, F as FailMsg, b as ResilientTimerMsg } from '../index-2kNxy6az.js';
6
- export { C as CacheConfig, d as CallBudget, e as CallPhase, f as CircuitConfig, g as CircuitPolicy, h as CircuitState, D as DEFAULT_RESILIENT_NAME, i as DeadlineExceededError, j as DeadlineNameOf, k as DefaultResilientName, l as RateLimitConfig, m as ResilientCallDeadlineConfig, n as ResilientDeadlineType, o as ResilientErrType, p as ResilientOkType, q as ResilientRunType, r as RunCmdDef, s as RunErr, t as SettleMsg, u as SettleOutcome, v as SettleResult, w as SlidingWindow, T as TokenBucket, x as canPass, y as createResilientCall, z as defaultCircuitPolicy, A as initBucket, B as initCircuit, E as initWindow, G as liftResilience, H as onFailure, I as onSuccess, J as record, K as refill, L as remaining, M as runCmdDef, N as tryConsume } from '../index-2kNxy6az.js';
3
+ import { a as DeadlineSub } from '../index-Diyo8WBO.js';
4
+ export { A as ArmTimer, e as CacheEntry, c as CacheEvictMsg, f as CacheEvictionDeps, C as CacheEvictionSub, D as DeadlineExceeded, g as DeadlineMsgType, h as DeadlineOpts, b as DeadlinesSub, T as TtlCache, i as cacheEvictMsg, j as cacheEvictionSub, k as cacheEvictionSubscribe, d as deadlineExceeded, l as deadlineMsgType, m as deadlineSub, n as deadlines, o as deadlinesSub, p as evictExpired, q as get, r as has, t as initCache, u as nextTimer, v as remove, w as set, x as setTimeoutArmTimer, s as subscribeDeadline, y as subscribeWith } from '../index-Diyo8WBO.js';
5
+ import { d as ResilientConfig, b as ResilientState, R as RunCmd, S as SucceedMsg, F as FailMsg, a as ResilientTimerMsg } from '../index-B-VC4qjx.js';
6
+ export { C as CacheConfig, e as CallBudget, f as CallPhase, g as CircuitConfig, h as CircuitPolicy, i as CircuitState, j as DEFAULT_RESILIENT_NAME, k as DeadlineExceededError, c as DeadlineNameOf, D as DefaultResilientName, l as RateLimitConfig, m as ResilientCallDeadlineConfig, n as ResilientDeadlineType, o as ResilientErrType, p as ResilientOkType, q as ResilientRunType, r as RunCmdDef, s as RunErr, t as SettleMsg, u as SettleOutcome, v as SettleResult, w as SlidingWindow, T as TokenBucket, x as canPass, y as createResilientCall, z as defaultCircuitPolicy, A as initBucket, B as initCircuit, E as initWindow, G as liftResilience, H as onFailure, I as onSuccess, J as record, K as refill, L as remaining, M as runCmdDef, N as tryConsume } from '../index-B-VC4qjx.js';
7
7
  import { Rng, RetryPolicy } from '../retry-backoff/index.js';
8
8
  import '@standard-schema/spec';
9
- import '../types-SzNSHKlV.js';
9
+ import '../types-BiXj7k_k.js';
10
10
 
11
11
  /**
12
12
  * A minted credential: the opaque `value` to send on the wire, and the absolute
@@ -85,6 +85,7 @@ declare const refreshToken: CmdDef<"refresh_token", Record<string, never>, Token
85
85
  readonly [detail: string]: unknown;
86
86
  readonly _tag: "token_refresh_failed";
87
87
  }>;
88
+ /** The `refresh_token` Cmd value that `refreshTokenCmd` builds. */
88
89
  type RefreshTokenCmd = CmdOf<typeof refreshToken>;
89
90
  /** Construct a `refresh_token` Cmd. */
90
91
  declare function refreshTokenCmd(): RefreshTokenCmd;
@@ -1,10 +1,10 @@
1
- import { createResilientCall } from '../chunk-WTSOW2MD.js';
2
- export { DEFAULT_RESILIENT_NAME, canPass, createResilientCall, defaultCircuitPolicy, initBucket, initCircuit, initWindow, liftResilience, onFailure, onSuccess, record, refill, remaining, runCmdDef, tryConsume } from '../chunk-WTSOW2MD.js';
3
- export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, deadlineExceeded, deadlineMsgType, deadlineSub, deadlines, deadlinesSub, evictExpired, get, has, initCache, nextTimer, remove, set, setTimeoutArmTimer, subscribeDeadline, subscribeWith } from '../chunk-C3G2QFEZ.js';
1
+ import { createResilientCall } from '../chunk-MBQR3LFW.js';
2
+ export { DEFAULT_RESILIENT_NAME, canPass, createResilientCall, defaultCircuitPolicy, initBucket, initCircuit, initWindow, liftResilience, onFailure, onSuccess, record, refill, remaining, runCmdDef, tryConsume } from '../chunk-MBQR3LFW.js';
3
+ export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, deadlineExceeded, deadlineMsgType, deadlineSub, deadlines, deadlinesSub, evictExpired, get, has, initCache, nextTimer, remove, set, setTimeoutArmTimer, subscribeDeadline, subscribeWith } from '../chunk-QIE4Y6BK.js';
4
4
  import { defaultRng, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-VBDU4JL6.js';
5
5
  import { unchecked } from '../chunk-4LPY67E4.js';
6
- import '../chunk-FNGZMVIT.js';
7
- import { Cmd, without } from '../chunk-AIIXV4XB.js';
6
+ import '../chunk-VHKDUBXC.js';
7
+ import { Cmd, without } from '../chunk-GAVLND5C.js';
8
8
 
9
9
  // src/internal/resilience/token-refresh/index.ts
10
10
  function initTokenRefresh() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/internal/resilience/token-refresh/index.ts","../../src/internal/resilience/authed-call/index.ts","../../src/internal/resilience/retry-to-success/index.ts"],"names":["deadlines"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * internal/resilience/token-refresh — a credential's lifecycle as pure state + verbs,\n * with the actual token fetch deferred to a single injected port.\n *\n * This is the L1 brick `authed-call` (#10 in the composition specs) stacks on:\n * a token slice `{ token, stale }` plus pure transitions that decide *when* a\n * credential must be re-minted, and one `Cmd.define`d Cmd (`refresh_token`)\n * that asks for the re-mint. You write that Cmd's handler in your engine's\n * style (ADR 0021); the knob ships no I/O.\n *\n * It is a LEAF module in the same mold as `@demlik/tea/retry-backoff`: it\n * depends only on the core (`../index`), never on a sibling brick. The two\n * non-negotiables of every knob hold here by construction:\n *\n * - **Durable** — the token slice is plain JSON-serializable data, so it\n * survives a Durable Object eviction / page reload. A rehydrated machine\n * resumes knowing exactly which token it holds and whether it is stale.\n * - **Replayable** — every transition is a pure verb returning\n * `readonly [State, Cmd[]]`. No verb reads the wall clock or performs I/O,\n * so `replay` reconstructs the slice exactly. Time enters ONLY as an `at`\n * parameter the caller stamps; the network fetch enters ONLY in the\n * `refresh_token` handler you write. This keeps the substrate's purity invariants intact\n * (invariant 2: pure transitions; invariant 4: I/O is an effect, not a\n * reducer body).\n *\n * ## The staleness model — two independent reasons to refresh\n *\n * A token becomes unusable for two unrelated reasons, and the slice tracks both\n * without conflating them:\n *\n * 1. **Expiry** — the token has a known `expiresAt` (epoch ms). It is \"near\n * expiry\" once `at >= expiresAt - skewMs`. The `skewMs` skew refreshes\n * *ahead* of the hard deadline so an in-flight call never races a token\n * that expires mid-request. This is the proactive, time-driven reason and\n * is computed purely from the injected `at` — no clock in the verb.\n *\n * 2. **Server rejection (401)** — the server rejected a request the client\n * believed was still valid (clock skew, a server-side revocation, an early\n * rotation). `on401` flips a `stale` flag regardless of `expiresAt`. This\n * is the reactive reason: the client's belief about validity was wrong, so\n * the next `needsRefresh` returns true even though the clock says the\n * token is fine.\n *\n * `needsRefresh` is the union of the two (plus \"no token at all\"). One source\n * of truth for \"should I fetch a fresh credential before the next call?\", read\n * by the consumer's reducer at the call boundary.\n *\n * Typical wiring (inside a consumer machine):\n *\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n *\n * // in init:\n * init: (loaded) => [loaded ?? { auth: tr.init() }, Cmd.none],\n *\n * // in a reducer cell that is about to make an authed call:\n * before_call: (s, msg) => {\n * const [auth, cmds] = tr.ensureFresh(s.auth, msg.at);\n * return [{ ...s, auth }, cmds]; // emits a refresh Cmd iff a refresh is due\n * },\n *\n * // on a 401 from a guarded call:\n * got_401: (s) => [{ ...s, auth: tr.on401(s.auth) }, []],\n *\n * // when the refresh lands (the engine mints `refresh_token_ok`):\n * refresh_token_ok: (s, msg) => [{ ...s, auth: tr.refreshed(s.auth, msg.value) }, []],\n *\n * // and where the machine runs, the handler you write for the Cmd:\n * run(machine, {\n * interpret: {\n * refresh_token: async (_cmd, { ok, err }) => {\n * try { return ok(await mySdk.mintToken()); }\n * catch (cause) { return err({ _tag: \"token_refresh_failed\", cause }); }\n * },\n * },\n * });\n *\n * NOT a substrate primitive: from `../index` it reuses only `Cmd.define` — and\n * no sibling brick. It is a leaf utility internal to the package\n * (`src/internal/resilience/`, #46).\n */\n\nimport {\n Cmd,\n type CmdOf,\n type SettledErr,\n type SettledOk,\n type TaggedError,\n} from \"../../../index\";\nimport { unchecked } from \"../../schema\";\n\n/**\n * A minted credential: the opaque `value` to send on the wire, and the absolute\n * `expiresAt` (epoch milliseconds — the `Date.now()` scale) the issuer stamped\n * it with. Both fields are plain data so a `Token` is trivially serializable\n * into a consumer's `Store<S>`.\n *\n * `value` is `string` because that is the universal shape of a bearer token /\n * API key / session cookie on the wire. A consumer whose refresh port yields a\n * richer object can JSON-encode it into `value`, or keep this module's slice as\n * the \"is it fresh?\" gate and hold the richer object elsewhere in their Model.\n */\nexport interface Token {\n /** The opaque credential to present on each request (bearer token, API key). */\n readonly value: string;\n /** Absolute expiry instant, epoch milliseconds. The `Date.now()` scale. */\n readonly expiresAt: number;\n}\n\n/**\n * Pure configuration — the knob's only field. Carries no mutable state;\n * `TokenState` does. One config is shared across all the refreshes of one\n * logical credential (often a module-scope const).\n */\nexport interface TokenRefreshConfig {\n /**\n * Refresh-ahead skew, in milliseconds. A token is treated as needing a\n * refresh once `at >= expiresAt - skewMs`, i.e. `skewMs` BEFORE its hard\n * expiry. This buys headroom so a call that starts just under the wire never\n * races a token that lapses mid-flight, and absorbs small client/server clock\n * disagreement. `0` means \"refresh exactly at expiry\"; a negative value is\n * clamped to `0` (a skew that pushes the trigger past expiry is meaningless).\n *\n * @default 0\n */\n readonly skewMs?: number;\n}\n\n/**\n * The token slice this knob owns — the Model field a consumer spreads in via\n * `init()`. A two-field record rather than a phase union because the two\n * dimensions are orthogonal: a token can be present-and-fresh, present-but-stale\n * (a 401 came back), absent, or absent-and-stale (cleared after a failed\n * refresh). Every combination is meaningful, so neither field implies the\n * other's value and a discriminated union would only manufacture impossible\n * arms.\n *\n * - `token` — the currently held credential, or `null` when none has been\n * minted yet (fresh boot) or the last refresh failed.\n * - `stale` — set by `on401` when the server rejected a request the client\n * believed valid. Forces the next `needsRefresh` to true regardless of\n * `expiresAt`; cleared by `refreshed` once a new credential lands.\n *\n * Both fields `readonly` so the whole slice threads safely through a pure\n * reducer and serializes into a `Store<S>`.\n */\nexport interface TokenState {\n /** The held credential, or `null` if none is available (boot / failed refresh). */\n readonly token: Token | null;\n /** Whether a 401 has marked the held token unusable ahead of its `expiresAt`. */\n readonly stale: boolean;\n}\n\n/**\n * The starting slice: no token held, not stale. The first `needsRefresh` after\n * this returns `true` (no token), so a consumer's first guarded call mints a\n * credential before going out.\n */\nexport function initTokenRefresh(): TokenState {\n return { token: null, stale: false };\n}\n\n/**\n * The Cmd this knob emits to ask the runtime to mint a fresh token. Plain data\n * (no closure — invariant 1) carrying nothing: the refresh takes no input, and\n * the slice the result installs is read off the settled Msg, not this Cmd.\n * List it in the machine's `cmds`; its handler returns `ok(token)` or\n * `err({ _tag: \"token_refresh_failed\", … })`, and the engine mints\n * `refresh_token_ok` / `refresh_token_err`.\n */\nexport const refreshToken = Cmd.define(\"refresh_token\", {\n input: unchecked<Record<string, never>>(),\n ok: unchecked<Token>(),\n err: [\"token_refresh_failed\"],\n});\nexport type RefreshTokenCmd = CmdOf<typeof refreshToken>;\n\n/** Construct a `refresh_token` Cmd. */\nexport function refreshTokenCmd(): RefreshTokenCmd {\n return refreshToken({});\n}\n\n/**\n * The Msg the engine mints when the `refresh_token` handler returns\n * `ok(token)`: `value` is the freshly minted `Token`. Fold it with\n * `refreshed(state, msg.value)`.\n */\nexport type TokenRefreshedMsg = SettledOk<\n \"refresh_token\",\n RefreshTokenCmd,\n Token\n>;\n\n/**\n * The Msg the engine mints when the handler returns\n * `err({ _tag: \"token_refresh_failed\", … })` — errors are data, never\n * swallowed. The slice is left untouched by a failed refresh: the held token\n * (if any) stays held and stale stays set, so the consumer can retry; clearing\n * it is the consumer's policy, expressed in their reducer.\n */\nexport type TokenRefreshFailedMsg = SettledErr<\n \"refresh_token\",\n RefreshTokenCmd,\n TaggedError<\"token_refresh_failed\">\n>;\n\n/** The two Msgs a refresh can settle with. Union for a consumer's Msg type. */\nexport type TokenRefreshMsg = TokenRefreshedMsg | TokenRefreshFailedMsg;\n\n/**\n * The knob: hand it a config, get back the slice initializer and the pure\n * verbs, plus the `refresh_token` Cmd def (`run`) to list in `cmds`.\n *\n * @example\n * ```ts\n * import { createTokenRefresh } from \"@demlik/tea/resilience\";\n *\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n * const s0 = tr.init();\n * const [s1, cmds] = tr.ensureFresh(s0, Date.now()); // cmds = [refreshTokenCmd()]\n * ```\n */\nexport function createTokenRefresh(config: TokenRefreshConfig = {}) {\n // Clamp once at construction: a negative skew would push the refresh trigger\n // PAST expiry, which inverts the knob's intent (\"refresh ahead\"). NaN (a\n // miswired config) collapses to 0 so `needsRefresh`'s comparison stays total.\n const skewMs =\n config.skewMs !== undefined && config.skewMs > 0 ? config.skewMs : 0;\n\n /**\n * Whether the held token must be refreshed before the next guarded call, at\n * the injected instant `at` (epoch ms). PURE — reads only, no clock, no\n * mutation. True iff ANY of:\n *\n * - no token is held (`token === null`) — nothing to present;\n * - the token was marked `stale` by a 401 — the server disagrees with the\n * client's belief that it is valid;\n * - the token's `expiresAt` is not a finite number (`NaN` / `±Infinity`) —\n * a miswired issuer or a corrupted slice; we cannot reason about when it\n * lapses, so we fail SAFE toward minting a fresh one rather than letting\n * `at >= NaN` (always `false`) or `at >= Infinity` (always `false`) mask\n * a broken expiry as \"fresh forever\";\n * - the token is at or past its skewed expiry (`at >= expiresAt - skewMs`)\n * — the proactive refresh-ahead window has opened.\n *\n * The expiry cutoff is half-open (`>=`), matching the window edges in\n * `rate-limit` / `cache` / `circuit-breaker`: a token minted to expire at `t`\n * with zero skew is considered due at exactly `t`.\n */\n function needsRefresh(state: TokenState, at: number): boolean {\n if (state.token === null) return true;\n if (state.stale) return true;\n // Non-finite expiry (NaN/±Infinity) can never be compared into a \"due\"\n // verdict — `at >= NaN` and `at >= Infinity` are both `false`, which would\n // pin a broken token as fresh forever. Fail safe: treat it as needing a\n // refresh so the next call mints a credential with a real deadline.\n if (!Number.isFinite(state.token.expiresAt)) return true;\n return at >= state.token.expiresAt - skewMs;\n }\n\n /**\n * Mark the held token unusable because a request the client believed valid\n * came back 401. PURE — returns a new slice; the input is never mutated.\n * Sets `stale` regardless of `expiresAt`, so the next `needsRefresh` returns\n * true even when the clock still says the token is fine. The token VALUE is\n * kept (not nulled) — it is the consumer's last-known credential until a\n * refresh replaces it, and keeping it lets observability show what was\n * rejected.\n *\n * Idempotent: a second 401 on an already-stale slice returns the same shape.\n * A 401 with no token held is a no-op (already needs a refresh).\n */\n function on401(state: TokenState): TokenState {\n if (state.stale) return state;\n return { ...state, stale: true };\n }\n\n /**\n * Install a freshly minted `token`, clearing `stale`. PURE — returns a new\n * slice; the input is never mutated. This is the fold for the\n * `token_refreshed` Msg: after it, `needsRefresh(state, at)` is false for any\n * `at` below the new skewed expiry, and the stale flag is gone (the new\n * credential supersedes whatever the 401 rejected).\n *\n * This is the slice's WRITE boundary for a credential, so it is where the\n * durability invariant is paid for: it normalizes a negative-zero `expiresAt`\n * to `+0` before the value enters the slice. `-0` is the one finite double\n * JSON cannot round-trip — `JSON.stringify(-0)` is `\"0\"`, which parses back as\n * `+0`, so a slice carrying `-0` would diverge from its persisted form across\n * a Durable Object eviction / page reload (invariant: the boundary parses, the\n * core trusts). `-0` and `+0` denote the same epoch-ms instant, so collapsing\n * them loses no information and makes the un-round-trippable state\n * unrepresentable in the slice by construction.\n */\n function refreshed(state: TokenState, token: Token): TokenState {\n const normalized: Token = Object.is(token.expiresAt, -0)\n ? { ...token, expiresAt: 0 }\n : token;\n return { ...state, token: normalized, stale: false };\n }\n\n /**\n * The verb a consumer calls at the call boundary: \"make sure I have a fresh\n * credential before the next request.\" PURE — returns `[next, cmds]`. When a\n * refresh is due (`needsRefresh(state, at)`), emits a single `refresh_token`\n * Cmd; otherwise emits none. The slice is returned UNCHANGED in both branches\n * — the refresh itself is async and lands later via `refreshed`, so there is\n * no in-flight phase to record here. (A consumer that wants single-flight —\n * \"don't fire a second refresh while one is pending\" — layers that in their\n * own reducer, e.g. by tracking a pending flag; this brick stays minimal and\n * lets the consumer own that policy.)\n *\n * The `at` is the caller's stamped clock reading (epoch ms), never read here —\n * keeping the verb pure and `replay`-able.\n */\n function ensureFresh(\n state: TokenState,\n at: number,\n ): readonly [TokenState, readonly RefreshTokenCmd[]] {\n if (needsRefresh(state, at)) {\n return [state, [refreshTokenCmd()]];\n }\n return [state, []];\n }\n\n return {\n /** The `refresh_token` Cmd def — list it in the machine's `cmds`. */\n run: refreshToken,\n /** The slice initializer — spread into the consumer's `init`. */\n init: initTokenRefresh,\n needsRefresh,\n on401,\n refreshed,\n ensureFresh,\n };\n}\n\n/** The shape `createTokenRefresh` returns — useful for typing a held knob. */\nexport type TokenRefresh = ReturnType<typeof createTokenRefresh>;\n","/**\n * internal/resilience/authed-call — resilient-call with one extra dimension: a bearer\n * credential that must be minted before a call goes out and re-minted when the\n * server says it is no longer good (a 401). It is composition #10 in the specs:\n * *extends resilient-call; needs the L1 `token-refresh` brick*.\n *\n * The recipe — two existing bricks, stacked, nothing hand-wired:\n *\n * - `../resilient-call` owns ALL the call resilience: cache → circuit-breaker\n * → rate-limit → exponential-backoff retry, plus the per-call deadline. This\n * knob does not re-implement a single line of it. The `attempt` / `succeed`\n * / `fail` / `onTimer` verbs here are thin delegations that thread the\n * composed slice's `resilience` field through the resilient-call verb —\n * `succeed` and `fail` both through its one `settle`. Whatever resilience config a consumer passes flows straight\n * down — every `ResilientConfig` field is still optional, still \"omit a\n * brick → omit its gate\".\n *\n * - `../token-refresh` owns the credential lifecycle: the `{ token, stale }`\n * slice and the decisions about *when* a token must be refreshed (expiry\n * skew, or a reactive 401). This knob adds the one verb resilient-call has\n * no concept of — `on401` — and asks for a fresh token with token-refresh's\n * `refresh_token` Cmd.\n *\n * So the slice is exactly *resilient-call's slice + token-refresh's slice*:\n *\n * { resilience: ResilientState<I,R>, auth: TokenState }\n *\n * and the verb set is *resilient-call's four verbs + `on401`*, precisely the\n * spec's \"resilient-call's + `on401(s)` (refresh → retry once)\".\n *\n * ## The 401 dance — \"refresh → retry once\"\n *\n * A 401 is special: it is not a transient failure (backing off and retrying the\n * SAME credential just earns another 401), and it is not a terminal failure (a\n * fresh token usually fixes it). So `on401(s, key, at)` does three things:\n *\n * 1. marks the held token `stale` (via token-refresh's `on401`) and emits a\n * `refresh_token` Cmd to mint a new one;\n * 2. parks the in-flight call in a per-key `authRetry` budget so that, *once*,\n * the refresh landing re-issues that call with the new credential;\n * 3. caps that to a SINGLE auth-retry per call. A second 401 on the same call\n * after we already refreshed and retried does NOT loop — it settles the\n * call `failed` (errors are data: the failure is recorded, never swallowed\n * or spun on forever). The budget resets on the next fresh `attempt`.\n *\n * When the refresh resolves, the consumer folds `refreshed` into the auth slice\n * and calls `onRefreshed(s, at)`, which re-attempts every parked call through\n * the resilient gate with the new token in hand.\n *\n * ## The two non-negotiables (canon — inherited from both bricks)\n *\n * - **Durable** — the slice is `ResilientState` (plain data) + `TokenState`\n * (plain data) + a plain `Record<string, number>` retry budget. All JSON-\n * serializable, so it survives a Durable Object eviction / reload.\n * - **Replayable** — every verb returns `readonly [State, Cmd[]]` and never\n * reads the wall clock or RNG. Time arrives as an `at` parameter; the RNG\n * for retry jitter is injected once at `createAuthedCall(config, rng)` and\n * handed straight to the resilient-call knob. The knob ships no I/O: the\n * call and the token mint are two `Cmd.define`d Cmds whose handlers you\n * write (ADR 0021).\n *\n * ## Typical wiring\n *\n * const ac = createAuthedCall<string, Resp>({\n * retry: defaultRetryPolicy,\n * circuit: { threshold: 5, cooldownMs: 30_000 },\n * deadline: { ms: 5_000 },\n * skewMs: 30_000,\n * });\n *\n * // in the machine:\n * cmds: [ac.run, ac.refresh],\n * init: () => [ac.init(), []],\n * update: {\n * fetch: (s, m) => ac.attempt(s, m.key, m.input, m.at),\n * resilient_run_ok: (s, m) => ac.succeed(s, m),\n * resilient_run_err: (s, m) => ac.fail(s, m),\n * got_401: (s, m) => ac.on401(s, m.key, m.at),\n * refresh_token_ok: (s, m) => ac.onRefreshed(ac.installToken(s, m.value), m.at),\n * refresh_token_err: (s) => [s, []],\n * deadline_exceeded: (s, m) => ac.onTimer(s, m),\n * },\n * subs: [{ type: \"timer\", deps: (s) => ac.timer(s) }],\n *\n * // and where it runs — the two handlers you write:\n * run(machine, {\n * interpret: {\n * resilient_run: async (cmd, { ok, err }) => …, // the call itself\n * refresh_token: async (_cmd, { ok, err }) => …, // mint a Token\n * },\n * });\n */\n\nimport type { Cmd } from \"../../../index\";\nimport type { MsgType } from \"../../../protocol\";\nimport { without } from \"../../../pure/core\";\nimport type { DeadlineSub } from \"../deadline\";\nimport {\n createResilientCall,\n type FailMsg,\n type ResilientConfig,\n type ResilientState,\n type ResilientTimerMsg,\n type RunCmd,\n type SucceedMsg,\n} from \"../resilient-call\";\nimport {\n createTokenRefresh,\n type RefreshTokenCmd,\n refreshTokenCmd,\n type Token,\n type TokenRefreshConfig,\n type TokenRefreshMsg,\n type TokenState,\n} from \"../token-refresh\";\n\n// ===========================================================================\n// Config — the knob. The resilient-call config PLUS token-refresh's skew.\n// ===========================================================================\n\n/**\n * The authed-call knob. It is `ResilientConfig` widened by the token dimension:\n *\n * - `skewMs` — token-refresh's refresh-ahead skew (optional, defaults to 0).\n * - every `ResilientConfig` field (`retry` / `circuit` / `rateLimit` /\n * `cache` / `deadline`) — passed straight through to the resilient-call\n * knob, each still optional, each still \"omit a brick → omit its gate\".\n *\n * The token mint is not on the config: it is the `refresh_token` Cmd, and its\n * handler is yours.\n */\nexport type AuthedConfig = ResilientConfig & TokenRefreshConfig;\n\n// ===========================================================================\n// Slice — resilient-call's slice + token-refresh's slice + the auth-retry\n// budget. A flat record, every field plain data.\n// ===========================================================================\n\n/**\n * The composed slice. Three plain-data fields, no closures:\n *\n * - `resilience` — resilient-call's entire slice (circuit, bucket, retry,\n * cache, calls). Owned and mutated only via resilient-call's verbs.\n * - `auth` — token-refresh's `{ token, stale }` slice. Owned and mutated only\n * via token-refresh's verbs.\n * - `authRetry` — per-key count of auth-retries (401 → refresh → retry) spent\n * on a call. Caps the dance at one retry per call so a credential the server\n * keeps rejecting cannot spin forever. A key absent from the record has\n * spent zero. Cleared when a call settles or a fresh `attempt` starts.\n *\n * `pendingAuthRetry` carries the inputs of calls parked waiting for the refresh\n * to land, keyed so `onRefreshed` can re-issue exactly those calls. Plain data —\n * `input` is the same payload the call was issued with.\n */\nexport interface AuthedState<I, R> {\n readonly resilience: ResilientState<I, R>;\n readonly auth: TokenState;\n readonly authRetry: Readonly<Record<string, number>>;\n readonly pendingAuthRetry: Readonly<Record<string, I>>;\n}\n\n// ===========================================================================\n// Cmds + Msgs. The union of both bricks' Cmds/Msgs plus a 401 trigger Msg.\n// ===========================================================================\n\n/** The two effects this knob emits: resilient-call's run, token-refresh's mint. */\nexport type AuthedCmd<I> = RunCmd<I> | RefreshTokenCmd;\n\n/**\n * Every Msg the knob's verbs fold. Resilient-call's settle + timer Msgs, token-\n * refresh's refresh-result Msgs, and the 401 trigger the consumer dispatches\n * when a guarded call comes back unauthorized.\n */\nexport type Unauthorized = {\n readonly type: typeof MsgType.Unauthorized;\n readonly key: string;\n readonly at: number;\n};\n\n/**\n * The plain-data error a 401 we cannot fix settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (an Error object round-trips to `{}` under JSON and carries a stack trace,\n * both of which break the slice's durability invariant once it is persisted\n * into a `Store<S>` and reloaded). Mirrors resilient-call's `DeadlineExceededError`\n * sentinel discipline. `key` echoes the call that was rejected so a consumer can\n * attribute the failure.\n */\nexport type UnauthorizedError = {\n readonly _tag: \"unauthorized\";\n readonly key: string;\n};\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build an authed-call knob from `config`. `rng` is injected for retry jitter\n * and threaded straight into the underlying resilient-call knob — pass a fixed\n * `() => 0.5` in tests to pin backoff; defaults to `Math.random` (read only at\n * the verb boundary inside resilient-call, never in a brick op).\n *\n * Returns the uniform L2 knob contract, widened by the auth verbs (`on401`,\n * `onRefreshed`, `installToken`). `I` is the port input type, `R` the result.\n */\nexport function createAuthedCall<I, R>(\n config: AuthedConfig = {},\n rng: () => number = Math.random,\n) {\n // Delegate ALL call resilience to the root composition. The resilient config\n // is exactly `config` minus the two auth-only fields — but since\n // `ResilientConfig` only reads the brick fields it knows, passing the wider\n // `config` is harmless. We pass it directly so future resilient bricks flow\n // through without a re-spread.\n const rc = createResilientCall<I, R>(config, rng);\n // Delegate the credential lifecycle to the L1 token brick.\n const tr = createTokenRefresh({ skewMs: config.skewMs });\n\n /** The starting slice — both bricks' inits plus empty auth-retry bookkeeping. */\n function init(): AuthedState<I, R> {\n return {\n resilience: rc.init(),\n auth: tr.init(),\n authRetry: {},\n pendingAuthRetry: {},\n };\n }\n\n // Rebuild the host record after a resilient-call verb, preserving the auth\n // dimension. The single seam where a `[ResilientState, Cmd[]]` becomes a\n // `[AuthedState, Cmd[]]`.\n function liftResilient(\n s: AuthedState<I, R>,\n [resilience, cmds]: readonly [ResilientState<I, R>, readonly RunCmd<I>[]],\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n return [{ ...s, resilience }, cmds];\n }\n\n // Forget a key's auth bookkeeping: the 401 budget (`authRetry`) and any\n // parked input (`pendingAuthRetry`) that a settled or restarted call must\n // clear TOGETHER. These two records form a coupled invariant — a call that\n // settles, restarts, or gives up on its 401 dance drops both — so the joint\n // clear lives in one place rather than being open-coded at each site.\n function clearAuthKey(s: AuthedState<I, R>, key: string): AuthedState<I, R> {\n return {\n ...s,\n authRetry: without(s.authRetry, key),\n pendingAuthRetry: without(s.pendingAuthRetry, key),\n };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key`. Delegates the gate entirely to\n * resilient-call. A fresh attempt resets this key's auth-retry budget (a new\n * logical call gets its own single 401 retry) and drops any stale parked\n * input. PURE — `at` is the only clock.\n *\n * Note this knob does NOT proactively refresh on expiry inside `attempt`: the\n * expiry-driven refresh is the consumer's call-boundary concern (token-refresh\n * exposes `ensureFresh` for exactly that, re-exported below). `attempt` owns\n * the *reactive* path — it issues the call, and a 401 coming back triggers the\n * refresh. Keeping the two seams separate means a consumer who wants only\n * reactive auth (no proactive refresh) wires nothing extra.\n */\n function attempt(\n s: AuthedState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const cleared = clearAuthKey(s, key);\n return liftResilient(\n cleared,\n rc.attempt(cleared.resilience, key, input, at),\n );\n }\n\n // === Verb: succeed =======================================================\n\n /**\n * Record a success for `msg.cmd.key`: delegate to resilient-call (closes the\n * breaker, fills the cache, resets retry) and clear this key's auth-retry\n * bookkeeping — the call settled, so its 401 budget and any parked input are\n * forgotten. PURE.\n */\n function succeed(\n s: AuthedState<I, R>,\n msg: SucceedMsg<R>,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const settled = clearAuthKey(s, msg.cmd.key);\n const { call, cmds } = rc.settle(settled.resilience, msg);\n return liftResilient(settled, [call, cmds]);\n }\n\n // === Verb: fail ==========================================================\n\n /**\n * Record a NON-auth failure for `key` (a 5xx, a network drop — anything that\n * is not a 401). Delegate to resilient-call's `settle`, which trips the breaker\n * and backs off / settles per the retry policy. The auth dimension is\n * untouched: a generic failure says nothing about the credential. PURE.\n *\n * A 401 is NOT routed here — the consumer dispatches `unauthorized` and the\n * machine calls `on401` instead. Routing a 401 through `fail` would burn the\n * resilient retry budget on a credential that a plain re-issue cannot fix.\n */\n function fail(\n s: AuthedState<I, R>,\n msg: FailMsg,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n // If the resilient verb settles this call, forget its auth bookkeeping too.\n const { call: resilience, cmds } = rc.settle(s.resilience, msg);\n const next = settleAuthIfDone(\n { ...s, resilience },\n msg.cmd.key,\n resilience,\n );\n return [next, cmds];\n }\n\n // === Verb: on401 =========================================================\n\n /**\n * The auth dimension's one verb: the server rejected `key`'s call as\n * unauthorized. \"Refresh → retry once\":\n *\n * - First 401 on this call → mark the token stale, emit a `refresh_token`\n * Cmd, and PARK the call's input so the refresh landing re-issues it. The\n * resilient call phase is left as-is (still `running`, or `waiting_retry`\n * if a transient failure had already backed it off); the in-flight\n * resilient effect is abandoned in favour of the post-refresh re-issue.\n * - Second 401 (the budget is already spent) → settle the call `failed`\n * with the unauthorized error. No second refresh, no loop. Errors are\n * data: the failure is recorded on the resilient slice, visible to the\n * consumer.\n *\n * A 401 is NOT a downstream-health signal — a credential the server rejected\n * says nothing about the backend's health. So the terminal path routes through\n * resilient-call's `settleFailed` (which settles `failed` WITHOUT tripping the\n * shared breaker or advancing this key's retry counter), never `fail` (which\n * would punish a healthy target and burn the retry budget on a request that a\n * plain re-issue cannot fix).\n *\n * PURE — `at` is the instant a terminal 401 settles the call at.\n */\n function on401(\n s: AuthedState<I, R>,\n key: string,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const spent = s.authRetry[key] ?? 0;\n const call = s.resilience.calls[key];\n // A 401 can arrive while the call is `running` OR after a transient failure\n // backed it off to `waiting_retry`; both hold the input needed to re-issue.\n // Reading only `running` would drop a 401 that races a backoff — the call\n // would never refresh.\n const input =\n call?.phase === \"running\" || call?.phase === \"waiting_retry\"\n ? call.input\n : undefined;\n\n // Budget exhausted (already refreshed + retried once), or nothing to\n // re-issue: settle the call failed with the unauthorized error. Through\n // `settleFailed`, NOT `fail` — a 401 is not a breaker / retry signal.\n if (spent >= 1 || input === undefined) {\n const error: UnauthorizedError = { _tag: \"unauthorized\", key };\n const [settled] = rc.settleFailed(s.resilience, key, error, at);\n return [{ ...clearAuthKey(s, key), resilience: settled }, []];\n }\n\n // First 401: mark stale, park the call, ask for a fresh token.\n return [\n {\n ...s,\n auth: tr.on401(s.auth),\n authRetry: { ...s.authRetry, [key]: spent + 1 },\n pendingAuthRetry: { ...s.pendingAuthRetry, [key]: input },\n },\n [refreshTokenCmd()],\n ];\n }\n\n // === Verb: installToken ==================================================\n\n /**\n * Fold a freshly minted `token` into the auth slice (delegates to token-\n * refresh's `refreshed`: installs the token, clears `stale`). PURE. Call this\n * from the `refresh_token_ok` reducer cell, then chain `onRefreshed` to re-fire\n * the parked calls. Split from `onRefreshed` so a consumer can install a token\n * without auto-retrying (e.g. a proactive expiry refresh with nothing parked).\n */\n function installToken(s: AuthedState<I, R>, token: Token): AuthedState<I, R> {\n return { ...s, auth: tr.refreshed(s.auth, token) };\n }\n\n // === Verb: onRefreshed ===================================================\n\n /**\n * Re-issue every call parked by a 401 now that a fresh token has landed. Runs\n * each parked input back through resilient-call's `attempt` gate (so the\n * circuit / rate-limit / cache are all re-checked against the new `at`),\n * clears the parked set, and accumulates the run Cmds. The auth-retry COUNT is\n * preserved (each re-issued call has already spent one auth-retry, so a second\n * 401 on it settles failed). PURE — `at` is the gate's clock.\n *\n * No parked calls → a no-op that returns the slice unchanged by reference, so\n * a proactive (expiry) refresh with nothing in flight costs nothing.\n */\n function onRefreshed(\n s: AuthedState<I, R>,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const parked = Object.entries(s.pendingAuthRetry);\n if (parked.length === 0) return [s, []];\n\n let resilience = s.resilience;\n const cmds: AuthedCmd<I>[] = [];\n for (const [key, input] of parked) {\n const [next, runCmds] = rc.attempt(resilience, key, input, at);\n resilience = next;\n cmds.push(...runCmds);\n }\n return [{ ...s, resilience, pendingAuthRetry: {} }, cmds];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A retry / deadline timer fired — delegate straight to resilient-call. If the\n * delegated verb settled the call, forget its auth bookkeeping. PURE.\n */\n function onTimer(\n s: AuthedState<I, R>,\n msg: ResilientTimerMsg | { readonly id: string; readonly atMs: number },\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const [resilience, cmds] = rc.onTimer(s.resilience, msg);\n const next = settleAuthIfDone({ ...s, resilience }, undefined, resilience);\n return [next, cmds];\n }\n\n // --- helpers --------------------------------------------------------------\n\n // After a resilient verb, drop auth bookkeeping for any key the resilient\n // slice now reports as settled (succeeded / failed). `onlyKey` narrows the\n // scan to one key when the verb targeted one (fail); `undefined` scans all\n // (onTimer can settle a deadline on any key).\n function settleAuthIfDone(\n s: AuthedState<I, R>,\n onlyKey: string | undefined,\n resilience: ResilientState<I, R>,\n ): AuthedState<I, R> {\n const keys = onlyKey !== undefined ? [onlyKey] : Object.keys(s.authRetry);\n let next = s;\n for (const key of keys) {\n const phase = resilience.calls[key]?.phase;\n if (phase === \"succeeded\" || phase === \"failed\") {\n next = clearAuthKey(next, key);\n }\n }\n return next;\n }\n\n // === Timers ==============================================================\n\n /**\n * The deadlines this slice waits on — exactly resilient-call's (retry +\n * deadline timers keyed per call). The auth dimension arms NO timers: a\n * refresh is a one-shot Cmd, and the parked-call re-issue is driven by the\n * `refresh_token_ok` Msg, not a timer.\n */\n function deadlines(s: AuthedState<I, R>): readonly DeadlineSub[] {\n return rc.deadlines(s.resilience);\n }\n\n /**\n * The built-in `timer` Sub's deps: resilient-call's `timer` over the\n * `resilience` field. Declare `{ type: \"timer\", deps: (s) => ac.timer(s) }`.\n */\n function timer(s: AuthedState<I, R>) {\n return rc.timer(s.resilience);\n }\n\n return {\n /** The call's `Cmd.define`d run Cmd — list it in `cmds`. */\n run: rc.run,\n /** token-refresh's `refresh_token` Cmd def — list it in `cmds`. */\n refresh: tr.run,\n init,\n attempt,\n succeed,\n fail,\n on401,\n installToken,\n onRefreshed,\n onTimer,\n /** token-refresh's call-boundary verb, re-exposed for proactive expiry refresh. */\n needsRefresh: (s: AuthedState<I, R>, at: number) =>\n tr.needsRefresh(s.auth, at),\n deadlines,\n timer,\n };\n}\n\n/** The shape `createAuthedCall` returns — useful for typing a held knob. */\nexport type AuthedCall<I, R> = ReturnType<typeof createAuthedCall<I, R>>;\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.authed`. A convenience for the common single-slice host;\n * consumers with a differently-named field spread by hand. Pure — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftAuthed<\n S extends { authed: AuthedState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [AuthedState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, authed: slice }, cmds];\n}\n\n/**\n * Re-export the types a consumer wires into their reducer, so the whole\n * composition imports from one subpath.\n */\nexport type {\n ResilientState,\n TokenState,\n Token,\n TokenRefreshMsg,\n SucceedMsg,\n FailMsg,\n ResilientTimerMsg,\n RunCmd,\n};\n","/**\n * internal/resilience/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. Internal to the package\n * (`src/internal/resilience/`, #46), not a published 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
+ {"version":3,"sources":["../../src/internal/resilience/token-refresh/index.ts","../../src/internal/resilience/authed-call/index.ts","../../src/internal/resilience/retry-to-success/index.ts"],"names":["deadlines"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * internal/resilience/token-refresh — a credential's lifecycle as pure state + verbs,\n * with the actual token fetch deferred to a single injected port.\n *\n * This is the L1 brick `authed-call` (#10 in the composition specs) stacks on:\n * a token slice `{ token, stale }` plus pure transitions that decide *when* a\n * credential must be re-minted, and one `Cmd.define`d Cmd (`refresh_token`)\n * that asks for the re-mint. You write that Cmd's handler in your engine's\n * style (ADR 0021); the knob ships no I/O.\n *\n * It is a LEAF module in the same mold as `@demlik/tea/retry-backoff`: it\n * depends only on the core (`../index`), never on a sibling brick. The two\n * non-negotiables of every knob hold here by construction:\n *\n * - **Durable** — the token slice is plain JSON-serializable data, so it\n * survives a Durable Object eviction / page reload. A rehydrated machine\n * resumes knowing exactly which token it holds and whether it is stale.\n * - **Replayable** — every transition is a pure verb returning\n * `readonly [State, Cmd[]]`. No verb reads the wall clock or performs I/O,\n * so `replay` reconstructs the slice exactly. Time enters ONLY as an `at`\n * parameter the caller stamps; the network fetch enters ONLY in the\n * `refresh_token` handler you write. This keeps the substrate's purity invariants intact\n * (invariant 2: pure transitions; invariant 4: I/O is an effect, not a\n * reducer body).\n *\n * ## The staleness model — two independent reasons to refresh\n *\n * A token becomes unusable for two unrelated reasons, and the slice tracks both\n * without conflating them:\n *\n * 1. **Expiry** — the token has a known `expiresAt` (epoch ms). It is \"near\n * expiry\" once `at >= expiresAt - skewMs`. The `skewMs` skew refreshes\n * *ahead* of the hard deadline so an in-flight call never races a token\n * that expires mid-request. This is the proactive, time-driven reason and\n * is computed purely from the injected `at` — no clock in the verb.\n *\n * 2. **Server rejection (401)** — the server rejected a request the client\n * believed was still valid (clock skew, a server-side revocation, an early\n * rotation). `on401` flips a `stale` flag regardless of `expiresAt`. This\n * is the reactive reason: the client's belief about validity was wrong, so\n * the next `needsRefresh` returns true even though the clock says the\n * token is fine.\n *\n * `needsRefresh` is the union of the two (plus \"no token at all\"). One source\n * of truth for \"should I fetch a fresh credential before the next call?\", read\n * by the consumer's reducer at the call boundary.\n *\n * Typical wiring (inside a consumer machine):\n *\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n *\n * // in init:\n * init: (loaded) => [loaded ?? { auth: tr.init() }, Cmd.none],\n *\n * // in a reducer cell that is about to make an authed call:\n * before_call: (s, msg) => {\n * const [auth, cmds] = tr.ensureFresh(s.auth, msg.at);\n * return [{ ...s, auth }, cmds]; // emits a refresh Cmd iff a refresh is due\n * },\n *\n * // on a 401 from a guarded call:\n * got_401: (s) => [{ ...s, auth: tr.on401(s.auth) }, []],\n *\n * // when the refresh lands (the engine mints `refresh_token_ok`):\n * refresh_token_ok: (s, msg) => [{ ...s, auth: tr.refreshed(s.auth, msg.value) }, []],\n *\n * // and where the machine runs, the handler you write for the Cmd:\n * run(machine, {\n * interpret: {\n * refresh_token: async (_cmd, { ok, err }) => {\n * try { return ok(await mySdk.mintToken()); }\n * catch (cause) { return err({ _tag: \"token_refresh_failed\", cause }); }\n * },\n * },\n * });\n *\n * NOT a substrate primitive: from `../index` it reuses only `Cmd.define` — and\n * no sibling brick. It is a leaf utility internal to the package\n * (`src/internal/resilience/`, #46).\n */\n\nimport {\n Cmd,\n type CmdOf,\n type SettledErr,\n type SettledOk,\n type TaggedError,\n} from \"../../../index\";\nimport { unchecked } from \"../../schema\";\n\n/**\n * A minted credential: the opaque `value` to send on the wire, and the absolute\n * `expiresAt` (epoch milliseconds — the `Date.now()` scale) the issuer stamped\n * it with. Both fields are plain data so a `Token` is trivially serializable\n * into a consumer's `Store<S>`.\n *\n * `value` is `string` because that is the universal shape of a bearer token /\n * API key / session cookie on the wire. A consumer whose refresh port yields a\n * richer object can JSON-encode it into `value`, or keep this module's slice as\n * the \"is it fresh?\" gate and hold the richer object elsewhere in their Model.\n */\nexport interface Token {\n /** The opaque credential to present on each request (bearer token, API key). */\n readonly value: string;\n /** Absolute expiry instant, epoch milliseconds. The `Date.now()` scale. */\n readonly expiresAt: number;\n}\n\n/**\n * Pure configuration — the knob's only field. Carries no mutable state;\n * `TokenState` does. One config is shared across all the refreshes of one\n * logical credential (often a module-scope const).\n */\nexport interface TokenRefreshConfig {\n /**\n * Refresh-ahead skew, in milliseconds. A token is treated as needing a\n * refresh once `at >= expiresAt - skewMs`, i.e. `skewMs` BEFORE its hard\n * expiry. This buys headroom so a call that starts just under the wire never\n * races a token that lapses mid-flight, and absorbs small client/server clock\n * disagreement. `0` means \"refresh exactly at expiry\"; a negative value is\n * clamped to `0` (a skew that pushes the trigger past expiry is meaningless).\n *\n * @default 0\n */\n readonly skewMs?: number;\n}\n\n/**\n * The token slice this knob owns — the Model field a consumer spreads in via\n * `init()`. A two-field record rather than a phase union because the two\n * dimensions are orthogonal: a token can be present-and-fresh, present-but-stale\n * (a 401 came back), absent, or absent-and-stale (cleared after a failed\n * refresh). Every combination is meaningful, so neither field implies the\n * other's value and a discriminated union would only manufacture impossible\n * arms.\n *\n * - `token` — the currently held credential, or `null` when none has been\n * minted yet (fresh boot) or the last refresh failed.\n * - `stale` — set by `on401` when the server rejected a request the client\n * believed valid. Forces the next `needsRefresh` to true regardless of\n * `expiresAt`; cleared by `refreshed` once a new credential lands.\n *\n * Both fields `readonly` so the whole slice threads safely through a pure\n * reducer and serializes into a `Store<S>`.\n */\nexport interface TokenState {\n /** The held credential, or `null` if none is available (boot / failed refresh). */\n readonly token: Token | null;\n /** Whether a 401 has marked the held token unusable ahead of its `expiresAt`. */\n readonly stale: boolean;\n}\n\n/**\n * The starting slice: no token held, not stale. The first `needsRefresh` after\n * this returns `true` (no token), so a consumer's first guarded call mints a\n * credential before going out.\n */\nexport function initTokenRefresh(): TokenState {\n return { token: null, stale: false };\n}\n\n/**\n * The Cmd this knob emits to ask the runtime to mint a fresh token. Plain data\n * (no closure — invariant 1) carrying nothing: the refresh takes no input, and\n * the slice the result installs is read off the settled Msg, not this Cmd.\n * List it in the machine's `cmds`; its handler returns `ok(token)` or\n * `err({ _tag: \"token_refresh_failed\", … })`, and the engine mints\n * `refresh_token_ok` / `refresh_token_err`.\n */\nexport const refreshToken = Cmd.define(\"refresh_token\", {\n input: unchecked<Record<string, never>>(),\n ok: unchecked<Token>(),\n err: [\"token_refresh_failed\"],\n});\n/** The `refresh_token` Cmd value that `refreshTokenCmd` builds. */\nexport type RefreshTokenCmd = CmdOf<typeof refreshToken>;\n\n/** Construct a `refresh_token` Cmd. */\nexport function refreshTokenCmd(): RefreshTokenCmd {\n return refreshToken({});\n}\n\n/**\n * The Msg the engine mints when the `refresh_token` handler returns\n * `ok(token)`: `value` is the freshly minted `Token`. Fold it with\n * `refreshed(state, msg.value)`.\n */\nexport type TokenRefreshedMsg = SettledOk<\n \"refresh_token\",\n RefreshTokenCmd,\n Token\n>;\n\n/**\n * The Msg the engine mints when the handler returns\n * `err({ _tag: \"token_refresh_failed\", … })` — errors are data, never\n * swallowed. The slice is left untouched by a failed refresh: the held token\n * (if any) stays held and stale stays set, so the consumer can retry; clearing\n * it is the consumer's policy, expressed in their reducer.\n */\nexport type TokenRefreshFailedMsg = SettledErr<\n \"refresh_token\",\n RefreshTokenCmd,\n TaggedError<\"token_refresh_failed\">\n>;\n\n/** The two Msgs a refresh can settle with. Union for a consumer's Msg type. */\nexport type TokenRefreshMsg = TokenRefreshedMsg | TokenRefreshFailedMsg;\n\n/**\n * The knob: hand it a config, get back the slice initializer and the pure\n * verbs, plus the `refresh_token` Cmd def (`run`) to list in `cmds`.\n *\n * @example\n * ```ts\n * import { createTokenRefresh } from \"@demlik/tea/resilience\";\n *\n * const tr = createTokenRefresh({ skewMs: 30_000 });\n * const s0 = tr.init();\n * const [s1, cmds] = tr.ensureFresh(s0, Date.now()); // cmds = [refreshTokenCmd()]\n * ```\n */\nexport function createTokenRefresh(config: TokenRefreshConfig = {}) {\n // Clamp once at construction: a negative skew would push the refresh trigger\n // PAST expiry, which inverts the knob's intent (\"refresh ahead\"). NaN (a\n // miswired config) collapses to 0 so `needsRefresh`'s comparison stays total.\n const skewMs =\n config.skewMs !== undefined && config.skewMs > 0 ? config.skewMs : 0;\n\n /**\n * Whether the held token must be refreshed before the next guarded call, at\n * the injected instant `at` (epoch ms). PURE — reads only, no clock, no\n * mutation. True iff ANY of:\n *\n * - no token is held (`token === null`) — nothing to present;\n * - the token was marked `stale` by a 401 — the server disagrees with the\n * client's belief that it is valid;\n * - the token's `expiresAt` is not a finite number (`NaN` / `±Infinity`) —\n * a miswired issuer or a corrupted slice; we cannot reason about when it\n * lapses, so we fail SAFE toward minting a fresh one rather than letting\n * `at >= NaN` (always `false`) or `at >= Infinity` (always `false`) mask\n * a broken expiry as \"fresh forever\";\n * - the token is at or past its skewed expiry (`at >= expiresAt - skewMs`)\n * — the proactive refresh-ahead window has opened.\n *\n * The expiry cutoff is half-open (`>=`), matching the window edges in\n * `rate-limit` / `cache` / `circuit-breaker`: a token minted to expire at `t`\n * with zero skew is considered due at exactly `t`.\n */\n function needsRefresh(state: TokenState, at: number): boolean {\n if (state.token === null) return true;\n if (state.stale) return true;\n // Non-finite expiry (NaN/±Infinity) can never be compared into a \"due\"\n // verdict — `at >= NaN` and `at >= Infinity` are both `false`, which would\n // pin a broken token as fresh forever. Fail safe: treat it as needing a\n // refresh so the next call mints a credential with a real deadline.\n if (!Number.isFinite(state.token.expiresAt)) return true;\n return at >= state.token.expiresAt - skewMs;\n }\n\n /**\n * Mark the held token unusable because a request the client believed valid\n * came back 401. PURE — returns a new slice; the input is never mutated.\n * Sets `stale` regardless of `expiresAt`, so the next `needsRefresh` returns\n * true even when the clock still says the token is fine. The token VALUE is\n * kept (not nulled) — it is the consumer's last-known credential until a\n * refresh replaces it, and keeping it lets observability show what was\n * rejected.\n *\n * Idempotent: a second 401 on an already-stale slice returns the same shape.\n * A 401 with no token held is a no-op (already needs a refresh).\n */\n function on401(state: TokenState): TokenState {\n if (state.stale) return state;\n return { ...state, stale: true };\n }\n\n /**\n * Install a freshly minted `token`, clearing `stale`. PURE — returns a new\n * slice; the input is never mutated. This is the fold for the\n * `token_refreshed` Msg: after it, `needsRefresh(state, at)` is false for any\n * `at` below the new skewed expiry, and the stale flag is gone (the new\n * credential supersedes whatever the 401 rejected).\n *\n * This is the slice's WRITE boundary for a credential, so it is where the\n * durability invariant is paid for: it normalizes a negative-zero `expiresAt`\n * to `+0` before the value enters the slice. `-0` is the one finite double\n * JSON cannot round-trip — `JSON.stringify(-0)` is `\"0\"`, which parses back as\n * `+0`, so a slice carrying `-0` would diverge from its persisted form across\n * a Durable Object eviction / page reload (invariant: the boundary parses, the\n * core trusts). `-0` and `+0` denote the same epoch-ms instant, so collapsing\n * them loses no information and makes the un-round-trippable state\n * unrepresentable in the slice by construction.\n */\n function refreshed(state: TokenState, token: Token): TokenState {\n const normalized: Token = Object.is(token.expiresAt, -0)\n ? { ...token, expiresAt: 0 }\n : token;\n return { ...state, token: normalized, stale: false };\n }\n\n /**\n * The verb a consumer calls at the call boundary: \"make sure I have a fresh\n * credential before the next request.\" PURE — returns `[next, cmds]`. When a\n * refresh is due (`needsRefresh(state, at)`), emits a single `refresh_token`\n * Cmd; otherwise emits none. The slice is returned UNCHANGED in both branches\n * — the refresh itself is async and lands later via `refreshed`, so there is\n * no in-flight phase to record here. (A consumer that wants single-flight —\n * \"don't fire a second refresh while one is pending\" — layers that in their\n * own reducer, e.g. by tracking a pending flag; this brick stays minimal and\n * lets the consumer own that policy.)\n *\n * The `at` is the caller's stamped clock reading (epoch ms), never read here —\n * keeping the verb pure and `replay`-able.\n */\n function ensureFresh(\n state: TokenState,\n at: number,\n ): readonly [TokenState, readonly RefreshTokenCmd[]] {\n if (needsRefresh(state, at)) {\n return [state, [refreshTokenCmd()]];\n }\n return [state, []];\n }\n\n return {\n /** The `refresh_token` Cmd def — list it in the machine's `cmds`. */\n run: refreshToken,\n /** The slice initializer — spread into the consumer's `init`. */\n init: initTokenRefresh,\n needsRefresh,\n on401,\n refreshed,\n ensureFresh,\n };\n}\n\n/** The shape `createTokenRefresh` returns — useful for typing a held knob. */\nexport type TokenRefresh = ReturnType<typeof createTokenRefresh>;\n","/**\n * internal/resilience/authed-call — resilient-call with one extra dimension: a bearer\n * credential that must be minted before a call goes out and re-minted when the\n * server says it is no longer good (a 401). It is composition #10 in the specs:\n * *extends resilient-call; needs the L1 `token-refresh` brick*.\n *\n * The recipe — two existing bricks, stacked, nothing hand-wired:\n *\n * - `../resilient-call` owns ALL the call resilience: cache → circuit-breaker\n * → rate-limit → exponential-backoff retry, plus the per-call deadline. This\n * knob does not re-implement a single line of it. The `attempt` / `succeed`\n * / `fail` / `onTimer` verbs here are thin delegations that thread the\n * composed slice's `resilience` field through the resilient-call verb —\n * `succeed` and `fail` both through its one `settle`. Whatever resilience config a consumer passes flows straight\n * down — every `ResilientConfig` field is still optional, still \"omit a\n * brick → omit its gate\".\n *\n * - `../token-refresh` owns the credential lifecycle: the `{ token, stale }`\n * slice and the decisions about *when* a token must be refreshed (expiry\n * skew, or a reactive 401). This knob adds the one verb resilient-call has\n * no concept of — `on401` — and asks for a fresh token with token-refresh's\n * `refresh_token` Cmd.\n *\n * So the slice is exactly *resilient-call's slice + token-refresh's slice*:\n *\n * { resilience: ResilientState<I,R>, auth: TokenState }\n *\n * and the verb set is *resilient-call's four verbs + `on401`*, precisely the\n * spec's \"resilient-call's + `on401(s)` (refresh → retry once)\".\n *\n * ## The 401 dance — \"refresh → retry once\"\n *\n * A 401 is special: it is not a transient failure (backing off and retrying the\n * SAME credential just earns another 401), and it is not a terminal failure (a\n * fresh token usually fixes it). So `on401(s, key, at)` does three things:\n *\n * 1. marks the held token `stale` (via token-refresh's `on401`) and emits a\n * `refresh_token` Cmd to mint a new one;\n * 2. parks the in-flight call in a per-key `authRetry` budget so that, *once*,\n * the refresh landing re-issues that call with the new credential;\n * 3. caps that to a SINGLE auth-retry per call. A second 401 on the same call\n * after we already refreshed and retried does NOT loop — it settles the\n * call `failed` (errors are data: the failure is recorded, never swallowed\n * or spun on forever). The budget resets on the next fresh `attempt`.\n *\n * When the refresh resolves, the consumer folds `refreshed` into the auth slice\n * and calls `onRefreshed(s, at)`, which re-attempts every parked call through\n * the resilient gate with the new token in hand.\n *\n * ## The two non-negotiables (canon — inherited from both bricks)\n *\n * - **Durable** — the slice is `ResilientState` (plain data) + `TokenState`\n * (plain data) + a plain `Record<string, number>` retry budget. All JSON-\n * serializable, so it survives a Durable Object eviction / reload.\n * - **Replayable** — every verb returns `readonly [State, Cmd[]]` and never\n * reads the wall clock or RNG. Time arrives as an `at` parameter; the RNG\n * for retry jitter is injected once at `createAuthedCall(config, rng)` and\n * handed straight to the resilient-call knob. The knob ships no I/O: the\n * call and the token mint are two `Cmd.define`d Cmds whose handlers you\n * write (ADR 0021).\n *\n * ## Typical wiring\n *\n * const ac = createAuthedCall<string, Resp>({\n * retry: defaultRetryPolicy,\n * circuit: { threshold: 5, cooldownMs: 30_000 },\n * deadline: { ms: 5_000 },\n * skewMs: 30_000,\n * });\n *\n * // in the machine:\n * cmds: [ac.run, ac.refresh],\n * init: () => [ac.init(), []],\n * update: {\n * fetch: (s, m) => ac.attempt(s, m.key, m.input, m.at),\n * resilient_run_ok: (s, m) => ac.succeed(s, m),\n * resilient_run_err: (s, m) => ac.fail(s, m),\n * got_401: (s, m) => ac.on401(s, m.key, m.at),\n * refresh_token_ok: (s, m) => ac.onRefreshed(ac.installToken(s, m.value), m.at),\n * refresh_token_err: (s) => [s, []],\n * deadline_exceeded: (s, m) => ac.onTimer(s, m),\n * },\n * subs: [{ type: \"timer\", deps: (s) => ac.timer(s) }],\n *\n * // and where it runs — the two handlers you write:\n * run(machine, {\n * interpret: {\n * resilient_run: async (cmd, { ok, err }) => …, // the call itself\n * refresh_token: async (_cmd, { ok, err }) => …, // mint a Token\n * },\n * });\n */\n\nimport type { Cmd } from \"../../../index\";\nimport type { MsgType } from \"../../../protocol\";\nimport { without } from \"../../../pure/core\";\nimport type { DeadlineSub } from \"../deadline\";\nimport {\n createResilientCall,\n type FailMsg,\n type ResilientConfig,\n type ResilientState,\n type ResilientTimerMsg,\n type RunCmd,\n type SucceedMsg,\n} from \"../resilient-call\";\nimport {\n createTokenRefresh,\n type RefreshTokenCmd,\n refreshTokenCmd,\n type Token,\n type TokenRefreshConfig,\n type TokenRefreshMsg,\n type TokenState,\n} from \"../token-refresh\";\n\n// ===========================================================================\n// Config — the knob. The resilient-call config PLUS token-refresh's skew.\n// ===========================================================================\n\n/**\n * The authed-call knob. It is `ResilientConfig` widened by the token dimension:\n *\n * - `skewMs` — token-refresh's refresh-ahead skew (optional, defaults to 0).\n * - every `ResilientConfig` field (`retry` / `circuit` / `rateLimit` /\n * `cache` / `deadline`) — passed straight through to the resilient-call\n * knob, each still optional, each still \"omit a brick → omit its gate\".\n *\n * The token mint is not on the config: it is the `refresh_token` Cmd, and its\n * handler is yours.\n */\nexport type AuthedConfig = ResilientConfig & TokenRefreshConfig;\n\n// ===========================================================================\n// Slice — resilient-call's slice + token-refresh's slice + the auth-retry\n// budget. A flat record, every field plain data.\n// ===========================================================================\n\n/**\n * The composed slice. Three plain-data fields, no closures:\n *\n * - `resilience` — resilient-call's entire slice (circuit, bucket, retry,\n * cache, calls). Owned and mutated only via resilient-call's verbs.\n * - `auth` — token-refresh's `{ token, stale }` slice. Owned and mutated only\n * via token-refresh's verbs.\n * - `authRetry` — per-key count of auth-retries (401 → refresh → retry) spent\n * on a call. Caps the dance at one retry per call so a credential the server\n * keeps rejecting cannot spin forever. A key absent from the record has\n * spent zero. Cleared when a call settles or a fresh `attempt` starts.\n *\n * `pendingAuthRetry` carries the inputs of calls parked waiting for the refresh\n * to land, keyed so `onRefreshed` can re-issue exactly those calls. Plain data —\n * `input` is the same payload the call was issued with.\n */\nexport interface AuthedState<I, R> {\n readonly resilience: ResilientState<I, R>;\n readonly auth: TokenState;\n readonly authRetry: Readonly<Record<string, number>>;\n readonly pendingAuthRetry: Readonly<Record<string, I>>;\n}\n\n// ===========================================================================\n// Cmds + Msgs. The union of both bricks' Cmds/Msgs plus a 401 trigger Msg.\n// ===========================================================================\n\n/** The two effects this knob emits: resilient-call's run, token-refresh's mint. */\nexport type AuthedCmd<I> = RunCmd<I> | RefreshTokenCmd;\n\n/**\n * Every Msg the knob's verbs fold. Resilient-call's settle + timer Msgs, token-\n * refresh's refresh-result Msgs, and the 401 trigger the consumer dispatches\n * when a guarded call comes back unauthorized.\n */\nexport type Unauthorized = {\n readonly type: typeof MsgType.Unauthorized;\n readonly key: string;\n readonly at: number;\n};\n\n/**\n * The plain-data error a 401 we cannot fix settles with. A `{_tag, ...}`\n * sentinel — NEVER a `new Error(...)` — so the slice stays JSON-serializable\n * (an Error object round-trips to `{}` under JSON and carries a stack trace,\n * both of which break the slice's durability invariant once it is persisted\n * into a `Store<S>` and reloaded). Mirrors resilient-call's `DeadlineExceededError`\n * sentinel discipline. `key` echoes the call that was rejected so a consumer can\n * attribute the failure.\n */\nexport type UnauthorizedError = {\n readonly _tag: \"unauthorized\";\n readonly key: string;\n};\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build an authed-call knob from `config`. `rng` is injected for retry jitter\n * and threaded straight into the underlying resilient-call knob — pass a fixed\n * `() => 0.5` in tests to pin backoff; defaults to `Math.random` (read only at\n * the verb boundary inside resilient-call, never in a brick op).\n *\n * Returns the uniform L2 knob contract, widened by the auth verbs (`on401`,\n * `onRefreshed`, `installToken`). `I` is the port input type, `R` the result.\n */\nexport function createAuthedCall<I, R>(\n config: AuthedConfig = {},\n rng: () => number = Math.random,\n) {\n // Delegate ALL call resilience to the root composition. The resilient config\n // is exactly `config` minus the two auth-only fields — but since\n // `ResilientConfig` only reads the brick fields it knows, passing the wider\n // `config` is harmless. We pass it directly so future resilient bricks flow\n // through without a re-spread.\n const rc = createResilientCall<I, R>(config, rng);\n // Delegate the credential lifecycle to the L1 token brick.\n const tr = createTokenRefresh({ skewMs: config.skewMs });\n\n /** The starting slice — both bricks' inits plus empty auth-retry bookkeeping. */\n function init(): AuthedState<I, R> {\n return {\n resilience: rc.init(),\n auth: tr.init(),\n authRetry: {},\n pendingAuthRetry: {},\n };\n }\n\n // Rebuild the host record after a resilient-call verb, preserving the auth\n // dimension. The single seam where a `[ResilientState, Cmd[]]` becomes a\n // `[AuthedState, Cmd[]]`.\n function liftResilient(\n s: AuthedState<I, R>,\n [resilience, cmds]: readonly [ResilientState<I, R>, readonly RunCmd<I>[]],\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n return [{ ...s, resilience }, cmds];\n }\n\n // Forget a key's auth bookkeeping: the 401 budget (`authRetry`) and any\n // parked input (`pendingAuthRetry`) that a settled or restarted call must\n // clear TOGETHER. These two records form a coupled invariant — a call that\n // settles, restarts, or gives up on its 401 dance drops both — so the joint\n // clear lives in one place rather than being open-coded at each site.\n function clearAuthKey(s: AuthedState<I, R>, key: string): AuthedState<I, R> {\n return {\n ...s,\n authRetry: without(s.authRetry, key),\n pendingAuthRetry: without(s.pendingAuthRetry, key),\n };\n }\n\n // === Verb: attempt =======================================================\n\n /**\n * Start (or restart) the call for `key`. Delegates the gate entirely to\n * resilient-call. A fresh attempt resets this key's auth-retry budget (a new\n * logical call gets its own single 401 retry) and drops any stale parked\n * input. PURE — `at` is the only clock.\n *\n * Note this knob does NOT proactively refresh on expiry inside `attempt`: the\n * expiry-driven refresh is the consumer's call-boundary concern (token-refresh\n * exposes `ensureFresh` for exactly that, re-exported below). `attempt` owns\n * the *reactive* path — it issues the call, and a 401 coming back triggers the\n * refresh. Keeping the two seams separate means a consumer who wants only\n * reactive auth (no proactive refresh) wires nothing extra.\n */\n function attempt(\n s: AuthedState<I, R>,\n key: string,\n input: I,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const cleared = clearAuthKey(s, key);\n return liftResilient(\n cleared,\n rc.attempt(cleared.resilience, key, input, at),\n );\n }\n\n // === Verb: succeed =======================================================\n\n /**\n * Record a success for `msg.cmd.key`: delegate to resilient-call (closes the\n * breaker, fills the cache, resets retry) and clear this key's auth-retry\n * bookkeeping — the call settled, so its 401 budget and any parked input are\n * forgotten. PURE.\n */\n function succeed(\n s: AuthedState<I, R>,\n msg: SucceedMsg<R>,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const settled = clearAuthKey(s, msg.cmd.key);\n const { call, cmds } = rc.settle(settled.resilience, msg);\n return liftResilient(settled, [call, cmds]);\n }\n\n // === Verb: fail ==========================================================\n\n /**\n * Record a NON-auth failure for `key` (a 5xx, a network drop — anything that\n * is not a 401). Delegate to resilient-call's `settle`, which trips the breaker\n * and backs off / settles per the retry policy. The auth dimension is\n * untouched: a generic failure says nothing about the credential. PURE.\n *\n * A 401 is NOT routed here — the consumer dispatches `unauthorized` and the\n * machine calls `on401` instead. Routing a 401 through `fail` would burn the\n * resilient retry budget on a credential that a plain re-issue cannot fix.\n */\n function fail(\n s: AuthedState<I, R>,\n msg: FailMsg,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n // If the resilient verb settles this call, forget its auth bookkeeping too.\n const { call: resilience, cmds } = rc.settle(s.resilience, msg);\n const next = settleAuthIfDone(\n { ...s, resilience },\n msg.cmd.key,\n resilience,\n );\n return [next, cmds];\n }\n\n // === Verb: on401 =========================================================\n\n /**\n * The auth dimension's one verb: the server rejected `key`'s call as\n * unauthorized. \"Refresh → retry once\":\n *\n * - First 401 on this call → mark the token stale, emit a `refresh_token`\n * Cmd, and PARK the call's input so the refresh landing re-issues it. The\n * resilient call phase is left as-is (still `running`, or `waiting_retry`\n * if a transient failure had already backed it off); the in-flight\n * resilient effect is abandoned in favour of the post-refresh re-issue.\n * - Second 401 (the budget is already spent) → settle the call `failed`\n * with the unauthorized error. No second refresh, no loop. Errors are\n * data: the failure is recorded on the resilient slice, visible to the\n * consumer.\n *\n * A 401 is NOT a downstream-health signal — a credential the server rejected\n * says nothing about the backend's health. So the terminal path routes through\n * resilient-call's `settleFailed` (which settles `failed` WITHOUT tripping the\n * shared breaker or advancing this key's retry counter), never `fail` (which\n * would punish a healthy target and burn the retry budget on a request that a\n * plain re-issue cannot fix).\n *\n * PURE — `at` is the instant a terminal 401 settles the call at.\n */\n function on401(\n s: AuthedState<I, R>,\n key: string,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const spent = s.authRetry[key] ?? 0;\n const call = s.resilience.calls[key];\n // A 401 can arrive while the call is `running` OR after a transient failure\n // backed it off to `waiting_retry`; both hold the input needed to re-issue.\n // Reading only `running` would drop a 401 that races a backoff — the call\n // would never refresh.\n const input =\n call?.phase === \"running\" || call?.phase === \"waiting_retry\"\n ? call.input\n : undefined;\n\n // Budget exhausted (already refreshed + retried once), or nothing to\n // re-issue: settle the call failed with the unauthorized error. Through\n // `settleFailed`, NOT `fail` — a 401 is not a breaker / retry signal.\n if (spent >= 1 || input === undefined) {\n const error: UnauthorizedError = { _tag: \"unauthorized\", key };\n const [settled] = rc.settleFailed(s.resilience, key, error, at);\n return [{ ...clearAuthKey(s, key), resilience: settled }, []];\n }\n\n // First 401: mark stale, park the call, ask for a fresh token.\n return [\n {\n ...s,\n auth: tr.on401(s.auth),\n authRetry: { ...s.authRetry, [key]: spent + 1 },\n pendingAuthRetry: { ...s.pendingAuthRetry, [key]: input },\n },\n [refreshTokenCmd()],\n ];\n }\n\n // === Verb: installToken ==================================================\n\n /**\n * Fold a freshly minted `token` into the auth slice (delegates to token-\n * refresh's `refreshed`: installs the token, clears `stale`). PURE. Call this\n * from the `refresh_token_ok` reducer cell, then chain `onRefreshed` to re-fire\n * the parked calls. Split from `onRefreshed` so a consumer can install a token\n * without auto-retrying (e.g. a proactive expiry refresh with nothing parked).\n */\n function installToken(s: AuthedState<I, R>, token: Token): AuthedState<I, R> {\n return { ...s, auth: tr.refreshed(s.auth, token) };\n }\n\n // === Verb: onRefreshed ===================================================\n\n /**\n * Re-issue every call parked by a 401 now that a fresh token has landed. Runs\n * each parked input back through resilient-call's `attempt` gate (so the\n * circuit / rate-limit / cache are all re-checked against the new `at`),\n * clears the parked set, and accumulates the run Cmds. The auth-retry COUNT is\n * preserved (each re-issued call has already spent one auth-retry, so a second\n * 401 on it settles failed). PURE — `at` is the gate's clock.\n *\n * No parked calls → a no-op that returns the slice unchanged by reference, so\n * a proactive (expiry) refresh with nothing in flight costs nothing.\n */\n function onRefreshed(\n s: AuthedState<I, R>,\n at: number,\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const parked = Object.entries(s.pendingAuthRetry);\n if (parked.length === 0) return [s, []];\n\n let resilience = s.resilience;\n const cmds: AuthedCmd<I>[] = [];\n for (const [key, input] of parked) {\n const [next, runCmds] = rc.attempt(resilience, key, input, at);\n resilience = next;\n cmds.push(...runCmds);\n }\n return [{ ...s, resilience, pendingAuthRetry: {} }, cmds];\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A retry / deadline timer fired — delegate straight to resilient-call. If the\n * delegated verb settled the call, forget its auth bookkeeping. PURE.\n */\n function onTimer(\n s: AuthedState<I, R>,\n msg: ResilientTimerMsg | { readonly id: string; readonly atMs: number },\n ): readonly [AuthedState<I, R>, readonly AuthedCmd<I>[]] {\n const [resilience, cmds] = rc.onTimer(s.resilience, msg);\n const next = settleAuthIfDone({ ...s, resilience }, undefined, resilience);\n return [next, cmds];\n }\n\n // --- helpers --------------------------------------------------------------\n\n // After a resilient verb, drop auth bookkeeping for any key the resilient\n // slice now reports as settled (succeeded / failed). `onlyKey` narrows the\n // scan to one key when the verb targeted one (fail); `undefined` scans all\n // (onTimer can settle a deadline on any key).\n function settleAuthIfDone(\n s: AuthedState<I, R>,\n onlyKey: string | undefined,\n resilience: ResilientState<I, R>,\n ): AuthedState<I, R> {\n const keys = onlyKey !== undefined ? [onlyKey] : Object.keys(s.authRetry);\n let next = s;\n for (const key of keys) {\n const phase = resilience.calls[key]?.phase;\n if (phase === \"succeeded\" || phase === \"failed\") {\n next = clearAuthKey(next, key);\n }\n }\n return next;\n }\n\n // === Timers ==============================================================\n\n /**\n * The deadlines this slice waits on — exactly resilient-call's (retry +\n * deadline timers keyed per call). The auth dimension arms NO timers: a\n * refresh is a one-shot Cmd, and the parked-call re-issue is driven by the\n * `refresh_token_ok` Msg, not a timer.\n */\n function deadlines(s: AuthedState<I, R>): readonly DeadlineSub[] {\n return rc.deadlines(s.resilience);\n }\n\n /**\n * The built-in `timer` Sub's deps: resilient-call's `timer` over the\n * `resilience` field. Declare `{ type: \"timer\", deps: (s) => ac.timer(s) }`.\n */\n function timer(s: AuthedState<I, R>) {\n return rc.timer(s.resilience);\n }\n\n return {\n /** The call's `Cmd.define`d run Cmd — list it in `cmds`. */\n run: rc.run,\n /** token-refresh's `refresh_token` Cmd def — list it in `cmds`. */\n refresh: tr.run,\n init,\n attempt,\n succeed,\n fail,\n on401,\n installToken,\n onRefreshed,\n onTimer,\n /** token-refresh's call-boundary verb, re-exposed for proactive expiry refresh. */\n needsRefresh: (s: AuthedState<I, R>, at: number) =>\n tr.needsRefresh(s.auth, at),\n deadlines,\n timer,\n };\n}\n\n/** The shape `createAuthedCall` returns — useful for typing a held knob. */\nexport type AuthedCall<I, R> = ReturnType<typeof createAuthedCall<I, R>>;\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.authed`. A convenience for the common single-slice host;\n * consumers with a differently-named field spread by hand. Pure — a thin record\n * rebuild, no clock / RNG.\n */\nexport function liftAuthed<\n S extends { authed: AuthedState<I, R> },\n I,\n R,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [AuthedState<I, R>, readonly C[]],\n): readonly [S, readonly C[]] {\n return [{ ...state, authed: slice }, cmds];\n}\n\n/**\n * Re-export the types a consumer wires into their reducer, so the whole\n * composition imports from one subpath.\n */\nexport type {\n ResilientState,\n TokenState,\n Token,\n TokenRefreshMsg,\n SucceedMsg,\n FailMsg,\n ResilientTimerMsg,\n RunCmd,\n};\n","/**\n * internal/resilience/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. Internal to the package\n * (`src/internal/resilience/`, #46), not a published 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,8 +1,8 @@
1
+ import { Effect, Stream, Scope } from 'effect';
2
+ import { P as Port, C as Cmd, E as ErrorsOf, p as OkOfCmd, q as DeclaredErrorsOf, c as Sub, r as BuiltinSubType, B as BuiltinSub, M as Machine } from './core-BKJW1Kmq.js';
3
+ import { c as DispatchSettle, C as CtxArg, S as Store, O as OnError, a as Supervision, T as TelemetrySink } from './runtime-types-BPKp2bGo.js';
1
4
  import * as effect_Cause from 'effect/Cause';
2
5
  import * as effect_Types from 'effect/Types';
3
- import { Effect, Stream, Scope } from 'effect';
4
- import { P as Port, C as Cmd, v as ErrorsOf, J as OkOfCmd, u as DeclaredErrorsOf, c as Sub, s as BuiltinSubType, B as BuiltinSub, M as Machine } from './core-Ce9ncpuv.js';
5
- import { f as DispatchSettle, C as CtxArg, S as Store, O as OnError, a as Supervision, T as TelemetrySink } from './runtime-types-BKg2CGMx.js';
6
6
 
7
7
  declare const Stopped_base: new <A extends Record<string, any> = {}>(args: effect_Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => effect_Cause.YieldableError & {
8
8
  readonly _tag: "Stopped";
@@ -90,8 +90,13 @@ interface EffectBootingRuntime<S, M extends {
90
90
  */
91
91
  readonly ready: Effect.Effect<EffectRuntime<S, M, E, Err>, Err | StoreFailed>;
92
92
  /**
93
- * Stop the run: drain in-flight work, stop its Subs, flush the final State.
94
- * Closing the run's Scope does the same.
93
+ * Stop the run: drain in-flight work, up to `disposeTimeoutMs`, stop its
94
+ * Subs, flush the final State. `stop()` interrupts every handler still in
95
+ * flight, so the limit is reached only by a handler that cannot be
96
+ * interrupted. Closing the run's Scope stops the run the same way, and
97
+ * `stop()` leaves that Scope open. So for a child under `spawn` the two
98
+ * differ: `stop()` does not take the child out of the host's table or tell
99
+ * the parent. Stop such a child with the `stop` helper.
95
100
  */
96
101
  stop(): Effect.Effect<void>;
97
102
  }
@@ -237,7 +242,18 @@ type EffectRunOptions<S, M extends {
237
242
  readonly terminal?: (state: S) => boolean;
238
243
  /** A fire-and-forget sink for every applied transition. */
239
244
  readonly telemetry?: TelemetrySink;
240
- /** How long `stop()` waits for async teardown. Defaults to 5_000ms. */
245
+ /**
246
+ * How long `stop()` waits for the queue, and then for async teardown,
247
+ * before giving up on each. Defaults to 5_000ms. `stop()` interrupts every
248
+ * handler here, so the first wait ends early unless a handler cannot be
249
+ * interrupted.
250
+ *
251
+ * A store call is not interrupted. tea starts none after `stop()` has
252
+ * returned, but one already under way when `stop()` gave up can finish
253
+ * after it. On a plain store that late save can overwrite what a newer
254
+ * run saved; a fenced store refuses it. So a host that starts a new run on
255
+ * the same store after a `stop()` that gave up needs a fenced store.
256
+ */
241
257
  readonly disposeTimeoutMs?: number;
242
258
  };
243
259
  /**
@@ -283,4 +299,4 @@ declare function run<S, M extends {
283
299
  type: string;
284
300
  } = never>(machine: Machine<S, M, C, U, Ctx>, opts: EffectRunOptions<S, M, C, U, Ctx, E, I, B>): Effect.Effect<EffectBootingRuntime<S, M, E, CellErrors<C, I>>, never, Scope.Scope | InterpretServices<I> | SubscribeServices<B>>;
285
301
 
286
- export { type CellErrors as C, type EffectInterpret as E, type InterpretServices as I, type SubscribeOption as S, type EffectSubscribe as a, type SubscribeServices as b, type EffectBootingRuntime as c, type EffectInterpretCell as d, type EffectRunOptions as e, type EffectRunner as f, type EffectRuntime as g, Stopped as h, StoreFailed as i, run as r };
302
+ export { type CellErrors as C, type EffectInterpret as E, type InterpretServices as I, type SubscribeOption as S, type EffectSubscribe as a, type SubscribeServices as b, type EffectRuntime as c, StoreFailed as d, type EffectBootingRuntime as e, type EffectInterpretCell as f, type EffectRunOptions as g, type EffectRunner as h, Stopped as i, run as r };