@demlik/tea 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +49 -0
  2. package/dist/agent/index.d.ts +16 -770
  3. package/dist/agent/index.js +18 -16
  4. package/dist/authed-call/index.d.ts +3 -3
  5. package/dist/authed-call/index.js +22 -36
  6. package/dist/authed-call/index.js.map +1 -1
  7. package/dist/await-terminal/index.d.ts +2 -2
  8. package/dist/await-terminal/index.js +2 -2
  9. package/dist/batch-window/index.d.ts +3 -3
  10. package/dist/batch-window/index.js +3 -3
  11. package/dist/cache/index.d.ts +2 -2
  12. package/dist/cache/index.js +5 -2
  13. package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
  14. package/dist/chunk-25LBDCBC.js.map +1 -0
  15. package/dist/{chunk-IK4S5DJN.js → chunk-2AHAR7CX.js} +7 -14
  16. package/dist/chunk-2AHAR7CX.js.map +1 -0
  17. package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
  18. package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
  19. package/dist/{chunk-VFGKTGGC.js → chunk-5W7CNAHB.js} +191 -178
  20. package/dist/chunk-5W7CNAHB.js.map +1 -0
  21. package/dist/{chunk-6GIGQT6C.js → chunk-B5RAZVXW.js} +12 -11
  22. package/dist/chunk-B5RAZVXW.js.map +1 -0
  23. package/dist/{chunk-6VDCTR7N.js → chunk-D4NJBQAH.js} +258 -151
  24. package/dist/chunk-D4NJBQAH.js.map +1 -0
  25. package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
  26. package/dist/chunk-D736QUGF.js.map +1 -0
  27. package/dist/{chunk-EGWTT5XJ.js → chunk-ET6QQNSQ.js} +76 -95
  28. package/dist/chunk-ET6QQNSQ.js.map +1 -0
  29. package/dist/chunk-EWJP3YYP.js +21 -0
  30. package/dist/chunk-EWJP3YYP.js.map +1 -0
  31. package/dist/{chunk-M6UZT7HS.js → chunk-FCKP6N7Y.js} +6 -5
  32. package/dist/chunk-FCKP6N7Y.js.map +1 -0
  33. package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
  34. package/dist/chunk-FV3Z5MBE.js.map +1 -0
  35. package/dist/chunk-G5E74IJB.js +8 -0
  36. package/dist/chunk-G5E74IJB.js.map +1 -0
  37. package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
  38. package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
  39. package/dist/{chunk-FOGL3TC7.js → chunk-JXYYRKD6.js} +8 -10
  40. package/dist/chunk-JXYYRKD6.js.map +1 -0
  41. package/dist/{chunk-ZDRN3YCE.js → chunk-LJJV6ZD5.js} +17 -24
  42. package/dist/chunk-LJJV6ZD5.js.map +1 -0
  43. package/dist/chunk-LYLDCIPH.js +365 -0
  44. package/dist/chunk-LYLDCIPH.js.map +1 -0
  45. package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
  46. package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
  47. package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
  48. package/dist/chunk-MUUROGIH.js.map +1 -0
  49. package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
  50. package/dist/chunk-PBDYW2TU.js.map +1 -0
  51. package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
  52. package/dist/chunk-Q2UM7BXU.js.map +1 -0
  53. package/dist/chunk-RMX4EOQK.js +26 -0
  54. package/dist/chunk-RMX4EOQK.js.map +1 -0
  55. package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
  56. package/dist/chunk-S2EGRPCM.js.map +1 -0
  57. package/dist/{chunk-5KVHID6A.js → chunk-SPWVPIFQ.js} +4 -4
  58. package/dist/{chunk-5KVHID6A.js.map → chunk-SPWVPIFQ.js.map} +1 -1
  59. package/dist/chunk-SSHDBOXF.js +71 -0
  60. package/dist/chunk-SSHDBOXF.js.map +1 -0
  61. package/dist/{chunk-6OUDPSSG.js → chunk-TWDHNIY6.js} +3 -3
  62. package/dist/{chunk-6OUDPSSG.js.map → chunk-TWDHNIY6.js.map} +1 -1
  63. package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
  64. package/dist/chunk-UF4MZSLT.js.map +1 -0
  65. package/dist/chunk-ZJ5AOYBH.js +8 -0
  66. package/dist/chunk-ZJ5AOYBH.js.map +1 -0
  67. package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
  68. package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
  69. package/dist/circuit-breaker/index.d.ts +1 -0
  70. package/dist/circuit-breaker/index.js +1 -1
  71. package/dist/{core-BmlJOvxM.d.ts → core-42kapW0n.d.ts} +96 -6
  72. package/dist/deadline/index.d.ts +77 -11
  73. package/dist/deadline/index.js +3 -3
  74. package/dist/devtools/index.js +8 -28
  75. package/dist/devtools/index.js.map +1 -1
  76. package/dist/do/index.d.ts +52 -24
  77. package/dist/do/index.js +94 -92
  78. package/dist/do/index.js.map +1 -1
  79. package/dist/{durable-effects-9LxFpmh3.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
  80. package/dist/extension/index.d.ts +25 -7
  81. package/dist/extension/index.js +4 -3
  82. package/dist/extension/index.js.map +1 -1
  83. package/dist/extension/react.js +4 -3
  84. package/dist/extension/react.js.map +1 -1
  85. package/dist/extension/subs/index.d.ts +2 -2
  86. package/dist/extension/subs/index.js +7 -10
  87. package/dist/extension/subs/index.js.map +1 -1
  88. package/dist/fan-out/index.d.ts +10 -10
  89. package/dist/fan-out/index.js +3 -3
  90. package/dist/idempotent-intake/index.d.ts +2 -2
  91. package/dist/idempotent-intake/index.js +3 -3
  92. package/dist/index.d.ts +50 -489
  93. package/dist/index.js +2 -2
  94. package/dist/llm-call/index.d.ts +3 -2
  95. package/dist/llm-call/index.js +12 -10
  96. package/dist/machine-B_BF4jij.d.ts +804 -0
  97. package/dist/machine-viz/index.d.ts +1 -1
  98. package/dist/machine-viz/index.js +2 -2
  99. package/dist/mem/index.d.ts +3 -2
  100. package/dist/mem/index.js.map +1 -1
  101. package/dist/monitored-run/index.d.ts +54 -30
  102. package/dist/monitored-run/index.js +7 -7
  103. package/dist/node/index.d.ts +3 -2
  104. package/dist/node/index.js +2 -3
  105. package/dist/node/index.js.map +1 -1
  106. package/dist/paginated-walk/index.d.ts +2 -2
  107. package/dist/paginated-walk/index.js +11 -10
  108. package/dist/parity/index.d.ts +6 -5
  109. package/dist/parity/index.js +5 -4
  110. package/dist/parity/index.js.map +1 -1
  111. package/dist/pbt/arbitraries/index.d.ts +1 -1
  112. package/dist/pbt/index.d.ts +1 -1
  113. package/dist/pbt/index.js +3 -3
  114. package/dist/pbt/index.js.map +1 -1
  115. package/dist/pbt/runners/index.d.ts +1 -1
  116. package/dist/pbt/runners/index.js +3 -3
  117. package/dist/poller/index.d.ts +59 -17
  118. package/dist/poller/index.js +56 -20
  119. package/dist/poller/index.js.map +1 -1
  120. package/dist/prediction/index.d.ts +1 -1
  121. package/dist/prediction/index.js +2 -2
  122. package/dist/pure/index.d.ts +1 -1
  123. package/dist/pure/index.js +2 -2
  124. package/dist/rate-limit/index.d.ts +2 -2
  125. package/dist/rate-limit/index.js +1 -1
  126. package/dist/react/index.d.ts +10 -3
  127. package/dist/react/index.js +2 -2
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/reconciler/index.d.ts +2 -2
  130. package/dist/reconciler/index.js +13 -15
  131. package/dist/reconciler/index.js.map +1 -1
  132. package/dist/recorder/index.d.ts +20 -20
  133. package/dist/recorder/index.js +1 -1
  134. package/dist/resilient-call/index.d.ts +31 -6
  135. package/dist/resilient-call/index.js +10 -9
  136. package/dist/retry-backoff/index.d.ts +191 -14
  137. package/dist/retry-backoff/index.js +1 -1
  138. package/dist/retry-to-success/index.js +1 -1
  139. package/dist/runtime-types-CukV8kMX.d.ts +406 -0
  140. package/dist/saga/index.d.ts +3 -2
  141. package/dist/saga/index.js +2 -2
  142. package/dist/saga/index.js.map +1 -1
  143. package/dist/snapshot/index.d.ts +1 -1
  144. package/dist/snapshot/index.js +3 -3
  145. package/dist/subs/index.d.ts +372 -4
  146. package/dist/subs/index.js +4 -1
  147. package/dist/testing/index.d.ts +2 -2
  148. package/dist/testing/index.js +2 -2
  149. package/dist/throttled-input/index.d.ts +2 -2
  150. package/dist/throttled-input/index.js +14 -13
  151. package/dist/throttled-input/index.js.map +1 -1
  152. package/dist/token-refresh/index.d.ts +1 -1
  153. package/dist/token-refresh/index.js +3 -3
  154. package/dist/trace-replay/index.d.ts +12 -7
  155. package/dist/trace-replay/index.js +4 -3
  156. package/dist/{types-BoUWgwSm.d.ts → types-KbL3W1mj.d.ts} +1 -1
  157. package/dist/with-deadline/index.d.ts +1 -1
  158. package/dist/with-deadline/index.js +2 -2
  159. package/dist/with-resilience/index.d.ts +21 -2
  160. package/dist/with-resilience/index.js +12 -23
  161. package/dist/with-resilience/index.js.map +1 -1
  162. package/dist/with-telemetry/index.d.ts +1 -1
  163. package/dist/with-telemetry/index.js +2 -2
  164. package/dist/work-queue/adapter.d.ts +2 -2
  165. package/dist/work-queue/adapter.js +2 -2
  166. package/dist/work-queue/index.d.ts +3 -2
  167. package/dist/work-queue/index.js +1 -1
  168. package/dist/work-queue/index.js.map +1 -1
  169. package/dist/work-queue/ops.d.ts +2 -2
  170. package/dist/work-queue/ops.js +1 -1
  171. package/dist/workflow/index.d.ts +51 -19
  172. package/dist/workflow/index.js +71 -52
  173. package/dist/workflow/index.js.map +1 -1
  174. package/package.json +4 -1
  175. package/dist/chunk-2YOE5PIS.js.map +0 -1
  176. package/dist/chunk-3545BOYL.js.map +0 -1
  177. package/dist/chunk-5VNWZADP.js.map +0 -1
  178. package/dist/chunk-6GIGQT6C.js.map +0 -1
  179. package/dist/chunk-6VDCTR7N.js.map +0 -1
  180. package/dist/chunk-DYRKDCJI.js.map +0 -1
  181. package/dist/chunk-EGWTT5XJ.js.map +0 -1
  182. package/dist/chunk-EMJTPHI7.js +0 -91
  183. package/dist/chunk-EMJTPHI7.js.map +0 -1
  184. package/dist/chunk-FOGL3TC7.js.map +0 -1
  185. package/dist/chunk-IK4S5DJN.js.map +0 -1
  186. package/dist/chunk-M6UZT7HS.js.map +0 -1
  187. package/dist/chunk-NSDFA6M6.js +0 -198
  188. package/dist/chunk-NSDFA6M6.js.map +0 -1
  189. package/dist/chunk-SC3FAFZN.js.map +0 -1
  190. package/dist/chunk-SKAMZKTE.js +0 -20
  191. package/dist/chunk-SKAMZKTE.js.map +0 -1
  192. package/dist/chunk-TGG26XMK.js.map +0 -1
  193. package/dist/chunk-UV6UWGEO.js.map +0 -1
  194. package/dist/chunk-UYWUVEP6.js.map +0 -1
  195. package/dist/chunk-VFGKTGGC.js.map +0 -1
  196. package/dist/chunk-ZDRN3YCE.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/subs/types.ts"],"names":[],"mappings":";AA2CO,SAAS,iBAAA,CACd,UACA,GAAA,EACM;AACN,EAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,QAAA,CAAS,GAAG,CAAA;AAChC","file":"chunk-ZJ5AOYBH.js","sourcesContent":["// ---------------------------------------------------------------------------\n// SubscribeHandler<S, M, Ctx> — the structural shape of a single entry in\n// `Machine.subscribe`. Pulled out as a named type so factory return types\n// in this subpath can match the substrate's expected `subscribe` cell shape\n// without re-deriving it from `Machine`'s mapped type.\n//\n// A Sub factory in this subpath returns a `SubscribeHandler<S, M, Ctx>` —\n// the same `(sub, ctx, dispatch) => cleanup` shape the substrate expects in\n// `machine.subscribe[type]`. Callers assign the factory's return value\n// directly to a subscribe cell:\n//\n// subscribe: {\n// \"audit-idle\": fromPort(\n// (ctx) => ctx.auditRuntime,\n// auditIdlePort,\n// (idle) => ({ type: \"audit:idle\", idle }),\n// ),\n// }\n//\n// The factory absorbs the lifecycle (subscribe + cleanup); the caller keeps\n// the intent (which port, which msg). Strengthens invariant 9 (the surface\n// for cross-cutting Sub topologies is named, small, and exported from one\n// subpath instead of redrawn at every call site).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../index\";\n\nexport type SubscribeHandler<S extends Sub, M, Ctx> = (\n sub: S,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n) => () => void;\n\n// ---------------------------------------------------------------------------\n// dispatchIfPresent — the null-drop dispatch idiom every Sub factory in this\n// subpath (and the chrome/node factories) shares. A factory's callback maps a\n// platform event to `M | null`; `null` is the explicit \"drop this emission\"\n// signal — the event fired but the caller decided it isn't interesting given\n// current state, so no dispatch is made and the listener stays armed. Hoisted\n// here so the one drop-on-null rule lives once instead of re-inlined as\n// `const msg = fn(...); if (msg !== null) dispatch(msg);` at every listener.\n// ---------------------------------------------------------------------------\n\nexport function dispatchIfPresent<M>(\n dispatch: (msg: M) => void,\n msg: M | null,\n): void {\n if (msg !== null) dispatch(msg);\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { idempotencyMemory } from './chunk-BYQUGDD3.js';
2
- import { queueAdapter } from './chunk-HJO6VNQI.js';
2
+ import { queueAdapter } from './chunk-MU6MCIAC.js';
3
3
 
4
4
  // src/idempotent-intake/index.ts
5
5
  function createIntake(config) {
@@ -87,5 +87,5 @@ function createIntake(config) {
87
87
  }
88
88
 
89
89
  export { createIntake };
90
- //# sourceMappingURL=chunk-LEC4U6CX.js.map
91
- //# sourceMappingURL=chunk-LEC4U6CX.js.map
90
+ //# sourceMappingURL=chunk-ZVJZ72HY.js.map
91
+ //# sourceMappingURL=chunk-ZVJZ72HY.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-LEC4U6CX.js","sourcesContent":["/**\n * @demlik/tea/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue` and thread them by hand.\n */\n\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\nimport type { Cmd } from \"../index\";\nimport type { QueueItem } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport interface IntakeProcessCmd<P> extends Cmd<\"intake:process\"> {\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n}\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport interface IntakeReplayCmd<R> extends Cmd<\"intake:replay\"> {\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n}\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [{ type: \"intake:replay\", key, result: entry.result }]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [\n nextState,\n [{ type: \"intake:process\", key, itemId: item.id, payload }],\n ];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
1
+ {"version":3,"sources":["../src/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-ZVJZ72HY.js","sourcesContent":["/**\n * @demlik/tea/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue` and thread them by hand.\n */\n\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\nimport type { Cmd } from \"../index\";\nimport type { QueueItem } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport interface IntakeProcessCmd<P> extends Cmd<\"intake:process\"> {\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n}\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport interface IntakeReplayCmd<R> extends Cmd<\"intake:replay\"> {\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n}\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [{ type: \"intake:replay\", key, result: entry.result }]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [\n nextState,\n [{ type: \"intake:process\", key, itemId: item.id, payload }],\n ];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  /**
2
+ * @packageDocumentation
2
3
  * @demlik/tea/circuit-breaker — per-target failure tracking as pure state + ops.
3
4
  *
4
5
  * Same shape as `@demlik/tea/work-queue`'s `ops.ts`, `@demlik/tea/rate-limit`,
@@ -1,3 +1,3 @@
1
- export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess } from '../chunk-SC3FAFZN.js';
1
+ export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess } from '../chunk-UF4MZSLT.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -11,6 +11,7 @@ declare class NoCellError extends Error {
11
11
  readonly msgType: string;
12
12
  readonly stateName: string;
13
13
  readonly name = "NoCellError";
14
+ readonly _tag: "NoCellError";
14
15
  constructor(msgType: string, stateName: string);
15
16
  }
16
17
  declare function applyCell<S, M extends {
@@ -44,7 +45,7 @@ type Transitions<S extends {
44
45
  };
45
46
  };
46
47
  type Cmd<T extends string = string> = {
47
- type: T;
48
+ readonly type: T;
48
49
  };
49
50
  declare const Cmd: {
50
51
  /**
@@ -113,9 +114,46 @@ type SubId = string & {
113
114
  */
114
115
  declare function subId(s: string): SubId;
115
116
  type Sub<T extends string = string> = {
116
- id: SubId;
117
- type: T;
117
+ readonly id: SubId;
118
+ readonly type: T;
118
119
  };
120
+ type Dispose = () => void;
121
+ declare function structuralHash(deps: unknown): string;
122
+ interface DepKeyedSub<S, M, Ctx = unknown> {
123
+ /**
124
+ * The slice of state this Sub depends on. `null` ⇒ the Sub is inactive in
125
+ * this state (the substrate disposes it if it was running). Non-null ⇒ the
126
+ * Sub is active, keyed on `structuralHash(deps)`.
127
+ *
128
+ * Pure (invariant 2). Plain JSON-compatible data only (invariant 1) — the
129
+ * structural hash walks it deterministically.
130
+ */
131
+ readonly deps: (state: S) => unknown;
132
+ /**
133
+ * Open the resource for the current `state` (whose `deps` the kernel just
134
+ * found non-null). Receives `dispatch` to fire follow-up Msgs (a timer's
135
+ * fire, an inbound frame) and the machine's `ctx` (the host's transport
136
+ * factory, storage handles). Returns the `Dispose` the substrate runs on
137
+ * teardown / re-arm. Same role as a `subscribe[type]` handler's returned
138
+ * cleanup.
139
+ *
140
+ * `source` takes `state` (not the deps slice) so the slice type `D` never
141
+ * escapes the entry — each battery's `.depKeyed` closes over its own typed
142
+ * deps internally (cast-free existential: the entry that produced the deps
143
+ * is the entry that consumes them).
144
+ */
145
+ readonly source: (state: S, dispatch: (msg: M) => void, ctx: Ctx) => Dispose;
146
+ }
147
+ interface Identity<S, M> {
148
+ /** The identity THIS instance owns. Pure (invariant 2); plain value. */
149
+ readonly ofState: (state: S) => unknown;
150
+ /**
151
+ * The identity a message is addressed to, or `undefined` when the message
152
+ * carries no identity (lifecycle / pre-identity messages — never dropped).
153
+ * Pure (invariant 2); plain value when defined.
154
+ */
155
+ readonly ofMsg: (msg: M) => unknown;
156
+ }
119
157
  interface Port<T> {
120
158
  readonly __brand: "port";
121
159
  readonly name: string;
@@ -133,15 +171,25 @@ interface PortEmitter {
133
171
  emit<T>(port: Port<T>, value: T): void;
134
172
  }
135
173
  type NoCtx = Readonly<Record<never, never>>;
136
- /** Spelled-out alias for `NoCtx`, for sites that prefer the long name. */
174
+ /**
175
+ * Spelled-out alias for `NoCtx`.
176
+ *
177
+ * @deprecated Use {@link NoCtx}. One Way to Do Each Thing — `NoCtx` is the
178
+ * single non-deprecated name for the context-free-ctx marker (this alias has
179
+ * no in-tree uses). Kept as a re-export for one minor per the deprecate-don't-
180
+ * delete policy in `MAINTAINING.md`, then removed.
181
+ */
137
182
  type ContextFree = NoCtx;
138
183
  type Interpret<M extends {
139
184
  type: string;
140
185
  }, C extends Cmd, Ctx> = {
141
186
  [K in C["type"]]: (cmd: Extract<C, {
142
187
  type: K;
143
- }>, ctx: Ctx & PortEmitter) => Promise<M | void>;
188
+ }>, ctx: Ctx & PortEmitter, dispatch?: (msg: M) => void) => Promise<M | void>;
144
189
  };
190
+ type InterpretDetached<C extends Cmd, Allowed extends {
191
+ type: string;
192
+ }, Ctx> = (cmd: C, ctx: Ctx & PortEmitter, dispatch: (msg: Allowed) => void) => Promise<void>;
145
193
  type Subscribe<M extends {
146
194
  type: string;
147
195
  }, U extends Sub, Ctx> = {
@@ -172,6 +220,48 @@ type Machine<S, M extends {
172
220
  update: Reducer<S, M, C> | ([S] extends [{
173
221
  type: string;
174
222
  }] ? Transitions<S, M, C> : never);
223
+ /**
224
+ * Dep-keyed Subs. Each declares the state slice it depends on; the substrate
225
+ * derives the id (`structuralHash(deps)`) and the gate (`deps` non-null) and
226
+ * folds them into the SAME reconcile loop `subscriptions` feeds. The author
227
+ * never writes `subId(...)` and never lists Subs in a central
228
+ * `subscriptions(state)` aggregate — a per-Sub gate travels with the Sub, so
229
+ * a new phase can't silently forget to arm it.
230
+ *
231
+ * The deps slice type is erased to `unknown` per entry — each Sub's slice
232
+ * differs, but the slice type never crosses this field (the kernel only needs
233
+ * to hash it and pass `state` back to that same entry's `source`).
234
+ *
235
+ * Optional and independent of `subscriptions` / `subscribe`: a machine that
236
+ * omits `subs` reconciles exactly as before.
237
+ *
238
+ * Strengthens invariant 4 (lifecycle owned by the substrate) and invariant 7
239
+ * (identity derived, not hand-authored).
240
+ */
241
+ subs?: ReadonlyArray<DepKeyedSub<S, M, Ctx>>;
242
+ /**
243
+ * Instance-identity filter. Declares THIS instance's identity once; the
244
+ * substrate drops any message addressed to a DIFFERENT identity before it
245
+ * reaches `update`. Replaces the per-cell `if (msg.runId !== state.runId)`
246
+ * guard entirely — the reducer never sees a foreign-instance message, so no
247
+ * cell needs to check.
248
+ *
249
+ * Opt-in: a machine that omits `identity` skips the filter (every message
250
+ * reaches `update`, exactly as before). A message whose `ofMsg` returns
251
+ * `undefined` is identity-agnostic and always reaches `update` (lifecycle /
252
+ * pre-identity messages).
253
+ *
254
+ * Strengthens invariant 7 (identity is explicit — declared once, enforced by
255
+ * the substrate) and invariant 6 (mis-addressed messages dropped at one
256
+ * observable point, not per cell).
257
+ */
258
+ identity?: Identity<S, M>;
259
+ /**
260
+ * Manual Sub aggregate — the documented escape hatch, KEPT alongside `subs`.
261
+ * The machine that needs cross-Sub logic the per-Sub `deps` fold can't
262
+ * express lists Subs here; the substrate reconciles them via `subscribe`.
263
+ * Both paths feed ONE reconcile pass.
264
+ */
175
265
  subscriptions?: (state: S) => readonly U[];
176
266
  subscribe?: Subscribe<M, U, Ctx>;
177
267
  /**
@@ -194,4 +284,4 @@ declare function foldMsgs<S, M extends {
194
284
  type: string;
195
285
  }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, base: S, msgs: readonly M[]): S;
196
286
 
197
- export { Cmd as C, type Interpret as I, type Machine as M, type NoCtx as N, type Port as P, type Reducer as R, type Sub as S, type Transitions as T, type UpdateForm as U, type SubId as a, type ContextFree as b, type PortEmitter as c, type Subscribe as d, type SyncReturn as e, applyCell as f, detectUpdateForm as g, foldMsgs as h, formOf as i, NoCellError as j, msgKeysOf as m, subId as s };
287
+ export { Cmd as C, type DepKeyedSub as D, type InterpretDetached as I, type Machine as M, type NoCtx as N, type Port as P, type Reducer as R, type Sub as S, type Transitions as T, type UpdateForm as U, type SubId as a, type PortEmitter as b, type Interpret as c, type ContextFree as d, type Dispose as e, type Identity as f, type Subscribe as g, type SyncReturn as h, applyCell as i, detectUpdateForm as j, foldMsgs as k, formOf as l, msgKeysOf as m, NoCellError as n, subId as o, structuralHash as s };
@@ -1,5 +1,5 @@
1
- import { S as Sub } from '../core-BmlJOvxM.js';
2
- import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
1
+ import { S as Sub, a as SubId } from '../core-42kapW0n.js';
2
+ import { S as SubscribeHandler } from '../types-KbL3W1mj.js';
3
3
 
4
4
  /**
5
5
  * @demlik/tea/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock
@@ -29,6 +29,21 @@ import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
29
29
  * (invariant 2) — and is exercised in tests via vitest fake timers, matching
30
30
  * the no-injection convention of the `fromTimeout` / `fromInterval` exemplars.
31
31
  *
32
+ * WHAT BACKS THE TIMER IS THE HOST'S CHOICE. `setTimeout` is the universal
33
+ * default, but it is not universal: a Durable Object that hibernates has no
34
+ * live `setTimeout` to wake it, so its deadline must be backed by a `do_alarm`
35
+ * registered on the alarm registry; a test host wants a fake-timer schedule.
36
+ * That backing is the `ArmTimer` seam — `subscribeWith(armTimer)` builds the
37
+ * `subscribe.deadline` cell from a host-plugged timer, and `subscribeDeadline`
38
+ * IS `subscribeWith(setTimeoutArmTimer())`. One deadline surface, three hosts;
39
+ * there is no second Sub type and no second `atMs` anchor to keep in sync.
40
+ *
41
+ * The anchor is what makes the seam safe. `atMs` is ABSOLUTE, so whatever backs
42
+ * it arms to a fixed wall-clock instant: the host computes the gap itself
43
+ * (`atMs - Date.now()`), and on a host that just rehydrated after hibernation
44
+ * that gap is the SHRUNKEN remainder — the deadline honours the original target
45
+ * rather than resetting to a fresh full-length window.
46
+ *
32
47
  * Strengthens invariant 4 (external time is a subscription — the deadline is a
33
48
  * `Sub` the runtime reconciles, never a `setTimeout` leaked into a reducer) and
34
49
  * invariant 7 (identity is explicit — the Sub carries a stable `SubId`, so the
@@ -105,15 +120,66 @@ type DeadlineExceeded = {
105
120
  */
106
121
  declare function deadlineSub(id: string, atMs: number, opts?: DeadlineOpts): DeadlineSub;
107
122
  /**
108
- * The `subscribe["deadline"]` cell. Arms a one-shot timer for the remaining
109
- * delay `max(0, atMs - Date.now())` and dispatches `deadlineExceeded(...)` when
110
- * it fires; returns a cleanup that clears the pending timer.
123
+ * The host-plugged timer backing. Given the deadline's stable `id`, its
124
+ * absolute `atMs`, and the Msg to fire, arm the platform-appropriate timer
125
+ * (DO alarm registry, `setTimeout`, fake timer) and return a cleanup that
126
+ * cancels the pending fire.
127
+ *
128
+ * `id` is the Sub's reconcile id — a registry-backed host (the DO alarm slot)
129
+ * keys its entry on `id` so the cleanup deletes the EXACT entry the substrate
130
+ * reconciled. A `setTimeout`-backed host ignores `id` (the closure holds the
131
+ * handle).
132
+ *
133
+ * The host computes the gap itself: `atMs - Date.now()` is the REMAINING time.
134
+ * On a host that just rehydrated after hibernation that gap is the shrunken
135
+ * remainder — the deadline is NOT reset to its full length. That recomputation
136
+ * is the reason `atMs` rides on the Sub as an absolute instant rather than a
137
+ * delay: the anchor is what every backing agrees on.
138
+ */
139
+ type ArmTimer<M> = (id: SubId, atMs: number, msg: M, dispatch: (msg: M) => void) => () => void;
140
+ /**
141
+ * Build the `subscribe["deadline"]` cell from a host-plugged `armTimer`. This
142
+ * module owns the Sub shape, the anchor, and the Msg; the HOST owns what backs
143
+ * the timer. The returned handler reads `id` + `atMs` off the Sub, builds the
144
+ * `deadlineExceeded(...)` Msg, and hands all three to `armTimer`.
145
+ *
146
+ * Use it when `setTimeout` is the wrong backing — most concretely a Durable
147
+ * Object, which hibernates and must register a `do_alarm` instead:
148
+ *
149
+ * subscribe: {
150
+ * deadline: subscribeWith((id, atMs, msg, dispatch) =>
151
+ * alarms.register(id, atMs, () => dispatch(msg)),
152
+ * ),
153
+ * }
154
+ *
155
+ * For the `setTimeout` default, use {@link subscribeDeadline} — it is exactly
156
+ * `subscribeWith(setTimeoutArmTimer())`, so there is one deadline surface and
157
+ * one anchor, not a second one per host.
158
+ */
159
+ declare function subscribeWith(armTimer: ArmTimer<DeadlineExceeded>): SubscribeHandler<DeadlineSub, DeadlineExceeded, unknown>;
160
+ /**
161
+ * The `setTimeout` timer backing — for node / browser / any host whose timer is
162
+ * a plain `setTimeout`. Arms for the REMAINING time (`max(0, atMs - Date.now())`,
163
+ * floored at 0 so an already-past deadline fires on the NEXT tick rather than
164
+ * synchronously inside the reconcile pass), so a deadline re-derived after a
165
+ * rehydrate fires at the original instant, not a fresh full window.
166
+ *
167
+ * Composes on `fromTimeout` rather than redrawing `setTimeout` /
168
+ * `clearTimeout`: this package has ONE timer lifecycle, and the arm-timer seam
169
+ * plugs into it instead of forking it. A hibernating host does NOT use this —
170
+ * it plugs its own `armTimer` (a `do_alarm` registration) into
171
+ * {@link subscribeWith}.
172
+ */
173
+ declare function setTimeoutArmTimer(): ArmTimer<DeadlineExceeded>;
174
+ /**
175
+ * The `subscribe["deadline"]` cell for the DEFAULT `setTimeout` backing. Arms a
176
+ * one-shot timer for the remaining delay `max(0, atMs - Date.now())` and
177
+ * dispatches `deadlineExceeded(...)` when it fires; returns a cleanup that
178
+ * clears the pending timer.
111
179
  *
112
- * Composes on `fromTimeout`: it adapts the absolute `atMs` on the Sub to the
113
- * relative `delayMs` that `fromTimeout` schedules against, then hands off the
114
- * `setTimeout` / `clearTimeout` lifecycle wholesale. A past deadline collapses
115
- * to `delayMs: 0`, so `fromTimeout` fires on the next tick — asynchronously,
116
- * never inside the reconcile pass.
180
+ * It is `subscribeWith(setTimeoutArmTimer())` — the default backing named, not
181
+ * a separate implementation, so the host-plugged path and the default path can
182
+ * never disagree about the anchor.
117
183
  *
118
184
  * Assign directly to a `Subscribe` cell:
119
185
  *
@@ -129,4 +195,4 @@ declare const subscribeDeadline: SubscribeHandler<DeadlineSub, DeadlineExceeded,
129
195
  */
130
196
  declare function deadlineExceeded(id: string, atMs: number): DeadlineExceeded;
131
197
 
132
- export { type DeadlineExceeded, type DeadlineOpts, type DeadlineSub, deadlineExceeded, deadlineSub, subscribeDeadline };
198
+ export { type ArmTimer, type DeadlineExceeded, type DeadlineOpts, type DeadlineSub, deadlineExceeded, deadlineSub, setTimeoutArmTimer, subscribeDeadline, subscribeWith };
@@ -1,6 +1,6 @@
1
- export { deadlineExceeded, deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
1
+ export { deadlineExceeded, deadlineSub, setTimeoutArmTimer, subscribeDeadline, subscribeWith } from '../chunk-RMX4EOQK.js';
2
2
  import '../chunk-2YFLOCWX.js';
3
- import '../chunk-6VDCTR7N.js';
4
- import '../chunk-DYRKDCJI.js';
3
+ import '../chunk-D4NJBQAH.js';
4
+ import '../chunk-PBDYW2TU.js';
5
5
  //# sourceMappingURL=index.js.map
6
6
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
+ import { typeOf, indexPath, unionKeys, keyPath } from '../chunk-EWJP3YYP.js';
1
2
  import { jsx, jsxs } from 'react/jsx-runtime';
2
3
  import { useRef, useEffect, useState, useCallback } from 'react';
3
4
 
4
- // src/devtools/msg-log.tsx
5
5
  function MsgLog({ history, className }) {
6
6
  return /* @__PURE__ */ jsx("div", { className: `tea-dt-log${className ? ` ${className}` : ""}`, children: history.map((r, i) => /* @__PURE__ */ jsxs(
7
7
  "div",
@@ -17,13 +17,6 @@ function MsgLog({ history, className }) {
17
17
  }
18
18
 
19
19
  // src/devtools/state-diff.ts
20
- function typeOf(v) {
21
- if (v === null) return "null";
22
- if (v === void 0) return "undefined";
23
- if (Array.isArray(v)) return "array";
24
- if (typeof v === "object") return "object";
25
- return "primitive";
26
- }
27
20
  function diffState(expected, actual) {
28
21
  const out = [];
29
22
  walk(expected, actual, "state", out);
@@ -50,16 +43,15 @@ function walk(expected, actual, path, out) {
50
43
  const aa = actual;
51
44
  const len = Math.max(ae.length, aa.length);
52
45
  for (let i = 0; i < len; i++) {
53
- walk(ae[i], aa[i], `${path}[${i}]`, out);
46
+ walk(ae[i], aa[i], indexPath(path, i), out);
54
47
  }
55
48
  return;
56
49
  }
57
50
  if (te === "object") {
58
51
  const oe = expected;
59
52
  const oa = actual;
60
- const keys = [.../* @__PURE__ */ new Set([...Object.keys(oe), ...Object.keys(oa)])].sort();
61
- for (const key of keys) {
62
- walk(oe[key], oa[key], `${path}.${key}`, out);
53
+ for (const key of unionKeys(oe, oa)) {
54
+ walk(oe[key], oa[key], keyPath(path, key), out);
63
55
  }
64
56
  return;
65
57
  }
@@ -96,36 +88,24 @@ function changeRow(c) {
96
88
  return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-changed", children: [
97
89
  /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "~" }),
98
90
  /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
99
- /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmt(c.expected) }),
91
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmtValue(c.expected) }),
100
92
  /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-arrow", children: "\u2192" }),
101
- /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmt(c.actual) })
93
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmtValue(c.actual) })
102
94
  ] }, c.path);
103
95
  case "removed":
104
96
  return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-removed", children: [
105
97
  /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "-" }),
106
98
  /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
107
- /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmt(c.expected) })
99
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmtValue(c.expected) })
108
100
  ] }, c.path);
109
101
  case "added":
110
102
  return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-added", children: [
111
103
  /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "+" }),
112
104
  /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
113
- /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmt(c.actual) })
105
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmtValue(c.actual) })
114
106
  ] }, c.path);
115
107
  }
116
108
  }
117
- function fmt(v) {
118
- if (v === void 0) return "undefined";
119
- if (typeof v === "number" && Number.isNaN(v)) return "NaN";
120
- if (typeof v === "bigint") return `${v}n`;
121
- if (typeof v === "string") return JSON.stringify(v);
122
- if (typeof v === "function" || typeof v === "symbol") return String(v);
123
- try {
124
- return JSON.stringify(v) ?? String(v);
125
- } catch {
126
- return String(v);
127
- }
128
- }
129
109
  function StateDiff({ expected, actual, className }) {
130
110
  const changes = diffState(expected, actual);
131
111
  return /* @__PURE__ */ jsx(