@demlik/tea 0.19.0 → 0.20.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 (116) hide show
  1. package/README.md +25 -18
  2. package/dist/agent/index.d.ts +44 -13
  3. package/dist/agent/index.js +10 -10
  4. package/dist/{chunk-PCJA2HTZ.js → chunk-2774I242.js} +7 -7
  5. package/dist/chunk-2774I242.js.map +1 -0
  6. package/dist/{chunk-YIMOFXST.js → chunk-2R62Q5UK.js} +3 -3
  7. package/dist/chunk-2R62Q5UK.js.map +1 -0
  8. package/dist/{chunk-PQSDUX6B.js → chunk-5HVKDHY6.js} +3 -3
  9. package/dist/{chunk-PQSDUX6B.js.map → chunk-5HVKDHY6.js.map} +1 -1
  10. package/dist/{chunk-WTSOW2MD.js → chunk-72CN3VPY.js} +5 -5
  11. package/dist/{chunk-WTSOW2MD.js.map → chunk-72CN3VPY.js.map} +1 -1
  12. package/dist/{chunk-JVE2LBFJ.js → chunk-A73IJ27X.js} +3 -3
  13. package/dist/{chunk-JVE2LBFJ.js.map → chunk-A73IJ27X.js.map} +1 -1
  14. package/dist/{chunk-I7E7AD2S.js → chunk-AIXC33SE.js} +4 -4
  15. package/dist/chunk-AIXC33SE.js.map +1 -0
  16. package/dist/{chunk-FNGZMVIT.js → chunk-DVNXEB7V.js} +3 -3
  17. package/dist/chunk-DVNXEB7V.js.map +1 -0
  18. package/dist/{chunk-XVIKSRCK.js → chunk-EA47MI5Q.js} +4 -4
  19. package/dist/{chunk-XVIKSRCK.js.map → chunk-EA47MI5Q.js.map} +1 -1
  20. package/dist/{chunk-IHUNKHDX.js → chunk-FTCYY2DO.js} +3 -3
  21. package/dist/{chunk-IHUNKHDX.js.map → chunk-FTCYY2DO.js.map} +1 -1
  22. package/dist/{chunk-JVKU2UQC.js → chunk-HX3C6LDK.js} +3 -3
  23. package/dist/{chunk-JVKU2UQC.js.map → chunk-HX3C6LDK.js.map} +1 -1
  24. package/dist/{chunk-H2ELKHI6.js → chunk-JAKM53JF.js} +4 -4
  25. package/dist/chunk-JAKM53JF.js.map +1 -0
  26. package/dist/{chunk-JYNYG5SF.js → chunk-NGYFBD5G.js} +3 -3
  27. package/dist/chunk-NGYFBD5G.js.map +1 -0
  28. package/dist/{chunk-AIIXV4XB.js → chunk-RKVUVALV.js} +2 -2
  29. package/dist/chunk-RKVUVALV.js.map +1 -0
  30. package/dist/{chunk-C3G2QFEZ.js → chunk-TAKPC6SD.js} +4 -4
  31. package/dist/{chunk-C3G2QFEZ.js.map → chunk-TAKPC6SD.js.map} +1 -1
  32. package/dist/{chunk-BRX3243I.js → chunk-TGJLXZAU.js} +3 -3
  33. package/dist/{chunk-BRX3243I.js.map → chunk-TGJLXZAU.js.map} +1 -1
  34. package/dist/{chunk-AHW2CLPT.js → chunk-UHAQ4PI6.js} +4 -4
  35. package/dist/{chunk-AHW2CLPT.js.map → chunk-UHAQ4PI6.js.map} +1 -1
  36. package/dist/{chunk-H3MWNH7A.js → chunk-XWIE5V7R.js} +4 -4
  37. package/dist/{chunk-H3MWNH7A.js.map → chunk-XWIE5V7R.js.map} +1 -1
  38. package/dist/{core-Ce9ncpuv.d.ts → core-CtvAyPpH.d.ts} +153 -1
  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 +12 -12
  42. package/dist/do/index.js +11 -11
  43. package/dist/{drive-loop-CsUsW3Wo.d.ts → drive-loop-Dw1sGSsW.d.ts} +1 -1
  44. package/dist/{durable-effects-BmUrRA-h.d.ts → durable-effects-Ump5lSdO.d.ts} +1 -1
  45. package/dist/effect/index.d.ts +124 -5
  46. package/dist/effect/index.js +33 -4
  47. package/dist/effect/index.js.map +1 -1
  48. package/dist/extension/index.d.ts +84 -3
  49. package/dist/extension/index.js +2 -2
  50. package/dist/extension/index.js.map +1 -1
  51. package/dist/flow/index.d.ts +29 -13
  52. package/dist/flow/index.js +11 -11
  53. package/dist/flow/index.js.map +1 -1
  54. package/dist/idempotency/index.d.ts +11 -3
  55. package/dist/idempotency/index.js +3 -3
  56. package/dist/{index-D8sYVzCI.d.ts → index-Bk2hBUdK.d.ts} +6 -1
  57. package/dist/{index-2kNxy6az.d.ts → index-CKtEKGyp.d.ts} +15 -3
  58. package/dist/{index-e36uEyl4.d.ts → index-CVU7SK3a.d.ts} +2 -2
  59. package/dist/{index-Dy3379ve.d.ts → index-CZZ3HI-Z.d.ts} +4 -4
  60. package/dist/{index-Ce5GYAuI.d.ts → index-CrcoVfd_.d.ts} +2 -2
  61. package/dist/{index-DWdg7R1l.d.ts → index-D3NY3VCN.d.ts} +5 -1
  62. package/dist/{index-B1Wzarkl.d.ts → index-Dl71V7kt.d.ts} +1 -1
  63. package/dist/{index-Bf9_bNql.d.ts → index-EIgfl83J.d.ts} +2 -2
  64. package/dist/index.d.ts +66 -5
  65. package/dist/index.js +2 -2
  66. package/dist/jev/index.d.ts +39 -27
  67. package/dist/jev/index.js +12 -8
  68. package/dist/jev/index.js.map +1 -1
  69. package/dist/{machine-CuojfZup.d.ts → machine-Dtre2TmN.d.ts} +9 -7
  70. package/dist/machine-viz/index.d.ts +2 -1
  71. package/dist/machine-viz/index.js +2 -2
  72. package/dist/machine-viz/index.js.map +1 -1
  73. package/dist/mem/index.d.ts +2 -2
  74. package/dist/mem/index.js +2 -2
  75. package/dist/node/index.d.ts +11 -2
  76. package/dist/node/index.js +2 -2
  77. package/dist/node/index.js.map +1 -1
  78. package/dist/otel/index.d.ts +10 -10
  79. package/dist/otel/index.js +10 -10
  80. package/dist/paginate/index.d.ts +5 -4
  81. package/dist/paginate/index.js +5 -5
  82. package/dist/parity/index.d.ts +3 -3
  83. package/dist/parity/index.js +3 -3
  84. package/dist/pbt/index.d.ts +1 -1
  85. package/dist/pbt/index.js +2 -2
  86. package/dist/persistence/index.d.ts +5 -5
  87. package/dist/persistence/index.js +4 -4
  88. package/dist/promise/index.d.ts +7 -2
  89. package/dist/promise/index.js +4 -4
  90. package/dist/react/index.d.ts +2 -2
  91. package/dist/resilience/index.d.ts +7 -6
  92. package/dist/resilience/index.js +5 -5
  93. package/dist/resilience/index.js.map +1 -1
  94. package/dist/{index-BnsAqn0-.d.ts → run-fiBe7pOG.d.ts} +8 -5
  95. package/dist/{runtime-types-BKg2CGMx.d.ts → runtime-types-DEluQgGO.d.ts} +74 -2
  96. package/dist/testing/effect/index.d.ts +5 -5
  97. package/dist/testing/effect/index.js +8 -8
  98. package/dist/testing/index.d.ts +2 -2
  99. package/dist/testing/index.js +3 -3
  100. package/dist/testing/promise/index.d.ts +4 -4
  101. package/dist/testing/promise/index.js +5 -5
  102. package/dist/timing/index.d.ts +4 -4
  103. package/dist/timing/index.js +4 -4
  104. package/dist/types-CXNzlbAg.d.ts +9 -0
  105. package/dist/work-queue/index.d.ts +4 -4
  106. package/dist/work-queue/index.js +2 -2
  107. package/dist/work-queue/index.js.map +1 -1
  108. package/package.json +3 -2
  109. package/dist/chunk-AIIXV4XB.js.map +0 -1
  110. package/dist/chunk-FNGZMVIT.js.map +0 -1
  111. package/dist/chunk-H2ELKHI6.js.map +0 -1
  112. package/dist/chunk-I7E7AD2S.js.map +0 -1
  113. package/dist/chunk-JYNYG5SF.js.map +0 -1
  114. package/dist/chunk-PCJA2HTZ.js.map +0 -1
  115. package/dist/chunk-YIMOFXST.js.map +0 -1
  116. package/dist/types-SzNSHKlV.d.ts +0 -5
@@ -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-JAKM53JF.js';
2
+ import '../chunk-UHAQ4PI6.js';
3
+ import '../chunk-A73IJ27X.js';
4
+ import '../chunk-RKVUVALV.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-DEluQgGO.js';
2
+ import { C as Cmd, S as Sub, R as RunHandlers, M as Machine } from '../core-CtvAyPpH.js';
3
3
  import '@standard-schema/spec';
4
4
 
5
5
  /**
@@ -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-CtvAyPpH.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-CVU7SK3a.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-CVU7SK3a.js';
5
+ import { d as ResilientConfig, b as ResilientState, R as RunCmd, S as SucceedMsg, F as FailMsg, a as ResilientTimerMsg } from '../index-CKtEKGyp.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-CKtEKGyp.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-CXNzlbAg.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-72CN3VPY.js';
2
+ export { DEFAULT_RESILIENT_NAME, canPass, createResilientCall, defaultCircuitPolicy, initBucket, initCircuit, initWindow, liftResilience, onFailure, onSuccess, record, refill, remaining, runCmdDef, tryConsume } from '../chunk-72CN3VPY.js';
3
+ export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, deadlineExceeded, deadlineMsgType, deadlineSub, deadlines, deadlinesSub, evictExpired, get, has, initCache, nextTimer, remove, set, setTimeoutArmTimer, subscribeDeadline, subscribeWith } from '../chunk-TAKPC6SD.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-DVNXEB7V.js';
7
+ import { Cmd, without } from '../chunk-RKVUVALV.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, o as OkOfCmd, p as DeclaredErrorsOf, S as Sub, q as BuiltinSubType, B as BuiltinSub, M as Machine } from './core-CtvAyPpH.js';
3
+ import { c as DispatchSettle, C as CtxArg, S as Store, O as OnError, a as Supervision, T as TelemetrySink } from './runtime-types-DEluQgGO.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";
@@ -91,7 +91,10 @@ interface EffectBootingRuntime<S, M extends {
91
91
  readonly ready: Effect.Effect<EffectRuntime<S, M, E, Err>, Err | StoreFailed>;
92
92
  /**
93
93
  * Stop the run: drain in-flight work, stop its Subs, flush the final State.
94
- * Closing the run's Scope does the same.
94
+ * Closing the run's Scope stops the run the same way, and `stop()` leaves
95
+ * that Scope open. So for a child under `spawn` the two differ: `stop()`
96
+ * does not take the child out of the host's table or tell the parent. Stop
97
+ * such a child with the `stop` helper.
95
98
  */
96
99
  stop(): Effect.Effect<void>;
97
100
  }
@@ -283,4 +286,4 @@ declare function run<S, M extends {
283
286
  type: string;
284
287
  } = 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
288
 
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 };
289
+ export { type CellErrors as C, type EffectRuntime as E, type InterpretServices as I, StoreFailed as S, type EffectBootingRuntime as a, type EffectInterpret as b, type EffectInterpretCell as c, type EffectRunOptions as d, type EffectRunner as e, type EffectSubscribe as f, Stopped as g, type SubscribeServices as h, type SubscribeOption as i, run as r };
@@ -1,4 +1,4 @@
1
- import { C as Cmd, c as Sub, M as Machine, R as RunHandlers, P as Port, N as NoCellError, g as Reducer, A as AnyCmdDef, h as Settled, a as CmdOf, i as Transitions, B as BuiltinSub, U as UpdateForm, O as Outcome, j as InterpretDetached, H as HandlerCtx } from './core-Ce9ncpuv.js';
1
+ import { C as Cmd, S as Sub, M as Machine, R as RunHandlers, P as Port, N as NoCellError, g as Reducer, A as AnyCmdDef, h as Settled, a as CmdOf, i as Transitions, B as BuiltinSub, U as UpdateForm, O as Outcome, j as InterpretDetached, H as HandlerCtx } from './core-CtvAyPpH.js';
2
2
 
3
3
  /**
4
4
  * @demlik/tea runtime interface surface + pure helpers — the public types,
@@ -12,6 +12,11 @@ import { C as Cmd, c as Sub, M as Machine, R as RunHandlers, P as Port, N as NoC
12
12
  */
13
13
 
14
14
  declare const ReducerBrand: unique symbol;
15
+ /**
16
+ * A `Reducer` or `Transitions` value that went through `asReducer` or
17
+ * `defineMachine`. It carries the phantom brand and otherwise behaves exactly
18
+ * like the unbranded value.
19
+ */
15
20
  type Branded<T> = T & {
16
21
  readonly [ReducerBrand]: true;
17
22
  };
@@ -38,6 +43,10 @@ type Branded<T> = T & {
38
43
  * ```
39
44
  */
40
45
  declare function absurd(x: never): never;
46
+ /**
47
+ * An update cell that ignores its Msg: it returns the state unchanged, with no
48
+ * Cmds. Use it to say a state deliberately does nothing for a Msg.
49
+ */
41
50
  declare const noop: <S, M, C extends Cmd>(state: S, _msg: M) => readonly [S, readonly C[]];
42
51
  /**
43
52
  * Thrown by `definePort` when a name has already been registered in the current
@@ -313,6 +322,11 @@ declare class DispatchDiscardedError extends RuntimeDiscardNotice {
313
322
  * telemetry) that should NOT be folded into State. Strengthens invariant 7.
314
323
  */
315
324
  declare function definePort<T>(name: string): Port<T>;
325
+ /**
326
+ * Where an engine loads and saves a machine's state. Implement `load`, `save`
327
+ * and `migrate` to persist to your own storage, or use a store an adapter
328
+ * ships.
329
+ */
316
330
  interface Store<S> {
317
331
  load(): Promise<unknown>;
318
332
  save(state: S): Promise<void>;
@@ -361,6 +375,10 @@ declare class StoreRefusedError extends Error {
361
375
  readonly cause?: unknown;
362
376
  });
363
377
  }
378
+ /**
379
+ * What a fenced store's `loadFenced` returns: the raw saved value and the
380
+ * version it was written at.
381
+ */
364
382
  interface FencedRead {
365
383
  /** Raw bytes at the key, the `Store.load` contract unchanged. */
366
384
  readonly raw: unknown;
@@ -429,6 +447,10 @@ declare class StoreConflictError extends Error {
429
447
  readonly _tag: "store_conflict";
430
448
  constructor(expectedVersion: number, actualVersion: number);
431
449
  }
450
+ /**
451
+ * The minimal validator shape `schemaMigrate` accepts: an object with
452
+ * `safeParse`. A zod schema satisfies it.
453
+ */
432
454
  interface Schema<S> {
433
455
  safeParse(raw: unknown): {
434
456
  success: true;
@@ -447,7 +469,16 @@ interface Schema<S> {
447
469
  * return a {@link refuse}, so `run` fails instead of booting fresh over them.
448
470
  */
449
471
  declare function schemaMigrate<S>(schema: Schema<S>, upcast?: (raw: unknown) => unknown): (raw: unknown) => Migrated<S>;
472
+ /**
473
+ * How long a `dispatch` waits before it resolves: `quiescent`, the default,
474
+ * waits for every follow-up Msg to drain, and `once` waits for the single
475
+ * transition.
476
+ */
450
477
  type DispatchSettle = "quiescent" | "once";
478
+ /**
479
+ * The dispatch-only view of a runtime. Use it as the type of a handle one
480
+ * runtime holds in order to send Msgs to another.
481
+ */
451
482
  interface RuntimeRef<M extends {
452
483
  type: string;
453
484
  }> {
@@ -699,6 +730,11 @@ type MachineTypes<S, M extends {
699
730
  readonly sub?: U;
700
731
  readonly ctx?: Ctx;
701
732
  };
733
+ /**
734
+ * Define a machine: its `init`, its `update` (a reducer or a transitions
735
+ * table), and optionally its `subs`, `cmds` and `identity`. Every tea program
736
+ * starts here; the result is plain data you hand to an engine's `run`.
737
+ */
702
738
  declare function defineMachine<S, M extends {
703
739
  type: string;
704
740
  }, D extends AnyCmdDef, U extends Sub = Sub<never>, Ctx = unknown>(m: NoInfer<Omit<Machine<S, M | Settled<D>, CmdOf<D>, U, Ctx>, "cmds">> & {
@@ -736,14 +772,35 @@ declare function defineMachine<S, M extends {
736
772
  }, C extends Cmd, U extends Sub, Ctx>(m: Omit<Machine<S, M, C, U, Ctx>, "update"> & {
737
773
  update: Reducer<S, M, C>;
738
774
  }): Machine<S, M, C, U, Ctx>;
775
+ /**
776
+ * Type-check a standalone reducer record and return it branded. Use it when you
777
+ * write `update` apart from `defineMachine`.
778
+ */
739
779
  declare function asReducer<S, M extends {
740
780
  type: string;
741
781
  }, C extends Cmd>(reducer: Reducer<S, M, C>): Branded<Reducer<S, M, C>>;
782
+ /**
783
+ * The `ctx` field of an engine's options: optional when the machine's `Ctx` is
784
+ * empty, required otherwise.
785
+ */
742
786
  type CtxArg<Ctx> = [Record<never, never>] extends [Ctx] ? {
743
787
  ctx?: Ctx;
744
788
  } : {
745
789
  ctx: Ctx;
746
790
  };
791
+ /**
792
+ * Run a machine's `init` and then its `update` over a list of Msgs, with no
793
+ * engine, store or effects. Use it in unit tests to assert the final state, the
794
+ * Cmds emitted, and the Subs that would be running.
795
+ *
796
+ * It composes `init(loaded ?? null, ctx)` and then `update(state, msg)` for each
797
+ * Msg. No Cmd handler is called, no `Store` is touched and no Sub is started.
798
+ *
799
+ * `subs` is each `machine.subs` entry that is on at the final state, as its
800
+ * runner would see it: `{ id, type, deps }`, the id derived exactly as the
801
+ * engine derives it, deduplicated by id the way the engine runs them. So a test
802
+ * asserts "the retry timer is armed in `waiting`" without wiring a runner.
803
+ */
747
804
  declare function replay<S, M extends {
748
805
  type: string;
749
806
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, opts: {
@@ -755,6 +812,11 @@ declare function replay<S, M extends {
755
812
  cmds: C[];
756
813
  subs: (U | BuiltinSub<M>)[];
757
814
  };
815
+ /**
816
+ * Adapt an `InterpretDetached` handler into an ordinary `interpret` cell. Use
817
+ * it for a Cmd whose work finishes after the handler returns and reports back
818
+ * through `dispatch`.
819
+ */
758
820
  declare function wrapDetached<C extends Cmd, M extends {
759
821
  type: string;
760
822
  }, Allowed extends M, Ctx>(handler: InterpretDetached<C, Allowed, Ctx>): (cmd: C, ctx: HandlerCtx<Ctx>, dispatch?: (msg: M) => void) => Promise<void>;
@@ -810,6 +872,16 @@ declare function tryFoldMsgs<S, M extends {
810
872
  update: object;
811
873
  __form?: UpdateForm;
812
874
  }, base: S, msgs: readonly M[]): Outcome<S, FoldRefusal<M>>;
875
+ /**
876
+ * Wrap a function that may throw into a Cmd handler that never rejects: success
877
+ * maps to one Msg through `onOk`, failure to another through `onErr`. Use it
878
+ * for a hand-written Cmd; a `Cmd.define`d Cmd does not need it, because its
879
+ * handler returns an `Outcome` and the engine mints the Msg (ADR 0021).
880
+ *
881
+ * A rejection and a synchronous throw both reach `onErr(error, cmd)`, with the
882
+ * original error untouched so `instanceof` checks inside `onErr` hold. The
883
+ * never-rejects promise assumes `onOk` and `onErr` do not throw.
884
+ */
813
885
  declare function tryInterpret<C extends Cmd, Ok, M, Ctx>(work: (cmd: C, ctx: Ctx) => Promise<Ok>, onOk: (value: Ok, cmd: C) => M, onErr: (error: unknown, cmd: C) => M): (cmd: C, ctx: Ctx) => Promise<M>;
814
886
 
815
- export { defineMachine as A, type BootingRuntime as B, type CtxArg as C, type DeletableStore as D, type EngineRun as E, type FencedStore as F, definePort as G, isFencedStore as H, IdentityDropNotice as I, noop as J, refuse as K, replay as L, type Migrated as M, schemaMigrate as N, type OnError as O, PortNameCollisionError as P, QuiescenceTimeoutError as Q, type Runtime as R, type Store as S, type TelemetrySink as T, tryApplyCell as U, tryFoldMsgs as V, tryInterpret as W, wrapDetached as X, __resetPortRegistry as _, type Supervision as a, type BootedRunHandle as b, type RunHandle as c, type Branded as d, DispatchDiscardedError as e, type DispatchSettle as f, DisposeTimeoutNotice as g, DriveFailedError as h, DriveStalledError as i, type FencedRead as j, type FoldRefusal as k, type MachineTypes as l, Refusal as m, type RunOptions as n, RuntimeDiscardNotice as o, RuntimeDiscardedError as p, type RuntimeErrorContext as q, type RuntimeErrorPhase as r, type RuntimeRef as s, type Schema as t, StoreConflictError as u, StoreRefusedError as v, type SupervisionStrategy as w, type TelemetryEvent as x, absurd as y, asReducer as z };
887
+ export { defineMachine as A, type BootingRuntime as B, type CtxArg as C, type DeletableStore as D, type EngineRun as E, type FencedStore as F, definePort as G, isFencedStore as H, IdentityDropNotice as I, noop as J, refuse as K, replay as L, type Migrated as M, schemaMigrate as N, type OnError as O, PortNameCollisionError as P, QuiescenceTimeoutError as Q, type Runtime as R, type Store as S, type TelemetrySink as T, tryApplyCell as U, tryFoldMsgs as V, tryInterpret as W, wrapDetached as X, __resetPortRegistry as _, type Supervision as a, type BootedRunHandle as b, type DispatchSettle as c, type RunHandle as d, type Branded as e, DispatchDiscardedError as f, DisposeTimeoutNotice as g, DriveFailedError as h, DriveStalledError as i, type FencedRead as j, type FoldRefusal as k, type MachineTypes as l, Refusal as m, type RunOptions as n, RuntimeDiscardNotice as o, RuntimeDiscardedError as p, type RuntimeErrorContext as q, type RuntimeErrorPhase as r, type RuntimeRef as s, type Schema as t, StoreConflictError as u, StoreRefusedError as v, type SupervisionStrategy as w, type TelemetryEvent as x, absurd as y, asReducer as z };
@@ -1,9 +1,9 @@
1
- import { D as DriveRoundsExceededError, a as DriveNoHandlerError, b as DriveOptions, c as DriveResult } from '../../drive-loop-CsUsW3Wo.js';
2
- export { d as DEFAULT_MAX_ROUNDS, e as DriveCtxArg, f as DriveTraceEntry, g as driveTraceOf } from '../../drive-loop-CsUsW3Wo.js';
1
+ import { D as DriveRoundsExceededError, a as DriveNoHandlerError, b as DriveOptions, c as DriveResult } from '../../drive-loop-Dw1sGSsW.js';
2
+ export { d as DEFAULT_MAX_ROUNDS, e as DriveCtxArg, f as DriveTraceEntry, g as driveTraceOf } from '../../drive-loop-Dw1sGSsW.js';
3
3
  import { Effect } from 'effect';
4
- import { C as CellErrors, S as SubscribeOption, E as EffectInterpret, a as EffectSubscribe, I as InterpretServices, b as SubscribeServices } from '../../index-BnsAqn0-.js';
5
- import { C as Cmd, c as Sub, M as Machine } from '../../core-Ce9ncpuv.js';
6
- import '../../runtime-types-BKg2CGMx.js';
4
+ import { C as CellErrors, i as SubscribeOption, b as EffectInterpret, f as EffectSubscribe, I as InterpretServices, h as SubscribeServices } from '../../run-fiBe7pOG.js';
5
+ import { C as Cmd, S as Sub, M as Machine } from '../../core-CtvAyPpH.js';
6
+ import '../../runtime-types-DEluQgGO.js';
7
7
  import 'effect/Cause';
8
8
  import 'effect/Types';
9
9
  import '@standard-schema/spec';
@@ -1,11 +1,11 @@
1
- import { CellFailure, builtinRunners, cellResult, settleCellExit } from '../../chunk-I7E7AD2S.js';
2
- import { driveLoop, DEFAULT_MAX_ROUNDS, driveContractOf, DriveRoundsExceededError, DriveNoHandlerError, annotateTrace, driveTraceOf, desiredSubs } from '../../chunk-H3MWNH7A.js';
3
- export { DEFAULT_MAX_ROUNDS, DriveNoHandlerError, DriveRoundsExceededError, driveTraceOf } from '../../chunk-H3MWNH7A.js';
4
- import '../../chunk-JVKU2UQC.js';
5
- import '../../chunk-AHW2CLPT.js';
6
- import '../../chunk-JVE2LBFJ.js';
7
- import '../../chunk-FNGZMVIT.js';
8
- import '../../chunk-AIIXV4XB.js';
1
+ import { CellFailure, builtinRunners, cellResult, settleCellExit } from '../../chunk-AIXC33SE.js';
2
+ import { driveLoop, DEFAULT_MAX_ROUNDS, driveContractOf, DriveRoundsExceededError, DriveNoHandlerError, annotateTrace, driveTraceOf, desiredSubs } from '../../chunk-XWIE5V7R.js';
3
+ export { DEFAULT_MAX_ROUNDS, DriveNoHandlerError, DriveRoundsExceededError, driveTraceOf } from '../../chunk-XWIE5V7R.js';
4
+ import '../../chunk-HX3C6LDK.js';
5
+ import '../../chunk-UHAQ4PI6.js';
6
+ import '../../chunk-A73IJ27X.js';
7
+ import '../../chunk-DVNXEB7V.js';
8
+ import '../../chunk-RKVUVALV.js';
9
9
  import { Effect, Stream, Exit, Cause, Fiber } from 'effect';
10
10
 
11
11
  var DriveInterrupted = class {
@@ -1,5 +1,5 @@
1
- import { C as Cmd, c as Sub, M as Machine, B as BuiltinSub } from '../core-Ce9ncpuv.js';
2
- import { R as Runtime } from '../runtime-types-BKg2CGMx.js';
1
+ import { C as Cmd, S as Sub, M as Machine, B as BuiltinSub } from '../core-CtvAyPpH.js';
2
+ import { R as Runtime } from '../runtime-types-DEluQgGO.js';
3
3
  import '@standard-schema/spec';
4
4
 
5
5
  /**
@@ -1,6 +1,6 @@
1
- export { bindMachine } from '../chunk-JVKU2UQC.js';
2
- import '../chunk-FNGZMVIT.js';
3
- import { replay } from '../chunk-AIIXV4XB.js';
1
+ export { bindMachine } from '../chunk-HX3C6LDK.js';
2
+ import '../chunk-DVNXEB7V.js';
3
+ import { replay } from '../chunk-RKVUVALV.js';
4
4
  import { expect, vi } from 'vitest';
5
5
 
6
6
  function expectFinalState(machine, opts, expected) {
@@ -1,7 +1,7 @@
1
- import { b as DriveOptions, c as DriveResult } from '../../drive-loop-CsUsW3Wo.js';
2
- export { d as DEFAULT_MAX_ROUNDS, e as DriveCtxArg, a as DriveNoHandlerError, D as DriveRoundsExceededError, f as DriveTraceEntry, g as driveTraceOf } from '../../drive-loop-CsUsW3Wo.js';
3
- import { C as Cmd, c as Sub, M as Machine, I as Interpret } from '../../core-Ce9ncpuv.js';
4
- import '../../runtime-types-BKg2CGMx.js';
1
+ import { b as DriveOptions, c as DriveResult } from '../../drive-loop-Dw1sGSsW.js';
2
+ export { d as DEFAULT_MAX_ROUNDS, e as DriveCtxArg, a as DriveNoHandlerError, D as DriveRoundsExceededError, f as DriveTraceEntry, g as driveTraceOf } from '../../drive-loop-Dw1sGSsW.js';
3
+ import { C as Cmd, S as Sub, M as Machine, I as Interpret } from '../../core-CtvAyPpH.js';
4
+ import '../../runtime-types-DEluQgGO.js';
5
5
  import '@standard-schema/spec';
6
6
 
7
7
  /**
@@ -1,8 +1,8 @@
1
- import { driveContractOf, driveLoop, DEFAULT_MAX_ROUNDS } from '../../chunk-H3MWNH7A.js';
2
- export { DEFAULT_MAX_ROUNDS, DriveNoHandlerError, DriveRoundsExceededError, driveTraceOf } from '../../chunk-H3MWNH7A.js';
3
- import '../../chunk-JVKU2UQC.js';
4
- import '../../chunk-FNGZMVIT.js';
5
- import { cmdEdge, Outcome } from '../../chunk-AIIXV4XB.js';
1
+ import { driveContractOf, driveLoop, DEFAULT_MAX_ROUNDS } from '../../chunk-XWIE5V7R.js';
2
+ export { DEFAULT_MAX_ROUNDS, DriveNoHandlerError, DriveRoundsExceededError, driveTraceOf } from '../../chunk-XWIE5V7R.js';
3
+ import '../../chunk-HX3C6LDK.js';
4
+ import '../../chunk-DVNXEB7V.js';
5
+ import { cmdEdge, Outcome } from '../../chunk-RKVUVALV.js';
6
6
 
7
7
  // src/testing/promise/drive.ts
8
8
  async function drive(machine, initial, msg, handlers, ...[opts]) {