@demlik/tea 0.1.0 → 0.1.1

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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
  18. package/dist/chunk-2WZHS4P2.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-5VNWZADP.js +49 -0
  24. package/dist/chunk-5VNWZADP.js.map +1 -0
  25. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  26. package/dist/chunk-7FYQGDMF.js.map +1 -0
  27. package/dist/chunk-7VEG4KHB.js +202 -0
  28. package/dist/chunk-7VEG4KHB.js.map +1 -0
  29. package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
  30. package/dist/chunk-A56GNPI7.js.map +1 -0
  31. package/dist/chunk-BNJYE4AU.js +157 -0
  32. package/dist/chunk-BNJYE4AU.js.map +1 -0
  33. package/dist/chunk-BYQUGDD3.js +16 -0
  34. package/dist/chunk-BYQUGDD3.js.map +1 -0
  35. package/dist/chunk-D5EQDLDV.js +186 -0
  36. package/dist/chunk-D5EQDLDV.js.map +1 -0
  37. package/dist/chunk-EDGDR5XX.js +60 -0
  38. package/dist/chunk-EDGDR5XX.js.map +1 -0
  39. package/dist/chunk-EIZJTYKR.js +93 -0
  40. package/dist/chunk-EIZJTYKR.js.map +1 -0
  41. package/dist/chunk-EQRUAP2A.js +56 -0
  42. package/dist/chunk-EQRUAP2A.js.map +1 -0
  43. package/dist/chunk-FH5RUIIL.js +61 -0
  44. package/dist/chunk-FH5RUIIL.js.map +1 -0
  45. package/dist/chunk-HJO6VNQI.js +15 -0
  46. package/dist/chunk-HJO6VNQI.js.map +1 -0
  47. package/dist/chunk-IFBQJYGI.js +132 -0
  48. package/dist/chunk-IFBQJYGI.js.map +1 -0
  49. package/dist/chunk-IN33Q4VJ.js +52 -0
  50. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  51. package/dist/chunk-JELSZN25.js +47 -0
  52. package/dist/chunk-JELSZN25.js.map +1 -0
  53. package/dist/chunk-K575PT4S.js +58 -0
  54. package/dist/chunk-K575PT4S.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  58. package/dist/chunk-NSDFA6M6.js.map +1 -0
  59. package/dist/chunk-QGZW4W3E.js +20 -0
  60. package/dist/chunk-QGZW4W3E.js.map +1 -0
  61. package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
  62. package/dist/chunk-RGTPPF4U.js.map +1 -0
  63. package/dist/chunk-RVJFGRGK.js +222 -0
  64. package/dist/chunk-RVJFGRGK.js.map +1 -0
  65. package/dist/chunk-RVU3RBE6.js +31 -0
  66. package/dist/chunk-RVU3RBE6.js.map +1 -0
  67. package/dist/chunk-SC3FAFZN.js +53 -0
  68. package/dist/chunk-SC3FAFZN.js.map +1 -0
  69. package/dist/chunk-SINXE47Y.js +55 -0
  70. package/dist/chunk-SINXE47Y.js.map +1 -0
  71. package/dist/chunk-TAO4YOLO.js +87 -0
  72. package/dist/chunk-TAO4YOLO.js.map +1 -0
  73. package/dist/chunk-TC4HWABA.js +506 -0
  74. package/dist/chunk-TC4HWABA.js.map +1 -0
  75. package/dist/chunk-TGG26XMK.js +52 -0
  76. package/dist/chunk-TGG26XMK.js.map +1 -0
  77. package/dist/chunk-UV6UWGEO.js +147 -0
  78. package/dist/chunk-UV6UWGEO.js.map +1 -0
  79. package/dist/chunk-UYWUVEP6.js +158 -0
  80. package/dist/chunk-UYWUVEP6.js.map +1 -0
  81. package/dist/chunk-WUV3PSCZ.js +54 -0
  82. package/dist/chunk-WUV3PSCZ.js.map +1 -0
  83. package/dist/chunk-XRXKH7KA.js +268 -0
  84. package/dist/chunk-XRXKH7KA.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-CymXZCeY.d.ts +178 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.js +1 -54
  92. package/dist/debounce/index.js.map +1 -1
  93. package/dist/devtools/index.d.ts +1 -1
  94. package/dist/devtools/index.js +18 -6
  95. package/dist/devtools/index.js.map +1 -1
  96. package/dist/do/index.d.ts +1732 -92
  97. package/dist/do/index.js +677 -31
  98. package/dist/do/index.js.map +1 -1
  99. package/dist/durable-effects-2MDxDs45.d.ts +180 -0
  100. package/dist/extension/index.d.ts +31 -13
  101. package/dist/extension/index.js +5 -3
  102. package/dist/extension/index.js.map +1 -1
  103. package/dist/extension/react.js +8 -3
  104. package/dist/extension/react.js.map +1 -1
  105. package/dist/extension/subs/index.d.ts +2 -2
  106. package/dist/extension/subs/index.js.map +1 -1
  107. package/dist/extension/test-utils.js +11 -3
  108. package/dist/extension/test-utils.js.map +1 -1
  109. package/dist/fan-out/index.d.ts +239 -0
  110. package/dist/fan-out/index.js +5 -0
  111. package/dist/fan-out/index.js.map +1 -0
  112. package/dist/idempotency/adapter.d.ts +62 -0
  113. package/dist/idempotency/adapter.js +4 -0
  114. package/dist/idempotency/adapter.js.map +1 -0
  115. package/dist/idempotency/index.js +1 -56
  116. package/dist/idempotency/index.js.map +1 -1
  117. package/dist/idempotent-intake/index.d.ts +186 -0
  118. package/dist/idempotent-intake/index.js +7 -0
  119. package/dist/idempotent-intake/index.js.map +1 -0
  120. package/dist/index.d.ts +341 -191
  121. package/dist/index.js +2 -1
  122. package/dist/llm-call/index.d.ts +309 -0
  123. package/dist/llm-call/index.js +14 -0
  124. package/dist/llm-call/index.js.map +1 -0
  125. package/dist/machine-viz/index.d.ts +1 -1
  126. package/dist/machine-viz/index.js +1 -1
  127. package/dist/machine-viz/index.js.map +1 -1
  128. package/dist/mem/index.d.ts +1 -0
  129. package/dist/monitored-run/index.d.ts +253 -0
  130. package/dist/monitored-run/index.js +10 -0
  131. package/dist/monitored-run/index.js.map +1 -0
  132. package/dist/node/index.d.ts +5 -5
  133. package/dist/node/index.js.map +1 -1
  134. package/dist/paginated-walk/index.d.ts +234 -0
  135. package/dist/paginated-walk/index.js +15 -0
  136. package/dist/paginated-walk/index.js.map +1 -0
  137. package/dist/paginator/index.d.ts +271 -0
  138. package/dist/paginator/index.js +3 -0
  139. package/dist/paginator/index.js.map +1 -0
  140. package/dist/parity/index.d.ts +86 -0
  141. package/dist/parity/index.js +77 -0
  142. package/dist/parity/index.js.map +1 -0
  143. package/dist/pbt/arbitraries/index.d.ts +1 -1
  144. package/dist/pbt/arbitraries/index.js +1 -1
  145. package/dist/pbt/index.d.ts +1 -1
  146. package/dist/pbt/index.js +5 -4
  147. package/dist/pbt/index.js.map +1 -1
  148. package/dist/pbt/runners/index.d.ts +1 -1
  149. package/dist/pbt/runners/index.js +3 -2
  150. package/dist/poller/index.d.ts +288 -0
  151. package/dist/poller/index.js +79 -0
  152. package/dist/poller/index.js.map +1 -0
  153. package/dist/prediction/index.d.ts +109 -0
  154. package/dist/prediction/index.js +4 -0
  155. package/dist/prediction/index.js.map +1 -0
  156. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  157. package/dist/pure/index.d.ts +2 -0
  158. package/dist/pure/index.js +4 -0
  159. package/dist/pure/index.js.map +1 -0
  160. package/dist/raft/demo.d.ts +105 -0
  161. package/dist/raft/demo.js +172 -0
  162. package/dist/raft/demo.js.map +1 -0
  163. package/dist/raft/index.d.ts +548 -0
  164. package/dist/raft/index.js +7 -0
  165. package/dist/raft/index.js.map +1 -0
  166. package/dist/raft/sim.d.ts +222 -0
  167. package/dist/raft/sim.js +8 -0
  168. package/dist/raft/sim.js.map +1 -0
  169. package/dist/raft/timeline.d.ts +46 -0
  170. package/dist/raft/timeline.js +62 -0
  171. package/dist/raft/timeline.js.map +1 -0
  172. package/dist/rate-limit/index.js +1 -43
  173. package/dist/rate-limit/index.js.map +1 -1
  174. package/dist/react/index.d.ts +2 -1
  175. package/dist/react/index.js +39 -20
  176. package/dist/react/index.js.map +1 -1
  177. package/dist/reconciler/index.d.ts +192 -0
  178. package/dist/reconciler/index.js +166 -0
  179. package/dist/reconciler/index.js.map +1 -0
  180. package/dist/recorder/index.d.ts +7 -3
  181. package/dist/recorder/index.js +1 -155
  182. package/dist/recorder/index.js.map +1 -1
  183. package/dist/resilient-call/index.d.ts +251 -0
  184. package/dist/resilient-call/index.js +13 -0
  185. package/dist/resilient-call/index.js.map +1 -0
  186. package/dist/retry-backoff/index.d.ts +42 -5
  187. package/dist/retry-backoff/index.js +1 -35
  188. package/dist/retry-backoff/index.js.map +1 -1
  189. package/dist/saga/index.d.ts +225 -0
  190. package/dist/saga/index.js +150 -0
  191. package/dist/saga/index.js.map +1 -0
  192. package/dist/snapshot/index.d.ts +334 -0
  193. package/dist/snapshot/index.js +5 -0
  194. package/dist/snapshot/index.js.map +1 -0
  195. package/dist/subs/index.d.ts +48 -10
  196. package/dist/subs/index.js +1 -1
  197. package/dist/testing/index.d.ts +107 -2
  198. package/dist/testing/index.js +204 -6
  199. package/dist/testing/index.js.map +1 -1
  200. package/dist/throttle/index.js +1 -52
  201. package/dist/throttle/index.js.map +1 -1
  202. package/dist/throttled-input/index.d.ts +372 -0
  203. package/dist/throttled-input/index.js +88 -0
  204. package/dist/throttled-input/index.js.map +1 -0
  205. package/dist/token-refresh/index.d.ts +215 -0
  206. package/dist/token-refresh/index.js +6 -0
  207. package/dist/token-refresh/index.js.map +1 -0
  208. package/dist/trace-replay/index.d.ts +11 -2
  209. package/dist/trace-replay/index.js +3 -55
  210. package/dist/trace-replay/index.js.map +1 -1
  211. package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
  212. package/dist/with-deadline/index.d.ts +196 -0
  213. package/dist/with-deadline/index.js +130 -0
  214. package/dist/with-deadline/index.js.map +1 -0
  215. package/dist/with-resilience/index.d.ts +205 -0
  216. package/dist/with-resilience/index.js +274 -0
  217. package/dist/with-resilience/index.js.map +1 -0
  218. package/dist/with-telemetry/index.d.ts +149 -0
  219. package/dist/with-telemetry/index.js +95 -0
  220. package/dist/with-telemetry/index.js.map +1 -0
  221. package/dist/work-queue/adapter.d.ts +82 -0
  222. package/dist/work-queue/adapter.js +4 -0
  223. package/dist/work-queue/adapter.js.map +1 -0
  224. package/dist/work-queue/index.d.ts +25 -11
  225. package/dist/work-queue/index.js +1 -46
  226. package/dist/work-queue/index.js.map +1 -1
  227. package/dist/work-queue/ops.d.ts +69 -0
  228. package/dist/work-queue/ops.js +3 -0
  229. package/dist/work-queue/ops.js.map +1 -0
  230. package/dist/workflow/index.d.ts +444 -0
  231. package/dist/workflow/index.js +225 -0
  232. package/dist/workflow/index.js.map +1 -0
  233. package/package.json +162 -25
  234. package/dist/chunk-CMG2KMWX.js.map +0 -1
  235. package/dist/chunk-PYBQREFU.js.map +0 -1
  236. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  237. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  238. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/raft/index.ts"],"names":[],"mappings":"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","file":"chunk-XRXKH7KA.js","sourcesContent":["/**\n * @demlik/tea/raft — Raft (Figure 2) as a pure TEA reducer. Slices #119 (the\n * election half) + #120 (the log-replication half) of the consensus epic\n * (#117): the role state machine (`follower → candidate → leader`), election +\n * heartbeat timers as `DeadlineSub`s, `RequestVote` request/reply handling, and\n * — added in #120 — the real replicated log with the Figure-2 `AppendEntries`\n * consistency check, leader `nextIndex`/`matchIndex` bookkeeping, and\n * `commitIndex` advancement under the Figure-8 current-term safety rule.\n * Persistence/cold-wake (#122) and the safety-property suite + multi-node\n * harness (#121/#123) are LATER siblings — out of scope here.\n *\n * The Raft paper (\"In Search of an Understandable Consensus Algorithm\",\n * raft.github.io) maps onto TEA almost 1:1, and this module is the proof of\n * that mapping:\n *\n * - **Reducer** — the Raft transition function over `RaftState`. PURE: it\n * reads no ambient clock and no ambient RNG. Wall time arrives as the `at`\n * parameter every verb takes (the firing Msg carries it, same discipline\n * as `../poller`); election-timeout jitter comes from the `rng` injected\n * ONCE at `createRaftNode` (default `Math.random` at the effect boundary, a\n * fixed value in tests). No verb body names the global RNG or `Date.now`,\n * so a node driven by a recorded `(timer × message)` schedule re-decides\n * bit-for-bit — the headline replay-testing payoff of the epic.\n * - **Sub** — the election-timeout and heartbeat timers are `DeadlineSub`s\n * (`../deadline`, the same shape `../poller` / the agent layer arm). A\n * fired timer is a `Msg` the consumer routes back into a verb; the verb\n * never owns a `setTimeout`.\n * - **Cmd** — the RPCs (`RequestVote` request + reply, `AppendEntries`\n * request + reply) are `Cmd`s routed through the consumer's `interpret`\n * over an injected transport port. This module emits the Cmd literals; it\n * performs NO networking. Tests assert against the emitted Cmds and fake\n * the transport.\n *\n * Invalid states are unrepresentable: the leader-only volatile state\n * (`nextIndex` / `matchIndex`) lives ONLY on the `leader` variant of the role\n * union, so a follower or candidate cannot carry — or be asked for — replication\n * bookkeeping it has no business holding (#119 acceptance criterion).\n *\n * **Persistence seam (#120 → #122).** The replicated log *is* the event log\n * (epic Approach). The verbs here keep `log` a pure value computed by folding —\n * a client command or a follower-side `AppendEntries` append/truncate is a\n * deterministic function of `(state, msg)`, with no clock/RNG and no mutation.\n * That is exactly the port the event-sourced store under `../do`\n * (`doEventSourcedStore`) folds over: its `replay(machine, { loaded, msgs })`\n * re-runs this same reducer to rebuild `log` byte-identically. #120 therefore\n * routes the log through the pure-reducer seam and defers the *effectful* host\n * wiring (async persist-before-respond, snapshot retention, cold-wake replay)\n * to #122 — see {@link RaftState.log} and {@link RaftNode.onClientCommand}.\n *\n * Generic over the command payload `C` (`unknown` by default): a `LogEntry`\n * carries an opaque `command` the consumer's state machine interprets on\n * commit. Raft itself never inspects it.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths (`../deadline`)\n * and the core `Cmd` / `Sub` types. Consumers reach it via the\n * `@demlik/tea/raft` subpath.\n */\n\nimport { type DeadlineSub, deadlineSub } from \"../deadline\";\nimport { type Cmd, type Sub, subId } from \"../index\";\n\n// ===========================================================================\n// Identity & config\n// ===========================================================================\n\n/**\n * A Raft node identity. Opaque string; the cluster's `peers` set lists every\n * OTHER node (self excluded). Branded-by-convention only — kept a plain string\n * so transport adapters can use it as an address key without unwrapping.\n */\nexport type NodeId = string;\n\n/**\n * A Raft term — a monotonically increasing logical clock. Starts at 0 (no\n * leader elected yet) and bumps by one each time a node starts an election.\n */\nexport type Term = number;\n\n/**\n * The election-timeout window, in milliseconds. Raft randomizes each node's\n * timeout uniformly in `[minMs, maxMs]` so split votes resolve quickly — the\n * single source of liveness in leader election. The concrete duration is drawn\n * from the injected `rng` (never `Math.random` ambiently), so a fixed `rng`\n * pins the timeout and the run replays identically.\n */\nexport interface ElectionTimeout {\n /** Lower bound of the randomized election timeout, inclusive. */\n readonly minMs: number;\n /** Upper bound of the randomized election timeout, inclusive. */\n readonly maxMs: number;\n}\n\n/**\n * Static configuration for one Raft node, injected at construction. Pure data:\n * no node reads its own id or peer set from any ambient source.\n */\nexport interface RaftConfig {\n /** This node's identity. */\n readonly self: NodeId;\n /**\n * Every OTHER node in the cluster (self excluded). Cluster size is\n * `peers.length + 1`; a majority is `floor((peers.length + 1) / 2) + 1`.\n */\n readonly peers: readonly NodeId[];\n /** The randomized election-timeout window (see {@link ElectionTimeout}). */\n readonly electionTimeout: ElectionTimeout;\n /**\n * The leader's heartbeat interval, in milliseconds. A leader re-arms an empty\n * `AppendEntries` to every peer this often; it MUST be comfortably below\n * `electionTimeout.minMs` so followers never time out under a live leader.\n */\n readonly heartbeatMs: number;\n}\n\n// ===========================================================================\n// Log entry — the real { term, index, command } (#120)\n// ===========================================================================\n\n/**\n * One entry in the replicated log (Figure 2). Generic over the command payload\n * `C` (`unknown` by default): Raft itself never inspects `command` — it\n * replicates and orders the opaque blob, and the consumer's state machine\n * applies it once the entry commits.\n *\n * `index` is 1-based (Raft convention: index 0 means \"before the log\"; the\n * first real entry is index 1), so an empty log has `lastLogIndex` 0. It is\n * stored on the entry rather than derived from array position so the\n * consistency check and the leader's per-peer bookkeeping read it directly,\n * and so a persisted/replayed log carries its own indices.\n */\nexport interface LogEntry<C = unknown> {\n /** The term in which the leader created this entry. */\n readonly term: Term;\n /** This entry's 1-based position in the log. */\n readonly index: number;\n /** The opaque command this entry replicates; applied by the consumer on commit. */\n readonly command: C;\n}\n\n// ===========================================================================\n// Role union — invalid states unrepresentable\n// ===========================================================================\n\n/**\n * The role discriminated union. `_tag` is the discriminant (engine convention).\n *\n * The crux of \"make invalid states unrepresentable\": the leader-only volatile\n * state — `nextIndex` / `matchIndex`, the per-peer replication bookkeeping —\n * exists ONLY on the `leader` variant. A follower or candidate value has no\n * such field to read or mis-set; the type makes \"a follower with a matchIndex\n * map\" unspellable rather than merely discouraged (#119 acceptance criterion).\n *\n * `candidate` carries the per-election vote tally (`votesGranted`, including\n * self) so the reducer can decide majority without re-deriving it.\n */\nexport type Role =\n | { readonly _tag: \"follower\" }\n | {\n readonly _tag: \"candidate\";\n /**\n * The set of nodes (self + peers) that have granted this candidate a vote\n * THIS term. A `Set`-by-id semantic kept as a readonly array of unique\n * ids; the reducer dedups on insert so a duplicate reply never\n * double-counts toward the majority.\n */\n readonly votesGranted: readonly NodeId[];\n }\n | {\n readonly _tag: \"leader\";\n /**\n * For each peer, the index of the next log entry to send (Figure 2,\n * leader volatile state). Initialized to the leader's last-log index + 1\n * on election; #120 advances it on `AppendEntries`. Keyed by `NodeId`.\n */\n readonly nextIndex: Readonly<Record<NodeId, number>>;\n /**\n * For each peer, the highest log index known to be replicated (Figure 2,\n * leader volatile state). Initialized to 0; #120 advances it. Keyed by\n * `NodeId`.\n */\n readonly matchIndex: Readonly<Record<NodeId, number>>;\n };\n\n// ===========================================================================\n// RaftState — persistent + volatile, role-discriminated\n// ===========================================================================\n\n/**\n * The whole state of one Raft node, generic over the command payload `C`.\n * Persistent state (`currentTerm`, `votedFor`, `log`) survives crashes in a\n * real deployment (#122); volatile state (`commitIndex`, the leader's\n * `nextIndex`/`matchIndex`) is rebuilt on restart.\n */\nexport interface RaftState<C = unknown> {\n /**\n * Latest term this node has seen (Figure 2, persistent). Init 0; increases\n * monotonically — every higher-term message observed bumps it (and steps the\n * node down to follower).\n */\n readonly currentTerm: Term;\n /**\n * The candidate this node voted for in `currentTerm`, or `null` if it has not\n * voted this term (Figure 2, persistent). Reset to `null` whenever\n * `currentTerm` advances.\n */\n readonly votedFor: NodeId | null;\n /**\n * The replicated log (Figure 2, persistent). 1-based: `log[i]` has\n * `index === i + 1`; `lastLogIndex` is `log.length`. The log *is* the event\n * log (epic Approach): every mutation here — a leader appending a client\n * command, a follower truncating-and-appending on `AppendEntries` — is a pure\n * function of `(state, msg)`, so the event-sourced store under `../do` rebuilds\n * it by folding this same reducer over the recorded Msgs (#122 wires the host;\n * see the module header's persistence-seam note).\n */\n readonly log: readonly LogEntry<C>[];\n /**\n * Highest log index known to be committed (Figure 2, volatile). Init 0. A\n * follower advances it to `min(leaderCommit, lastNewEntryIndex)` on a matching\n * `AppendEntries`; a leader advances it under the Figure-8 current-term\n * majority rule (see {@link RaftNode.onAppendEntriesReply}). Entries up to and\n * including `commitIndex` are durably agreed and safe for the consumer to apply.\n */\n readonly commitIndex: number;\n /** The current role + its role-specific state (see {@link Role}). */\n readonly role: Role;\n}\n\n// ===========================================================================\n// Messages — fired timers + inbound RPCs, the verbs' inputs\n// ===========================================================================\n\n/**\n * Tagged wire shapes the consumer unions into its machine's Msg type and routes\n * into the matching verb. `_tag` is the discriminant. Each carries the sender's\n * `term` (Figure 2: every RPC carries the sender's `currentTerm`) so the\n * \"rules for all servers\" higher-term step-down applies uniformly.\n */\n\n/** Inbound `RequestVote` RPC — a candidate asking this node for its vote. */\nexport interface RequestVoteRequest {\n readonly _tag: \"request_vote_request\";\n /** The candidate's term. */\n readonly term: Term;\n /** The candidate requesting the vote. */\n readonly candidateId: NodeId;\n /**\n * Index of the candidate's last log entry (Figure 2). Read by the up-to-date\n * check — STUBBED in #119 (see {@link logIsUpToDate}); real comparison in #120.\n */\n readonly lastLogIndex: number;\n /** Term of the candidate's last log entry (Figure 2). Stub-read in #119. */\n readonly lastLogTerm: Term;\n}\n\n/** Inbound `RequestVote` reply — a peer answering this node's vote request. */\nexport interface RequestVoteReply {\n readonly _tag: \"request_vote_reply\";\n /** The replier's `currentTerm`, for the candidate to update itself. */\n readonly term: Term;\n /** The peer that replied (so the candidate can dedup its tally). */\n readonly from: NodeId;\n /** Whether the peer granted its vote. */\n readonly voteGranted: boolean;\n}\n\n/**\n * Inbound `AppendEntries` RPC (Figure 2). An empty `entries` array is a\n * heartbeat; a non-empty one replicates log entries. The `prevLog*` pair anchors\n * the consistency check, and `leaderCommit` carries the leader's commit index so\n * the follower can advance its own.\n */\nexport interface AppendEntriesRequest<C = unknown> {\n readonly _tag: \"append_entries_request\";\n /** The leader's term. */\n readonly term: Term;\n /** The leader sending this (so a follower can track the leader). */\n readonly leaderId: NodeId;\n /**\n * Index of the log entry immediately preceding `entries` (0 if `entries`\n * starts at the head of the log). The follower must already hold an entry at\n * this index whose term is `prevLogTerm`, or it rejects (consistency check).\n */\n readonly prevLogIndex: number;\n /** Term of the entry at `prevLogIndex` (0 when `prevLogIndex` is 0). */\n readonly prevLogTerm: Term;\n /** The new entries to store (empty for a heartbeat). Already index-stamped. */\n readonly entries: readonly LogEntry<C>[];\n /** The leader's `commitIndex`, so the follower can advance its own. */\n readonly leaderCommit: number;\n}\n\n/**\n * Inbound `AppendEntries` reply — a follower answering this leader's\n * replication RPC (Figure 2 reply shape). `matchIndex` is the index of the last\n * entry the follower now agrees on (0 if none), used by the leader to advance\n * `matchIndex`/`nextIndex` on success without re-deriving it.\n */\nexport interface AppendEntriesReply {\n readonly _tag: \"append_entries_reply\";\n /** The replier's `currentTerm`, for the leader to update itself / step down. */\n readonly term: Term;\n /** The follower that replied (so the leader can update its per-peer bookkeeping). */\n readonly from: NodeId;\n /** Whether the consistency check passed and the entries were stored. */\n readonly success: boolean;\n /**\n * On success, the index of the follower's last entry covered by this RPC\n * (`prevLogIndex + entries.length`) — the leader's new `matchIndex` for this\n * peer. Meaningless (0) on rejection.\n */\n readonly matchIndex: number;\n}\n\n/**\n * A client command submitted to this node (Figure 2 §5.3: clients send commands\n * to the leader). The leader appends it to its log and replicates; a non-leader\n * rejects it (the consumer would redirect the client to the known leader).\n */\nexport interface ClientCommand<C = unknown> {\n readonly _tag: \"client_command\";\n /** The opaque command to replicate (the consumer's state-machine input). */\n readonly command: C;\n}\n\n/**\n * The two fired-timer Msgs, dispatched by the `DeadlineSub`s this module arms.\n * The consumer maps a `deadline_exceeded` whose id matches the election /\n * heartbeat sub id onto these (`toRaftMsg` does the mapping), then routes them\n * into `onElectionTimeout` / `onHeartbeat`. Each carries the firing `at` (the\n * deadline's `atMs`) so the verb re-arms the next absolute target without\n * reading the clock.\n */\nexport interface ElectionTimeoutFired {\n readonly _tag: \"election_timeout_fired\";\n /** Absolute wall-clock ms the timer fired at (the deadline's target). */\n readonly at: number;\n}\nexport interface HeartbeatFired {\n readonly _tag: \"heartbeat_fired\";\n /** Absolute wall-clock ms the timer fired at (the deadline's target). */\n readonly at: number;\n}\n\n/** The full inbound-Msg union a Raft node's `update` handles. */\nexport type RaftMsg<C = unknown> =\n | RequestVoteRequest\n | RequestVoteReply\n | AppendEntriesRequest<C>\n | AppendEntriesReply\n | ClientCommand<C>\n | ElectionTimeoutFired\n | HeartbeatFired;\n\n// ===========================================================================\n// Cmds — outbound RPCs routed through the injected transport port\n// ===========================================================================\n\n/**\n * Outbound RPCs as `Cmd`s. The consumer's `interpret` performs each over the\n * injected transport port (a fetch binding, an in-memory bus in tests); this\n * module only emits the literals. `to` addresses a peer; a reply addresses the\n * original caller.\n */\n\n/** Send a `RequestVote` request to one peer (fanned out, one Cmd per peer). */\nexport interface SendRequestVote extends Cmd<\"raft:send_request_vote\"> {\n /** The peer to ask. */\n readonly to: NodeId;\n /** This candidate's term. */\n readonly term: Term;\n /** This candidate's id. */\n readonly candidateId: NodeId;\n /** This candidate's last-log index (stub payload until #120). */\n readonly lastLogIndex: number;\n /** This candidate's last-log term (stub payload until #120). */\n readonly lastLogTerm: Term;\n}\n\n/** Reply to a `RequestVote` request the original candidate awaits. */\nexport interface SendRequestVoteReply\n extends Cmd<\"raft:send_request_vote_reply\"> {\n /** The candidate that asked. */\n readonly to: NodeId;\n /** This node's `currentTerm`. */\n readonly term: Term;\n /** Whether this node granted its vote. */\n readonly voteGranted: boolean;\n}\n\n/**\n * Send an `AppendEntries` to one peer. Empty `entries` is a heartbeat; a\n * non-empty one replicates from the leader's `nextIndex` for that peer. Carries\n * the full Figure-2 payload so the follower can run its consistency check.\n */\nexport interface SendAppendEntries<C = unknown>\n extends Cmd<\"raft:send_append_entries\"> {\n /** The peer to send to. */\n readonly to: NodeId;\n /** The leader's term. */\n readonly term: Term;\n /** The leader's id. */\n readonly leaderId: NodeId;\n /** Index of the entry preceding `entries` (0 at the head of the log). */\n readonly prevLogIndex: number;\n /** Term of the entry at `prevLogIndex` (0 when `prevLogIndex` is 0). */\n readonly prevLogTerm: Term;\n /** The entries to replicate (empty for a heartbeat). */\n readonly entries: readonly LogEntry<C>[];\n /** The leader's `commitIndex`. */\n readonly leaderCommit: number;\n}\n\n/** Reply to an `AppendEntries` the original leader awaits. */\nexport interface SendAppendEntriesReply\n extends Cmd<\"raft:send_append_entries_reply\"> {\n /** The leader that asked. */\n readonly to: NodeId;\n /** This node's `currentTerm`. */\n readonly term: Term;\n /** Whether the consistency check passed and the entries were stored. */\n readonly success: boolean;\n /** On success, this node's new last-agreed index (the leader's `matchIndex`). */\n readonly matchIndex: number;\n}\n\n/** The full outbound-Cmd union a Raft node emits. */\nexport type RaftCmd<C = unknown> =\n | SendRequestVote\n | SendRequestVoteReply\n | SendAppendEntries<C>\n | SendAppendEntriesReply;\n\n// ===========================================================================\n// Subs — election + heartbeat DeadlineSubs\n// ===========================================================================\n\n/** Stable sub id for this node's election-timeout deadline. */\nfunction electionSubId(self: NodeId): string {\n return `raft:election:${self}`;\n}\n/** Stable sub id for this node's heartbeat deadline. */\nfunction heartbeatSubId(self: NodeId): string {\n return `raft:heartbeat:${self}`;\n}\n\n/**\n * Map a fired `deadline_exceeded` (from `../deadline`'s subscribe cell) onto the\n * Raft timer Msg it represents, by matching the sub id. Returns `null` if the\n * id belongs to neither Raft timer — so a consumer wiring several deadline\n * families can route ours without a brittle string-prefix check at the call\n * site. The deadline's `atMs` becomes the Msg's `at` (no clock read).\n */\nexport function toRaftMsg(\n self: NodeId,\n fired: { readonly id: string; readonly atMs: number },\n): ElectionTimeoutFired | HeartbeatFired | null {\n if (fired.id === electionSubId(self)) {\n return { _tag: \"election_timeout_fired\", at: fired.atMs };\n }\n if (fired.id === heartbeatSubId(self)) {\n return { _tag: \"heartbeat_fired\", at: fired.atMs };\n }\n return null;\n}\n\n// ===========================================================================\n// The node handle — pure verbs + subs, rng injected once\n// ===========================================================================\n\n/**\n * Result tuple every verb returns: the next state and the Cmds to emit. Matches\n * the `(state, cmds)` shape the substrate's reducer cells return, so a consumer\n * threads these straight through their `update`.\n */\nexport type RaftStep<C = unknown> = readonly [\n RaftState<C>,\n readonly RaftCmd<C>[],\n];\n\n/**\n * The node handle returned by {@link createRaftNode}. Spread its hooks into a\n * machine: `init()` seeds a fresh follower, the verbs (`onElectionTimeout`,\n * `onRequestVote`, `onRequestVoteReply`, `onAppendEntries`, `onHeartbeat`) are\n * the reducer cells, and `subs(state, now)` returns the live `DeadlineSub`s.\n *\n * Every verb is PURE given the injected `rng`: no clock read (time arrives as\n * `at`), no ambient randomness, no mutation, no throw.\n */\nexport interface RaftNode<C = unknown> {\n /** Seed a fresh node: term 0, no vote, empty log, follower. */\n init(): RaftState<C>;\n\n /**\n * The election timeout fired without a heartbeat / granted vote. Convert\n * follower OR candidate → candidate and start a NEW election: bump the term,\n * vote for self, reset the tally to `{self}`, and emit `RequestVote` to every\n * peer (carrying the real last-log index/term). A leader ignores its own\n * election timeout (it has a live mandate).\n *\n * `at` is the firing wall-clock (carried by the Msg) — used only so the\n * caller can re-arm the next election deadline at `at + <jittered timeout>`\n * via `subs`; the verb itself reads no clock.\n *\n * A single-node cluster (`peers` empty) wins the election immediately:\n * one vote (self) is already a majority, so the node converts straight to\n * leader and emits the first heartbeat round (empty here — no peers).\n */\n onElectionTimeout(state: RaftState<C>, at: number): RaftStep<C>;\n\n /**\n * Handle an inbound `RequestVote` request (Figure 2 receiver rule).\n *\n * - Rules for all servers: if `req.term > currentTerm`, adopt the term, clear\n * `votedFor`, and step down to follower BEFORE evaluating the grant.\n * - Reply `false` if `req.term < currentTerm` (stale candidate).\n * - Otherwise grant iff (`votedFor` is null OR already this candidate) AND the\n * candidate's log is at least as up-to-date ({@link logIsUpToDate} — the\n * real last-log term/index comparison, #120). Granting sets\n * `votedFor = candidateId`.\n *\n * Granting a vote (or stepping down) counts as \"heard from a valid leader /\n * candidate\", so the consumer re-arms the election timer off the resulting\n * state (`subs`).\n */\n onRequestVote(state: RaftState<C>, req: RequestVoteRequest): RaftStep<C>;\n\n /**\n * Handle an inbound `RequestVote` reply (the candidate tallying votes).\n *\n * - Rules for all servers: a higher-term reply steps the node down to follower\n * (it lost the election implicitly).\n * - A stale-term reply (`reply.term < currentTerm`) is dropped.\n * - A grant from a peer not yet counted adds to the tally; on reaching a\n * majority the candidate converts to leader (initializing `nextIndex` /\n * `matchIndex`) and emits the first `AppendEntries` round to every peer.\n * - A reply that arrives when the node is no longer a candidate (already\n * leader / stepped down) is a no-op.\n */\n onRequestVoteReply(state: RaftState<C>, reply: RequestVoteReply): RaftStep<C>;\n\n /**\n * Handle an inbound `AppendEntries` — heartbeat or replication (Figure 2\n * receiver rules). Always emits an `AppendEntries` reply Cmd unless the term\n * is stale-and-dropped without a state change.\n *\n * - Reply-side rule: `req.term < currentTerm` is rejected — a stale leader;\n * the node holds its role and replies `success: false`.\n * - `req.term >= currentTerm`: a valid leader for this term. Adopt the term if\n * higher and revert candidate → follower (a candidate that hears from a\n * leader of term ≥ its own steps down). Then run the **consistency check**:\n * if the follower's log has no entry at `prevLogIndex` with term\n * `prevLogTerm`, reply `success: false` (the leader backs off `nextIndex`\n * and retries). On a match, delete any conflicting suffix and append the new\n * `entries`, then advance `commitIndex` to\n * `min(leaderCommit, lastNewEntryIndex)`, and reply `success: true` with the\n * follower's new last-agreed index as `matchIndex`.\n *\n * Hearing a valid `AppendEntries` resets the election timer (the consumer\n * re-arms off the resulting state).\n */\n onAppendEntries(\n state: RaftState<C>,\n req: AppendEntriesRequest<C>,\n ): RaftStep<C>;\n\n /**\n * Handle an inbound `AppendEntries` reply (the leader updating its per-peer\n * bookkeeping, Figure 2 leader rules).\n *\n * - Rules for all servers: a higher-term reply steps the leader down to\n * follower.\n * - A reply received when no longer leader, or a stale-term reply, is a no-op.\n * - On `success`: set `matchIndex[from]` to the reply's `matchIndex` and\n * `nextIndex[from]` to `matchIndex + 1`, then advance `commitIndex` under the\n * **Figure-8 safety rule** — to the highest N such that N is on a majority of\n * `matchIndex`es AND `log[N].term === currentTerm` (a leader NEVER commits a\n * prior-term entry by counting replicas; it commits it only transitively once\n * a current-term entry above it commits).\n * - On rejection: decrement `nextIndex[from]` (floored at 1) and re-emit an\n * `AppendEntries` to that peer from the backed-off index (the retry).\n */\n onAppendEntriesReply(\n state: RaftState<C>,\n reply: AppendEntriesReply,\n ): RaftStep<C>;\n\n /**\n * A client submitted a command. On the leader: append a new entry\n * `{ term: currentTerm, index: lastLogIndex + 1, command }` to the log and\n * emit an `AppendEntries` round replicating it to every peer. On a non-leader:\n * a no-op (no append, no Cmds) — the consumer redirects the client to the\n * leader. This is the entry point that grows the replicated/event log.\n */\n onClientCommand(state: RaftState<C>, msg: ClientCommand<C>): RaftStep<C>;\n\n /**\n * The leader's heartbeat timer fired: emit an `AppendEntries` to every peer\n * (entries beyond that peer's `nextIndex`, empty when the peer is caught up).\n * A no-op (no Cmds) on a non-leader — a stale heartbeat fire racing a\n * step-down the reconcile pass has not yet retired.\n */\n onHeartbeat(state: RaftState<C>, at: number): RaftStep<C>;\n\n /**\n * The live `DeadlineSub`s for `state`, computed against the current clock\n * `now` (the ONLY clock read — at subscribe time, never in a verb; same seam\n * as `../poller`'s `subs`). The election-timeout target is `now + <jittered\n * timeout>` drawn from the injected `rng`; the heartbeat target is\n * `now + heartbeatMs`.\n *\n * - follower / candidate → arm ONLY the election timeout.\n * - leader → arm ONLY the heartbeat (a leader does not time itself out).\n */\n subs(state: RaftState<C>, now: number): readonly DeadlineSub[];\n}\n\n/**\n * The candidate \"log at least as up-to-date\" check (Figure 2 receiver rule,\n * §5.4.1 election restriction). Compares the candidate's last-log\n * (term, index) against ours: term dominates, index breaks a tie.\n *\n * Up-to-date iff the candidate's last-log term is GREATER than ours, OR the\n * terms are EQUAL and the candidate's last-log index is ≥ ours. (Our own\n * last-log term/index are read from the tail of `log`; an empty log is\n * (term 0, index 0), so any candidate is at least as up-to-date as an empty\n * log.) This is the property that keeps a leader's log a superset of every\n * committed entry — a node missing a committed entry can never gather a\n * majority, so it cannot win.\n */\nexport function logIsUpToDate(\n log: readonly LogEntry[],\n candidateLastLogIndex: number,\n candidateLastLogTerm: Term,\n): boolean {\n const last = log.at(-1);\n const ourLastTerm = last?.term ?? 0;\n const ourLastIndex = last?.index ?? 0;\n if (candidateLastLogTerm !== ourLastTerm) {\n return candidateLastLogTerm > ourLastTerm;\n }\n return candidateLastLogIndex >= ourLastIndex;\n}\n\n/**\n * The number of votes that constitutes a majority of the cluster. Cluster size\n * is `peers + self`; a strict majority is `floor(size / 2) + 1`. A single-node\n * cluster (size 1) needs 1 vote — its own.\n */\nfunction majority(peerCount: number): number {\n const clusterSize = peerCount + 1;\n return Math.floor(clusterSize / 2) + 1;\n}\n\n/**\n * Build a Raft node from `config`. Returns the hook bag to spread into a\n * machine. Pure factory — no clock read, no timer; everything happens when the\n * consumer calls the verbs / `subs` with an injected `at` / `now`.\n *\n * `rng` is injected ONCE here (default `Math.random` at the effect boundary, a\n * fixed value in tests) and read ONLY by `subs` to jitter the election timeout.\n * No verb body names the global RNG, so a node driven by a recorded schedule\n * replays bit-for-bit (the epic's headline payoff).\n */\nexport function createRaftNode<C = unknown>(\n config: RaftConfig,\n rng: () => number = Math.random,\n): RaftNode<C> {\n const { self, peers, electionTimeout, heartbeatMs } = config;\n const quorum = majority(peers.length);\n\n /** This node's last-log index (1-based; 0 for an empty log). */\n function lastIndex(state: RaftState<C>): number {\n return state.log.at(-1)?.index ?? 0;\n }\n /** Term of this node's last log entry (0 for an empty log). */\n function lastTerm(state: RaftState<C>): number {\n return state.log.at(-1)?.term ?? 0;\n }\n\n /**\n * Adopt a strictly-higher term: bump `currentTerm`, clear `votedFor`, and\n * step down to follower (Figure 2 \"rules for all servers\"). Pure; the caller\n * folds the rest of its decision on top of the returned state. Returns the\n * input unchanged when `term` is not higher (so callers can apply it\n * unconditionally before evaluating their own rule).\n */\n function adoptHigherTerm(state: RaftState<C>, term: Term): RaftState<C> {\n if (term <= state.currentTerm) return state;\n return {\n ...state,\n currentTerm: term,\n votedFor: null,\n role: { _tag: \"follower\" },\n };\n }\n\n /**\n * Convert to candidate and start a fresh election. Bumps the term, votes for\n * self, resets the tally to `{self}`, and emits `RequestVote` to every peer\n * carrying the real last-log index/term (the §5.4.1 election restriction the\n * receiver enforces via {@link logIsUpToDate}). Shared by `onElectionTimeout`\n * for both follower→candidate and candidate→candidate.\n */\n function startElection(state: RaftState<C>): RaftStep<C> {\n const newTerm = state.currentTerm + 1;\n const lastLogIndex = lastIndex(state);\n const lastLogTerm = lastTerm(state);\n\n const next: RaftState<C> = {\n ...state,\n currentTerm: newTerm,\n votedFor: self,\n role: { _tag: \"candidate\", votesGranted: [self] },\n };\n\n const requests: RaftCmd<C>[] = peers.map((peer) => ({\n type: \"raft:send_request_vote\",\n to: peer,\n term: newTerm,\n candidateId: self,\n lastLogIndex,\n lastLogTerm,\n }));\n\n // A single-node cluster reaches majority with its own vote alone — convert\n // straight to leader and emit the (empty, no-peer) first heartbeat round.\n if (\n next.role._tag === \"candidate\" &&\n next.role.votesGranted.length >= quorum\n ) {\n return becomeLeader(next);\n }\n\n return [next, requests];\n }\n\n /**\n * Convert a winning candidate to leader: initialize per-peer `nextIndex`\n * (leader's last-log index + 1) and `matchIndex` (0), then emit the first\n * `AppendEntries` round to every peer. Figure 2 leader-on-election rules.\n * `nextIndex` / `matchIndex` live only on the leader variant — built here,\n * nowhere else.\n */\n function becomeLeader(state: RaftState<C>): RaftStep<C> {\n const nextIdx = lastIndex(state) + 1;\n const nextIndex: Record<NodeId, number> = {};\n const matchIndex: Record<NodeId, number> = {};\n for (const peer of peers) {\n nextIndex[peer] = nextIdx;\n matchIndex[peer] = 0;\n }\n\n const leader: RaftState<C> = {\n ...state,\n role: { _tag: \"leader\", nextIndex, matchIndex },\n };\n\n return [leader, replicateAll(leader)];\n }\n\n /**\n * Build the `AppendEntries` Cmd for one peer from the leader's `nextIndex`\n * for it: `prevLogIndex` is `nextIndex - 1`, `entries` is everything from\n * `nextIndex` onward (empty when the peer is caught up → a heartbeat). Pure\n * read of the leader's log + per-peer cursor. Caller guarantees `state` is a\n * leader.\n */\n function appendEntriesFor(\n state: RaftState<C>,\n peer: NodeId,\n next: number,\n ): SendAppendEntries<C> {\n const prevLogIndex = next - 1;\n // log is 1-based and contiguous, so index i lives at log[i - 1]; `.at`\n // keeps the access total (0 term at the head / off the end).\n const prevLogTerm =\n prevLogIndex > 0 ? (state.log.at(prevLogIndex - 1)?.term ?? 0) : 0;\n const entries = state.log.slice(prevLogIndex);\n return {\n type: \"raft:send_append_entries\",\n to: peer,\n term: state.currentTerm,\n leaderId: self,\n prevLogIndex,\n prevLogTerm,\n entries,\n leaderCommit: state.commitIndex,\n };\n }\n\n /** The full `AppendEntries` fanout a leader sends (one Cmd per peer). */\n function replicateAll(state: RaftState<C>): RaftCmd<C>[] {\n if (state.role._tag !== \"leader\") return [];\n const { nextIndex } = state.role;\n // Every peer is a key of nextIndex (built in becomeLeader); `?? 1` keeps\n // the read total for the type-checker and floors a missing cursor at the\n // log head.\n return peers.map((peer) =>\n appendEntriesFor(state, peer, nextIndex[peer] ?? 1),\n );\n }\n\n /**\n * Advance a leader's `commitIndex` under the Figure-8 safety rule. Returns the\n * highest N > commitIndex such that (a) a MAJORITY of the cluster — counting\n * the leader's own log plus the peers whose `matchIndex >= N` — has the entry,\n * AND (b) `log[N].term === currentTerm`. Clause (b) is the subtle correctness\n * point: a leader must NOT commit an entry from a PRIOR term merely because it\n * sits on a majority of logs (Figure 8 shows such an entry can still be\n * overwritten). A prior-term entry commits only transitively, once a\n * current-term entry above it reaches a majority. Returns the unchanged\n * `commitIndex` when no such N exists.\n */\n function advanceCommitIndex(state: RaftState<C>): number {\n if (state.role._tag !== \"leader\") return state.commitIndex;\n const { matchIndex } = state.role;\n const lastLogIndex = lastIndex(state);\n let commit = state.commitIndex;\n // Walk candidate indices above the current commit; the highest qualifying N\n // wins (each higher N implies the lower ones, so we keep the max).\n for (let n = state.commitIndex + 1; n <= lastLogIndex; n++) {\n // Figure-8: only an entry from the current term is committable by count.\n if (state.log.at(n - 1)?.term !== state.currentTerm) continue;\n // The leader itself holds entry n (n <= lastLogIndex), so start the count\n // at 1, then add every peer that has replicated up to at least n.\n let replicas = 1;\n for (const peer of peers) {\n if ((matchIndex[peer] ?? 0) >= n) replicas++;\n }\n if (replicas >= quorum) commit = n;\n }\n return commit;\n }\n\n function init(): RaftState<C> {\n return {\n currentTerm: 0,\n votedFor: null,\n log: [],\n commitIndex: 0,\n role: { _tag: \"follower\" },\n };\n }\n\n function onElectionTimeout(state: RaftState<C>, _at: number): RaftStep<C> {\n // A leader has a live mandate; its own election timer is never armed (see\n // `subs`), but a stale fire racing a step-up must be a no-op regardless.\n if (state.role._tag === \"leader\") return [state, []];\n return startElection(state);\n }\n\n function onRequestVote(\n state: RaftState<C>,\n req: RequestVoteRequest,\n ): RaftStep<C> {\n // Rules for all servers: a higher term steps us down BEFORE we decide.\n const base = adoptHigherTerm(state, req.term);\n\n // Stale candidate — reject. Use `base.currentTerm` (already adopted) so the\n // reply carries our up-to-date term.\n if (req.term < base.currentTerm) {\n return [base, [voteReply(req.candidateId, base.currentTerm, false)]];\n }\n\n const free = base.votedFor === null || base.votedFor === req.candidateId;\n const upToDate = logIsUpToDate(base.log, req.lastLogIndex, req.lastLogTerm);\n const grant = free && upToDate;\n\n if (!grant) {\n return [base, [voteReply(req.candidateId, base.currentTerm, false)]];\n }\n\n // Granting records the vote (and resets the election timer via `subs`, off\n // the returned follower state).\n const granted: RaftState<C> = {\n ...base,\n votedFor: req.candidateId,\n role: { _tag: \"follower\" },\n };\n return [granted, [voteReply(req.candidateId, base.currentTerm, true)]];\n }\n\n function onRequestVoteReply(\n state: RaftState<C>,\n reply: RequestVoteReply,\n ): RaftStep<C> {\n // Rules for all servers: a higher-term reply steps us down (we lost).\n if (reply.term > state.currentTerm) {\n return [adoptHigherTerm(state, reply.term), []];\n }\n\n // Drop stale-term replies and replies for a role that is no longer\n // tallying (already leader / stepped down).\n if (reply.term < state.currentTerm) return [state, []];\n if (state.role._tag !== \"candidate\") return [state, []];\n if (!reply.voteGranted) return [state, []];\n\n // Dedup the granter so a duplicate reply never double-counts.\n if (state.role.votesGranted.includes(reply.from)) return [state, []];\n\n const votesGranted = [...state.role.votesGranted, reply.from];\n const tallied: RaftState<C> = {\n ...state,\n role: { _tag: \"candidate\", votesGranted },\n };\n\n if (votesGranted.length >= quorum) return becomeLeader(tallied);\n return [tallied, []];\n }\n\n function onAppendEntries(\n state: RaftState<C>,\n req: AppendEntriesRequest<C>,\n ): RaftStep<C> {\n // Stale leader — reject. Reply with our (higher) term so the sender steps\n // down; no state change.\n if (req.term < state.currentTerm) {\n return [state, [appendReply(req.leaderId, state.currentTerm, false, 0)]];\n }\n\n // Valid leader for term ≥ ours. Adopt a higher term (clears votedFor, steps\n // down); then ensure we are a follower for this term — a candidate that\n // hears from a leader of term ≥ its own reverts (Figure 2). Hearing a valid\n // AppendEntries resets the election timer (consumer re-arms off this state).\n const adopted = adoptHigherTerm(state, req.term);\n const base: RaftState<C> =\n adopted.role._tag === \"follower\"\n ? adopted\n : { ...adopted, role: { _tag: \"follower\" } };\n\n // Consistency check (Figure 2): the follower must hold an entry at\n // `prevLogIndex` whose term is `prevLogTerm` (prevLogIndex 0 always matches\n // — it anchors at the head). Otherwise reject; the leader backs off.\n const prevOk =\n req.prevLogIndex === 0 ||\n base.log[req.prevLogIndex - 1]?.term === req.prevLogTerm;\n if (!prevOk) {\n return [base, [appendReply(req.leaderId, base.currentTerm, false, 0)]];\n }\n\n // Match: keep the log up to prevLogIndex, then delete any conflicting suffix\n // and append the new entries. Splicing at prevLogIndex and concatenating is\n // the truncate-and-append in one step (an entry already present and\n // identical is simply overwritten with itself — idempotent on a retry).\n const kept = base.log.slice(0, req.prevLogIndex);\n const nextLog = [...kept, ...req.entries];\n const lastNew = req.prevLogIndex + req.entries.length;\n\n // Advance commitIndex to min(leaderCommit, index of last new entry). Never\n // run ahead of what this follower actually holds.\n const commitIndex =\n req.leaderCommit > base.commitIndex\n ? Math.min(req.leaderCommit, lastNew)\n : base.commitIndex;\n\n const next: RaftState<C> = { ...base, log: nextLog, commitIndex };\n return [next, [appendReply(req.leaderId, base.currentTerm, true, lastNew)]];\n }\n\n function onAppendEntriesReply(\n state: RaftState<C>,\n reply: AppendEntriesReply,\n ): RaftStep<C> {\n // Rules for all servers: a higher-term reply steps us down.\n if (reply.term > state.currentTerm) {\n return [adoptHigherTerm(state, reply.term), []];\n }\n // Only a leader of the matching term acts on replies; anything else is a\n // stale/misrouted reply and a no-op.\n if (state.role._tag !== \"leader\") return [state, []];\n if (reply.term < state.currentTerm) return [state, []];\n\n const { nextIndex, matchIndex } = state.role;\n\n if (!reply.success) {\n // Consistency check failed at the follower: back off nextIndex (floored\n // at 1) and retry from the lower bound. matchIndex is untouched.\n const backed = Math.max(1, (nextIndex[reply.from] ?? 1) - 1);\n const retried: RaftState<C> = {\n ...state,\n role: {\n _tag: \"leader\",\n nextIndex: { ...nextIndex, [reply.from]: backed },\n matchIndex,\n },\n };\n return [retried, [appendEntriesFor(retried, reply.from, backed)]];\n }\n\n // Success: this peer now agrees up to reply.matchIndex. Advance its cursors\n // monotonically (a stale/duplicated success must never pull them backward),\n // then re-evaluate the commit index under the Figure-8 rule.\n const newMatch = Math.max(matchIndex[reply.from] ?? 0, reply.matchIndex);\n const advanced: RaftState<C> = {\n ...state,\n role: {\n _tag: \"leader\",\n nextIndex: { ...nextIndex, [reply.from]: newMatch + 1 },\n matchIndex: { ...matchIndex, [reply.from]: newMatch },\n },\n };\n const commitIndex = advanceCommitIndex(advanced);\n return [{ ...advanced, commitIndex }, []];\n }\n\n function onClientCommand(\n state: RaftState<C>,\n msg: ClientCommand<C>,\n ): RaftStep<C> {\n // Only the leader accepts client commands; a non-leader rejects (the\n // consumer redirects to the known leader). No append, no Cmds.\n if (state.role._tag !== \"leader\") return [state, []];\n\n const entry: LogEntry<C> = {\n term: state.currentTerm,\n index: lastIndex(state) + 1,\n command: msg.command,\n };\n const appended: RaftState<C> = { ...state, log: [...state.log, entry] };\n return [appended, replicateAll(appended)];\n }\n\n function onHeartbeat(state: RaftState<C>, _at: number): RaftStep<C> {\n // Only a leader heartbeats. A stale fire racing a step-down is a no-op.\n if (state.role._tag !== \"leader\") return [state, []];\n return [state, replicateAll(state)];\n }\n\n function subs(state: RaftState<C>, now: number): readonly DeadlineSub[] {\n if (state.role._tag === \"leader\") {\n // A leader arms ONLY the heartbeat; it never times itself out.\n return [deadlineSub(heartbeatSubId(self), now + heartbeatMs)];\n }\n // Follower / candidate arm ONLY the election timeout, jittered in\n // [minMs, maxMs] from the injected rng (never Math.random ambiently).\n const span = electionTimeout.maxMs - electionTimeout.minMs;\n const timeout = electionTimeout.minMs + Math.floor(rng() * (span + 1));\n return [deadlineSub(electionSubId(self), now + timeout)];\n }\n\n /** Build a `RequestVote` reply Cmd addressed to the asking candidate. */\n function voteReply(\n to: NodeId,\n term: Term,\n voteGranted: boolean,\n ): SendRequestVoteReply {\n return { type: \"raft:send_request_vote_reply\", to, term, voteGranted };\n }\n\n /** Build an `AppendEntries` reply Cmd addressed to the asking leader. */\n function appendReply(\n to: NodeId,\n term: Term,\n success: boolean,\n matchIndex: number,\n ): SendAppendEntriesReply {\n return {\n type: \"raft:send_append_entries_reply\",\n to,\n term,\n success,\n matchIndex,\n };\n }\n\n return {\n init,\n onElectionTimeout,\n onRequestVote,\n onRequestVoteReply,\n onAppendEntries,\n onAppendEntriesReply,\n onClientCommand,\n onHeartbeat,\n subs,\n };\n}\n\n// Re-export the sub-id builders' identities for consumers wiring `subscribe`\n// cells / asserting against the armed sub ids without re-deriving the strings.\nexport { electionSubId, heartbeatSubId };\n\n// `Sub` / `subId` are imported for the `DeadlineSub` return type and id\n// branding inside `../deadline`; re-exported here so a consumer wiring the Raft\n// subs into a machine's `subscribe` map need not also reach into the core.\nexport type { Sub };\nexport { subId };\n"]}
@@ -1,53 +1,3 @@
1
- // src/circuit-breaker/index.ts
2
- var defaultCircuitPolicy = {
3
- failureThreshold: 5,
4
- cooldownMs: 3e4,
5
- halfOpenMaxProbes: 1
6
- };
7
- function initCircuit() {
8
- return { phase: "closed", failures: 0 };
9
- }
10
- function onSuccess(state, _policy) {
11
- switch (state.phase) {
12
- case "closed":
13
- return state.failures === 0 ? state : { phase: "closed", failures: 0 };
14
- case "half_open":
15
- return { phase: "closed", failures: 0 };
16
- case "open":
17
- return state;
18
- }
19
- }
20
- function onFailure(state, policy, nowMs) {
21
- switch (state.phase) {
22
- case "closed": {
23
- const failures = state.failures + 1;
24
- return failures >= policy.failureThreshold ? { phase: "open", openedAtMs: nowMs } : { phase: "closed", failures };
25
- }
26
- case "half_open":
27
- return { phase: "open", openedAtMs: nowMs };
28
- case "open":
29
- return state;
30
- }
31
- }
32
- function canPass(state, policy, nowMs) {
33
- switch (state.phase) {
34
- case "closed":
35
- return [state, true];
36
- case "open": {
37
- const elapsed = nowMs - state.openedAtMs;
38
- if (elapsed >= policy.cooldownMs) {
39
- return [{ phase: "half_open", probes: 1 }, true];
40
- }
41
- return [state, false];
42
- }
43
- case "half_open":
44
- if (state.probes < policy.halfOpenMaxProbes) {
45
- return [{ phase: "half_open", probes: state.probes + 1 }, true];
46
- }
47
- return [state, false];
48
- }
49
- }
50
-
51
- export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess };
1
+ export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess } from '../chunk-SC3FAFZN.js';
52
2
  //# sourceMappingURL=index.js.map
53
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/circuit-breaker/index.ts"],"names":[],"mappings":";AAwHO,IAAM,oBAAA,GAAsC;AAAA,EACjD,gBAAA,EAAkB,CAAA;AAAA,EAClB,UAAA,EAAY,GAAA;AAAA,EACZ,iBAAA,EAAmB;AACrB;AAoBO,SAAS,WAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,CAAA,EAAE;AACxC;AAYO,SAAS,SAAA,CACd,OACA,OAAA,EACc;AACd,EAAA,QAAQ,MAAM,KAAA;AAAO,IACnB,KAAK,QAAA;AAGH,MAAA,OAAO,KAAA,CAAM,aAAa,CAAA,GAAI,KAAA,GAAQ,EAAE,KAAA,EAAO,QAAA,EAAU,UAAU,CAAA,EAAE;AAAA,IACvE,KAAK,WAAA;AACH,MAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,CAAA,EAAE;AAAA,IACxC,KAAK,MAAA;AACH,MAAA,OAAO,KAAA;AAAA;AAEb;AAgBO,SAAS,SAAA,CACd,KAAA,EACA,MAAA,EACA,KAAA,EACc;AACd,EAAA,QAAQ,MAAM,KAAA;AAAO,IACnB,KAAK,QAAA,EAAU;AACb,MAAA,MAAM,QAAA,GAAW,MAAM,QAAA,GAAW,CAAA;AAGlC,MAAA,OAAO,QAAA,IAAY,MAAA,CAAO,gBAAA,GACtB,EAAE,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAM,GACnC,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAS;AAAA,IAClC;AAAA,IACA,KAAK,WAAA;AAEH,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAM;AAAA,IAC5C,KAAK,MAAA;AACH,MAAA,OAAO,KAAA;AAAA;AAEb;AA0BO,SAAS,OAAA,CACd,KAAA,EACA,MAAA,EACA,KAAA,EACkC;AAClC,EAAA,QAAQ,MAAM,KAAA;AAAO,IACnB,KAAK,QAAA;AACH,MAAA,OAAO,CAAC,OAAO,IAAI,CAAA;AAAA,IACrB,KAAK,MAAA,EAAQ;AACX,MAAA,MAAM,OAAA,GAAU,QAAQ,KAAA,CAAM,UAAA;AAG9B,MAAA,IAAI,OAAA,IAAW,OAAO,UAAA,EAAY;AAChC,QAAA,OAAO,CAAC,EAAE,KAAA,EAAO,aAAa,MAAA,EAAQ,CAAA,IAAK,IAAI,CAAA;AAAA,MACjD;AACA,MAAA,OAAO,CAAC,OAAO,KAAK,CAAA;AAAA,IACtB;AAAA,IACA,KAAK,WAAA;AACH,MAAA,IAAI,KAAA,CAAM,MAAA,GAAS,MAAA,CAAO,iBAAA,EAAmB;AAC3C,QAAA,OAAO,CAAC,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,MAChE;AACA,MAAA,OAAO,CAAC,OAAO,KAAK,CAAA;AAAA;AAE1B","file":"index.js","sourcesContent":["/**\n * @demlik/tea/circuit-breaker — per-target failure tracking as pure state + ops.\n *\n * Same shape as `@demlik/tea/work-queue`'s `ops.ts`, `@demlik/tea/rate-limit`,\n * and `@demlik/tea/idempotency`: a state type plus pure transition functions.\n * Host-agnostic by construction — no `Date.now()`, no timers, no I/O. The\n * caller injects the clock (`nowMs`) and wires the result into its own reducer\n * / interpret. This keeps the breaker inside TEA's invariant 2 (transitions are\n * pure: same inputs → same outputs, no `Date.now`) so a consumer can fold it\n * through `update` and exercise it with `replay` / the pbt fold without a fake\n * clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). `canPass` returns a `[next, allowed]` tuple so\n * the caller branches on the decision while threading the advanced state forward\n * — same convention as `rate-limit`'s `tryConsume` / `record`. `onSuccess` /\n * `onFailure` are the outcome-recording transitions the caller folds in once the\n * guarded call resolves.\n *\n * ## The phase machine\n *\n * A circuit breaker guards a fallible downstream target (an HTTP backend, a DB,\n * a flaky tool call). Instead of hammering a target that is already failing, it\n * trips OPEN after enough failures and fast-fails subsequent calls for a\n * cooldown, then probes with a few trial calls before fully closing again.\n * Three phases, each carrying ONLY its own data so impossible states are\n * unrepresentable (pattern 11):\n *\n * - **closed** — the healthy steady state. Calls pass through. Carries\n * `failures`: the count of *consecutive* failures since the last success.\n * A success resets it to 0; the `failureThreshold`-th failure trips the\n * breaker OPEN. Only `closed` has a failure counter — `open` and\n * `half_open` don't, because in those phases the count is irrelevant\n * (the breaker has already tripped / is already probing).\n *\n * - **open** — the tripped state. All calls fast-fail (no downstream load).\n * Carries `openedAtMs`: the clock reading at which the breaker tripped.\n * `canPass` compares `nowMs - openedAtMs` against `cooldownMs` to decide\n * when to start probing. Only `open` carries a timestamp — neither other\n * phase is timing anything.\n *\n * - **half_open** — the probing state. A limited number of trial calls are\n * allowed through to test whether the target has recovered. Carries\n * `probes`: the count of probes admitted so far this probing round.\n * `canPass` admits while `probes < halfOpenMaxProbes`, then fast-fails the\n * rest (so a slow target can't be flooded by every queued caller at once).\n * A probe success closes the breaker; a probe failure re-opens it (and\n * resets the cooldown clock). Only `half_open` counts in-flight probes.\n *\n * ### Transition table\n *\n * Rows are the current phase; columns are the event. `→` denotes the next\n * phase; `self` means unchanged. `nowMs` is the injected clock.\n *\n * | from \\ event | onSuccess | onFailure | canPass (→ allowed?) |\n * | ------------ | -------------- | -------------------------------------- | ------------------------------------------------------------------------------------ |\n * | closed | closed(0) | failures+1; trip → open(nowMs) at thr. | self → true (always) |\n * | open | self *(¹)* | self | elapsed ≥ cooldown → half_open(1), true; else self → false |\n * | half_open | → closed(0) | → open(nowMs) (probe failed) | probes < max → half_open(probes+1), true; else self → false |\n *\n * *(¹)* `onSuccess` on `open` \"shouldn't happen\": no call was admitted while\n * open, so no success can be reported for one. We leave the state unchanged\n * rather than throw — the breaker is a coordination aid, not a correctness\n * gate, and a spurious success report (e.g. a late-arriving result from before\n * the trip) must not silently un-trip the breaker. `onFailure` on `open` is\n * likewise a no-op: the breaker is already tripped; the `open` clock is anchored\n * at the trip, not bumped by late failures.\n *\n * ### The probe-admission asymmetry\n *\n * Entering `half_open` happens inside `canPass`, not `onSuccess` / `onFailure`,\n * because the cooldown is a *time* condition and only `canPass` reads the clock\n * against `openedAtMs`. The first `canPass` after the cooldown elapses both\n * transitions the phase AND admits the probe (`[half_open(1), true]`) — one\n * caller drives the recovery attempt. Subsequent `canPass` calls in the same\n * round admit up to `halfOpenMaxProbes` total, then fast-fail until a probe\n * resolves and `onSuccess` / `onFailure` moves the phase out of `half_open`.\n *\n * This module is a textbook three-state machine — a clean `/machine-viz` demo.\n *\n * NOT exported from the package root — reached via the\n * `@demlik/tea/circuit-breaker` subpath, same one-shape-per-package rule as\n * `work-queue`, `rate-limit`, and `idempotency`.\n */\n\n/**\n * Circuit-breaker policy — pure configuration, no mutable state. One policy is\n * shared across all the calls guarded by one logical breaker (often a\n * module-scope const). `CircuitState` carries the mutable counts; this carries\n * the thresholds those counts are compared against.\n */\nexport interface CircuitPolicy {\n /**\n * Consecutive failures in `closed` that trip the breaker `open`. The\n * `failureThreshold`-th failure is the one that trips — with a threshold of\n * 5, the breaker survives 4 failures and opens on the 5th.\n */\n readonly failureThreshold: number;\n /**\n * How long the breaker stays `open` before `canPass` starts admitting\n * probes, in milliseconds. Measured from `openedAtMs`; the cutoff is\n * half-open (`elapsed >= cooldownMs` admits), matching `rate-limit` /\n * `idempotency` window edges — a breaker opened at `t` first probes at\n * `t + cooldownMs`.\n */\n readonly cooldownMs: number;\n /**\n * How many probe calls `half_open` admits before fast-failing the rest of\n * the round. Caps the load on a recovering target — a single slow probe\n * can't be joined by every queued caller. The round ends when a probe\n * resolves: success closes the breaker, failure re-opens it.\n */\n readonly halfOpenMaxProbes: number;\n}\n\n/**\n * Sensible defaults: trip after 5 consecutive failures, cool down for 30s,\n * admit a single probe before deciding. Tuned for guarding a network backend;\n * override per call site rather than mutating this shared const.\n */\nexport const defaultCircuitPolicy: CircuitPolicy = {\n failureThreshold: 5,\n cooldownMs: 30_000,\n halfOpenMaxProbes: 1,\n};\n\n/**\n * The breaker's phase. A discriminated union on `phase` so each phase carries\n * ONLY its own data and impossible combinations are unrepresentable (pattern\n * 11): there is no state with both a failure count AND an open timestamp,\n * because such a state is meaningless. `phase` is the discriminant; every field\n * is `readonly` so the whole value is trivially JSON-serializable into a\n * consumer's `Store<S>` and safe to thread through a pure reducer.\n *\n * - `closed` carries `failures` — consecutive failures since last success.\n * - `open` carries `openedAtMs` — clock reading when it tripped.\n * - `half_open` carries `probes` — probes admitted this round.\n */\nexport type CircuitState =\n | { readonly phase: \"closed\"; readonly failures: number }\n | { readonly phase: \"open\"; readonly openedAtMs: number }\n | { readonly phase: \"half_open\"; readonly probes: number };\n\n/** The starting state: closed with zero recorded failures. */\nexport function initCircuit(): CircuitState {\n return { phase: \"closed\", failures: 0 };\n}\n\n/**\n * Record a successful guarded call. PURE — returns a new state; the input is\n * never mutated.\n *\n * - `closed` → reset `failures` to 0 (a success breaks the consecutive run).\n * - `half_open` → the probe succeeded; the target has recovered → `closed`.\n * - `open` → unchanged. A success \"shouldn't happen\" while open (no call\n * was admitted to produce one); a late/spurious success must\n * not silently un-trip the breaker. See module header note ¹.\n */\nexport function onSuccess(\n state: CircuitState,\n _policy: CircuitPolicy,\n): CircuitState {\n switch (state.phase) {\n case \"closed\":\n // Already closed at 0 → return the same shape (cheap; the consumer's\n // store only re-persists on a real change if it dedupes).\n return state.failures === 0 ? state : { phase: \"closed\", failures: 0 };\n case \"half_open\":\n return { phase: \"closed\", failures: 0 };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Record a failed guarded call. PURE — returns a new state; the input is never\n * mutated. `nowMs` is the injected clock, stamped onto the `open` phase when the\n * breaker trips so `canPass` can measure the cooldown later.\n *\n * - `closed` → `failures + 1`; if that reaches `failureThreshold`, trip to\n * `open(nowMs)`. The threshold-th failure is the one that trips.\n * - `half_open` → the probe failed; the target is still unhealthy →\n * `open(nowMs)`, restarting the cooldown clock so a fresh\n * cooldown elapses before the next probe round.\n * - `open` → unchanged. The breaker is already tripped; the `open` clock\n * is anchored at the original trip, not bumped by late\n * failures. See module header note ¹.\n */\nexport function onFailure(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): CircuitState {\n switch (state.phase) {\n case \"closed\": {\n const failures = state.failures + 1;\n // `>=` not `===`: a threshold of 0 (or any already-exceeded count from a\n // policy change) trips immediately rather than over-counting forever.\n return failures >= policy.failureThreshold\n ? { phase: \"open\", openedAtMs: nowMs }\n : { phase: \"closed\", failures };\n }\n case \"half_open\":\n // A failed probe re-opens and resets the cooldown clock to nowMs.\n return { phase: \"open\", openedAtMs: nowMs };\n case \"open\":\n return state;\n }\n}\n\n/**\n * Decide whether a call may pass, advancing the phase as a side effect of the\n * decision. PURE — returns `[next, allowed]`; the input is never mutated.\n * Branch on the boolean, thread the state forward regardless:\n *\n * const [c1, allowed] = canPass(state.circuit, policy, nowMs);\n * if (!allowed) return [{ ...state, circuit: c1 }, [fastFailCmd]];\n * return [{ ...state, circuit: c1 }, [doGuardedCallCmd]];\n *\n * - `closed` → `[state, true]` — always passes; failures are recorded by\n * `onFailure` once the call resolves, not gated here.\n * - `open` → if `nowMs - openedAtMs >= cooldownMs` the cooldown has\n * elapsed → enter `half_open` and admit this caller as the\n * first probe: `[half_open(1), true]`. Otherwise still cooling\n * down → `[state, false]` (fast-fail, no downstream load).\n * - `half_open` → admit while `probes < halfOpenMaxProbes`, incrementing the\n * count: `[half_open(probes+1), true]`. Once the cap is hit →\n * `[state, false]` until a probe resolves and `onSuccess` /\n * `onFailure` moves the phase out of `half_open`.\n *\n * Entering `half_open` lives here (not in `onSuccess` / `onFailure`) because the\n * cooldown is a time condition and only `canPass` reads `nowMs` against\n * `openedAtMs`. See the module header's probe-admission note.\n */\nexport function canPass(\n state: CircuitState,\n policy: CircuitPolicy,\n nowMs: number,\n): readonly [CircuitState, boolean] {\n switch (state.phase) {\n case \"closed\":\n return [state, true];\n case \"open\": {\n const elapsed = nowMs - state.openedAtMs;\n // Half-open cutoff: elapsed exactly == cooldownMs admits the first probe,\n // matching the >= edge `rate-limit` / `idempotency` use for their windows.\n if (elapsed >= policy.cooldownMs) {\n return [{ phase: \"half_open\", probes: 1 }, true];\n }\n return [state, false];\n }\n case \"half_open\":\n if (state.probes < policy.halfOpenMaxProbes) {\n return [{ phase: \"half_open\", probes: state.probes + 1 }, true];\n }\n return [state, false];\n }\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,178 @@
1
+ type SyncReturn<S, C extends Cmd> = readonly [S, readonly C[]] & {
2
+ readonly then?: never;
3
+ };
4
+ type UpdateForm = "reducer" | "transitions";
5
+ declare function detectUpdateForm(update: object): UpdateForm;
6
+ declare function formOf(machine: {
7
+ update: object;
8
+ __form?: UpdateForm;
9
+ }): UpdateForm;
10
+ type Reducer<S, M extends {
11
+ type: string;
12
+ }, C extends Cmd> = {
13
+ [K in M["type"]]: (state: S, msg: Extract<M, {
14
+ type: K;
15
+ }>) => SyncReturn<S, C>;
16
+ };
17
+ type Transitions<S extends {
18
+ type: string;
19
+ }, M extends {
20
+ type: string;
21
+ }, C extends Cmd> = {
22
+ [P in S["type"]]: {
23
+ [K in M["type"]]: (state: Extract<S, {
24
+ type: P;
25
+ }>, msg: Extract<M, {
26
+ type: K;
27
+ }>) => SyncReturn<S, C>;
28
+ };
29
+ };
30
+ type Cmd<T extends string = string> = {
31
+ type: T;
32
+ };
33
+ declare const Cmd: {
34
+ /**
35
+ * The empty Cmd array. Typed `readonly never[]` so it's assignable to any
36
+ * `readonly C[]` for any `C extends Cmd`. Use in `init` returns and
37
+ * Transitions cells that emit zero effects:
38
+ *
39
+ * init: (loaded) => [loaded ?? initial, Cmd.none],
40
+ * tick: (state) => [state, Cmd.none],
41
+ *
42
+ * Elm's `Cmd.none` analogue. Cultural signal alongside the runtime help:
43
+ * `[state, Cmd.none]` reads as intent ("this transition emits nothing"),
44
+ * `[state, []]` reads as "empty array of what".
45
+ *
46
+ * Frozen at runtime so a downstream consumer can't `.push()` into the
47
+ * shared reference.
48
+ */
49
+ readonly none: readonly never[];
50
+ /**
51
+ * Flat concat of cmd arrays. Use when a cell composes effects from
52
+ * multiple conditional sources:
53
+ *
54
+ * return [next, Cmd.batch(
55
+ * Cmd.whenDefined(state.queueItemId, (id) => ({ type: "complete", id })),
56
+ * Cmd.when(state.windowId !== undefined, { type: "close_window", windowId }),
57
+ * [{ type: "detach_debugger", tabId: state.tabId }],
58
+ * )];
59
+ *
60
+ * `<const C>` keeps inline cmd literals' discriminants narrow (same
61
+ * rationale as `when` / `whenDefined` above).
62
+ */
63
+ readonly batch: <const C extends Cmd>(...arrs: readonly (readonly C[])[]) => readonly C[];
64
+ /**
65
+ * Emit `cmd` wrapped in a single-element array iff `cond` is true.
66
+ * Otherwise return the empty array. Spreads cleanly into the cmds array
67
+ * returned by a Transitions cell.
68
+ *
69
+ * `const C` (TypeScript 5.0+) keeps the inferred `type:` discriminator
70
+ * literal narrow when called with an object literal — so `Cmd.when(b,
71
+ * { type: "x", ... })` infers as `{ type: "x", ... }`, not `{ type:
72
+ * string, ... }`, and stays assignable to a discriminated-union arm
73
+ * without `as const` at the call site.
74
+ */
75
+ readonly when: <const C extends Cmd>(cond: boolean, cmd: C) => readonly C[];
76
+ /**
77
+ * If `value` is defined, call `build(value)` and emit the resulting cmd
78
+ * wrapped in a single-element array. If `value` is `undefined`, return
79
+ * the empty array. `value` is narrowed to `T` inside `build`.
80
+ *
81
+ * `const C` on the return type keeps the callback's object-literal
82
+ * `type:` field narrow (see `when` above for the full rationale).
83
+ */
84
+ readonly whenDefined: <T, const C extends Cmd>(value: T | undefined, build: (value: T) => C) => readonly C[];
85
+ };
86
+ type SubId = string & {
87
+ readonly __brand: "SubId";
88
+ };
89
+ /**
90
+ * Construct a `SubId` from a string. Use this everywhere a Sub literal is
91
+ * built — including ids derived from runtime data (`subId(\`ws:${auditId}\`)`).
92
+ * The brand is structural, not nominal, so dynamic ids are permitted; the
93
+ * constructor's job is to make the identity decision explicit at every call
94
+ * site so the type system can catch accidental raw-string drift.
95
+ *
96
+ * Strengthens invariant 7 (identity is explicit).
97
+ */
98
+ declare function subId(s: string): SubId;
99
+ type Sub<T extends string = string> = {
100
+ id: SubId;
101
+ type: T;
102
+ };
103
+ interface Port<T> {
104
+ readonly __brand: "port";
105
+ readonly name: string;
106
+ readonly __t?: T;
107
+ }
108
+ /**
109
+ * Augmentation injected onto `ctx` inside Cmd handlers. Handlers receive
110
+ * `ctx & PortEmitter` so they can call `ctx.emit(port, value)` synchronously.
111
+ * Subscribers registered via `runtime.subscribePort(port, listener)` receive
112
+ * the value immediately.
113
+ *
114
+ * Emitting to a port with no subscribers is a no-op (does not throw).
115
+ */
116
+ interface PortEmitter {
117
+ emit<T>(port: Port<T>, value: T): void;
118
+ }
119
+ type NoCtx = Readonly<Record<never, never>>;
120
+ /** Spelled-out alias for `NoCtx`, for sites that prefer the long name. */
121
+ type ContextFree = NoCtx;
122
+ type Interpret<M extends {
123
+ type: string;
124
+ }, C extends Cmd, Ctx> = {
125
+ [K in C["type"]]: (cmd: Extract<C, {
126
+ type: K;
127
+ }>, ctx: Ctx & PortEmitter) => Promise<M | void>;
128
+ };
129
+ type Subscribe<M extends {
130
+ type: string;
131
+ }, U extends Sub, Ctx> = {
132
+ [K in U["type"]]: (sub: Extract<U, {
133
+ type: K;
134
+ }>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
135
+ };
136
+ type Machine<S, M extends {
137
+ type: string;
138
+ }, C extends Cmd, U extends Sub, Ctx> = {
139
+ /**
140
+ * Boot the runtime. Called once by `run(...)` with whatever `Store.load()`
141
+ * returned: `null` on fresh boot, the persisted state on rehydrate.
142
+ *
143
+ * **Contract:** when `loaded !== null`, init MUST return `[loaded, []]` —
144
+ * no Cmds. Init's rehydrate branch is the migration / parse boundary, not
145
+ * the boot-effect hook. See Invariant 2 in `.patterns/tea/tea-invariants.md`.
146
+ *
147
+ * Boot effects routes:
148
+ * - Stateless infrastructure → host module top, outside TEA.
149
+ * - State-conditional resume → a `boot` Msg the host dispatches once
150
+ * after `run(...)` returns.
151
+ *
152
+ * Violations are caught at runtime by `replay` (which throws with a
153
+ * pointer to the alternatives).
154
+ */
155
+ init: (loaded: S | null, ctx: Ctx) => readonly [S, readonly C[]];
156
+ update: Reducer<S, M, C> | ([S] extends [{
157
+ type: string;
158
+ }] ? Transitions<S, M, C> : never);
159
+ subscriptions?: (state: S) => readonly U[];
160
+ subscribe?: Subscribe<M, U, Ctx>;
161
+ /**
162
+ * The update form ("reducer" | "transitions"), stamped non-enumerably by
163
+ * `defineMachine` at construction (see `UpdateForm` / `formOf`). Optional in
164
+ * the type so the structural `Machine` annotation form keeps accepting plain
165
+ * object literals; readers go through `formOf`, which falls back to
166
+ * `detectUpdateForm` when the tag is absent. Never written by hand.
167
+ */
168
+ readonly __form?: UpdateForm;
169
+ } & ([C] extends [Cmd<never>] ? {
170
+ interpret?: Interpret<M, C, Ctx>;
171
+ } : {
172
+ interpret: Interpret<M, C, Ctx>;
173
+ });
174
+ declare function foldMsgs<S, M extends {
175
+ type: string;
176
+ }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, base: S, msgs: readonly M[]): S;
177
+
178
+ 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, detectUpdateForm as f, foldMsgs as g, formOf as h, subId as s };
@@ -1,5 +1,5 @@
1
- import { Sub } from '../index.js';
2
- import { S as SubscribeHandler } from '../types-CwBC5wMn.js';
1
+ import { S as Sub } from '../core-CymXZCeY.js';
2
+ import { S as SubscribeHandler } from '../types-N9LZbdnr.js';
3
3
 
4
4
  /**
5
5
  * @demlik/tea/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock
@@ -48,7 +48,21 @@ import { S as SubscribeHandler } from '../types-CwBC5wMn.js';
48
48
  */
49
49
  type DeadlineSub = Sub<"deadline"> & {
50
50
  readonly atMs: number;
51
- };
51
+ } & DeadlineOpts;
52
+ /**
53
+ * Additive options the `deadlineSub` factory folds onto the Sub literal. This
54
+ * is the extension seam (invariant 5 — composition by reduction): future fields
55
+ * — `repeatMs` for a re-arming deadline, `jitterMs` for spread, etc. — land HERE
56
+ * as optional members, and every existing call site keeps compiling because the
57
+ * factory's third parameter is optional and the fields are `readonly` optionals
58
+ * on `DeadlineSub`. Callers are insulated from the concrete `DeadlineSub` shape:
59
+ * they ask for what they need by name, never positionally, so a shape extension
60
+ * never rewrites the ~13 `deadlineSub(id, atMs)` call sites.
61
+ *
62
+ * Empty today on purpose — the seam exists before the first extension does, so
63
+ * the first field is a one-line additive change, not a 13-site migration.
64
+ */
65
+ type DeadlineOpts = {};
52
66
  /**
53
67
  * The Msg the deadline dispatches when the wall clock crosses `atMs`. Exported
54
68
  * as a tagged shape so consumers can union it into their machine's Msg type and
@@ -83,8 +97,13 @@ type DeadlineExceeded = {
83
97
  * deadlines on the same machine.
84
98
  * @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`
85
99
  * for a 15-minute guard, or a persisted `expiresAt`).
100
+ * @param opts Optional additive fields folded onto the Sub literal (see
101
+ * {@link DeadlineOpts}). Omit it for the common case; pass it when a
102
+ * future field (e.g. `repeatMs`) needs to ride on the deadline. The
103
+ * optionality is the future-proofing seam — extending the shape
104
+ * never breaks an existing `deadlineSub(id, atMs)` call site.
86
105
  */
87
- declare function deadlineSub(id: string, atMs: number): DeadlineSub;
106
+ declare function deadlineSub(id: string, atMs: number, opts?: DeadlineOpts): DeadlineSub;
88
107
  /**
89
108
  * The `subscribe["deadline"]` cell. Arms a one-shot timer for the remaining
90
109
  * delay `max(0, atMs - Date.now())` and dispatches `deadlineExceeded(...)` when
@@ -110,4 +129,4 @@ declare const subscribeDeadline: SubscribeHandler<DeadlineSub, DeadlineExceeded,
110
129
  */
111
130
  declare function deadlineExceeded(id: string, atMs: number): DeadlineExceeded;
112
131
 
113
- export { type DeadlineExceeded, type DeadlineSub, deadlineExceeded, deadlineSub, subscribeDeadline };
132
+ export { type DeadlineExceeded, type DeadlineOpts, type DeadlineSub, deadlineExceeded, deadlineSub, subscribeDeadline };
@@ -1,20 +1,6 @@
1
- import { fromTimeout } from '../chunk-2YFLOCWX.js';
2
- import { subId } from '../chunk-CMG2KMWX.js';
3
-
4
- // src/deadline/index.ts
5
- function deadlineSub(id, atMs) {
6
- return { id: subId(id), type: "deadline", atMs };
7
- }
8
- var subscribeDeadline = (sub, ctx, dispatch) => {
9
- const remainingMs = Math.max(0, sub.atMs - Date.now());
10
- return fromTimeout(
11
- (armed) => deadlineExceeded(armed.id, armed.atMs)
12
- )({ ...sub, delayMs: remainingMs }, ctx, dispatch);
13
- };
14
- function deadlineExceeded(id, atMs) {
15
- return { type: "deadline_exceeded", id, atMs };
16
- }
17
-
18
- export { deadlineExceeded, deadlineSub, subscribeDeadline };
1
+ export { deadlineExceeded, deadlineSub, subscribeDeadline } from '../chunk-QGZW4W3E.js';
2
+ import '../chunk-2YFLOCWX.js';
3
+ import '../chunk-2WZHS4P2.js';
4
+ import '../chunk-BNJYE4AU.js';
19
5
  //# sourceMappingURL=index.js.map
20
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/deadline/index.ts"],"names":[],"mappings":";;;;AAuFO,SAAS,WAAA,CAAY,IAAY,IAAA,EAA2B;AACjE,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,CAAM,EAAE,CAAA,EAAG,IAAA,EAAM,YAAY,IAAA,EAAK;AACjD;AAmBO,IAAM,iBAAA,GAA8E,CACzF,GAAA,EACA,GAAA,EACA,QAAA,KACG;AAKH,EAAA,MAAM,WAAA,GAAc,KAAK,GAAA,CAAI,CAAA,EAAG,IAAI,IAAA,GAAO,IAAA,CAAK,KAAK,CAAA;AAMrD,EAAA,OAAO,WAAA;AAAA,IAAiE,CAAC,KAAA,KACvE,gBAAA,CAAiB,KAAA,CAAM,EAAA,EAAI,MAAM,IAAI;AAAA,GACvC,CAAE,EAAE,GAAG,GAAA,EAAK,SAAS,WAAA,EAAY,EAAG,KAAK,QAAQ,CAAA;AACnD;AAOO,SAAS,gBAAA,CAAiB,IAAY,IAAA,EAAgC;AAC3E,EAAA,OAAO,EAAE,IAAA,EAAM,mBAAA,EAAqB,EAAA,EAAI,IAAA,EAAK;AAC/C","file":"index.js","sourcesContent":["/**\n * @demlik/tea/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock\n * deadline is reached.\n *\n * Generalizes the recurring \"auto-fail a machine at T+N\" / \"15-minute stale\n * guard\" pattern: a Sub whose `subscriptions(state)` returns it only while the\n * deadline should be armed, and which dispatches a single `DeadlineExceeded`\n * Msg when the wall clock crosses `atMs`.\n *\n * Difference from `fromTimeout` (relative — \"fire after N ms\"): a deadline is\n * an ABSOLUTE target. The delay is computed at subscribe time as\n * `max(0, atMs - Date.now())`. The consequence that matters: subscribing LATE —\n * e.g. after a `Store` rehydrate that resumes a machine whose deadline was set\n * before the page reloaded — still fires at the correct absolute moment,\n * because the remaining delay is recomputed from the current clock, not stored\n * in the Sub. A deadline already in the past at subscribe time yields delay `0`,\n * so `setTimeout(fn, 0)` fires on the NEXT tick (never synchronously inside the\n * reconcile pass — the substrate must finish wiring all subs before any Msg\n * lands).\n *\n * Composition over reinvention: the timer lifecycle is `fromTimeout`'s, not\n * redrawn here. `deadlineSub` produces the Sub literal carrying `atMs`; the\n * `subscribe` cell (`subscribeDeadline`) translates `atMs` → a relative delay\n * and delegates the `setTimeout` / `clearTimeout` to `fromTimeout`. The clock\n * read (`Date.now()`) lives only in the subscribe handler — never in a reducer\n * (invariant 2) — and is exercised in tests via vitest fake timers, matching\n * the no-injection convention of the `fromTimeout` / `fromInterval` exemplars.\n *\n * Strengthens invariant 4 (external time is a subscription — the deadline is a\n * `Sub` the runtime reconciles, never a `setTimeout` leaked into a reducer) and\n * invariant 7 (identity is explicit — the Sub carries a stable `SubId`, so the\n * reconcile pass leaves it running across transitions instead of churning it).\n */\n\nimport { type Sub, subId } from \"../index\";\nimport { fromTimeout } from \"../subs/from-timeout\";\nimport type { SubscribeHandler } from \"../subs/types\";\n\n/**\n * The Sub variant a deadline produces. `atMs` is the absolute target — epoch\n * milliseconds (the `Date.now()` / `Date.parse(...)` scale), NOT a relative\n * delay. It rides on the Sub so the reconcile pass keys identity by `id`\n * alone: the same `id` across transitions means the same armed timer, even if\n * a later `subscriptions(state)` recomputes the literal.\n *\n * Lowercase `\"deadline\"` discriminant per the Sub naming convention (a source\n * noun that doubles as the SubId family); same shape family as the\n * `TimeoutSubData` the `fromTimeout` factory consumes.\n */\nexport type DeadlineSub = Sub<\"deadline\"> & { readonly atMs: number };\n\n/**\n * The Msg the deadline dispatches when the wall clock crosses `atMs`. Exported\n * as a tagged shape so consumers can union it into their machine's Msg type and\n * handle it in a reducer cell. `id` lets a reducer disambiguate WHICH deadline\n * fired when a machine arms several at once; `atMs` echoes the target the timer\n * was scheduled against (useful for assertions / observability).\n *\n * Snake-shaped `\"deadline_exceeded\"` matches the tagged-Msg style this module's\n * orchestrator requested; the actor-in-name PascalCase convention\n * (`TimerFiredDeadline`) is the consumer's to apply at their own boundary if\n * they prefer — `deadlineExceeded(...)` only fixes the wire shape, not the\n * consumer's local dialect.\n */\nexport type DeadlineExceeded = {\n readonly type: \"deadline_exceeded\";\n readonly id: string;\n readonly atMs: number;\n};\n\n/**\n * Build a deadline Sub literal. Pure data — no clock read, no timer; the timer\n * is armed later by the `subscribe` cell. `id` is branded via `subId(...)` so\n * accidental raw-string drift fails at the type level (invariant 7).\n *\n * Return the result directly from `subscriptions(state)` while the deadline\n * should be armed; drop it (return `[]`, or stop returning this id) to disarm —\n * the reconcile pass calls the cleanup, clearing the pending timer before it\n * fires. That is the \"cancel on state exit\" lifecycle, identical to\n * `fromTimeout`'s.\n *\n * @param id Stable identity for this deadline. Same `id` across transitions =\n * same armed timer (no churn). Use distinct ids for distinct\n * deadlines on the same machine.\n * @param atMs Absolute target in epoch milliseconds (e.g. `Date.now() + 900_000`\n * for a 15-minute guard, or a persisted `expiresAt`).\n */\nexport function deadlineSub(id: string, atMs: number): DeadlineSub {\n return { id: subId(id), type: \"deadline\", atMs };\n}\n\n/**\n * The `subscribe[\"deadline\"]` cell. Arms a one-shot timer for the remaining\n * delay `max(0, atMs - Date.now())` and dispatches `deadlineExceeded(...)` when\n * it fires; returns a cleanup that clears the pending timer.\n *\n * Composes on `fromTimeout`: it adapts the absolute `atMs` on the Sub to the\n * relative `delayMs` that `fromTimeout` schedules against, then hands off the\n * `setTimeout` / `clearTimeout` lifecycle wholesale. A past deadline collapses\n * to `delayMs: 0`, so `fromTimeout` fires on the next tick — asynchronously,\n * never inside the reconcile pass.\n *\n * Assign directly to a `Subscribe` cell:\n *\n * subscribe: {\n * deadline: subscribeDeadline,\n * }\n */\nexport const subscribeDeadline: SubscribeHandler<DeadlineSub, DeadlineExceeded, unknown> = (\n sub,\n ctx,\n dispatch,\n) => {\n // Recompute the remaining delay from the CURRENT clock so a late subscribe\n // (post-rehydrate) still targets the correct absolute moment. `max(0, …)`\n // keeps a past deadline at delay 0 → setTimeout(fn, 0) → fires next tick, not\n // synchronously.\n const remainingMs = Math.max(0, sub.atMs - Date.now());\n\n // Delegate the timer lifecycle to fromTimeout: build the relative-delay Sub\n // shape it consumes (`{ ...sub, delayMs }`) and let it own setTimeout /\n // clearTimeout. The msgFn closes over the deadline's identity + target so the\n // dispatched Msg carries both.\n return fromTimeout<DeadlineSub & { delayMs: number }, DeadlineExceeded>((armed) =>\n deadlineExceeded(armed.id, armed.atMs),\n )({ ...sub, delayMs: remainingMs }, ctx, dispatch);\n};\n\n/**\n * Construct the Msg the deadline dispatches. Exported so consumers can build /\n * assert the same shape (and so the subscribe cell and tests share one\n * constructor rather than two literals that can drift).\n */\nexport function deadlineExceeded(id: string, atMs: number): DeadlineExceeded {\n return { type: \"deadline_exceeded\", id, atMs };\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -1,56 +1,3 @@
1
- // src/debounce/index.ts
2
- function debounce(fn, ms, opts) {
3
- const leading = opts?.leading ?? false;
4
- const trailing = opts?.trailing ?? true;
5
- let handle = null;
6
- let pendingArgs = null;
7
- let leadingFired = false;
8
- function clearHandle() {
9
- if (handle !== null) {
10
- clearTimeout(handle);
11
- handle = null;
12
- }
13
- }
14
- function onTimerExpired() {
15
- handle = null;
16
- if (trailing && pendingArgs !== null) {
17
- const args = pendingArgs;
18
- pendingArgs = null;
19
- fn(...args);
20
- }
21
- leadingFired = false;
22
- }
23
- const debounced = (...args) => {
24
- const isBurstStart = handle === null;
25
- if (leading && isBurstStart && !leadingFired) {
26
- leadingFired = true;
27
- fn(...args);
28
- } else if (trailing) {
29
- pendingArgs = args;
30
- }
31
- clearHandle();
32
- handle = setTimeout(onTimerExpired, ms);
33
- };
34
- debounced.cancel = () => {
35
- clearHandle();
36
- pendingArgs = null;
37
- leadingFired = false;
38
- };
39
- debounced.flush = () => {
40
- if (pendingArgs !== null) {
41
- const args = pendingArgs;
42
- pendingArgs = null;
43
- clearHandle();
44
- leadingFired = false;
45
- fn(...args);
46
- } else {
47
- clearHandle();
48
- leadingFired = false;
49
- }
50
- };
51
- return debounced;
52
- }
53
-
54
- export { debounce };
1
+ export { debounce } from '../chunk-EQRUAP2A.js';
55
2
  //# sourceMappingURL=index.js.map
56
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/debounce/index.ts"],"names":[],"mappings":";AA8GO,SAAS,QAAA,CACd,EAAA,EACA,EAAA,EACA,IAAA,EACc;AACd,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,IAAW,KAAA;AACjC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,IAAA;AAMnC,EAAA,IAAI,MAAA,GAA+C,IAAA;AAOnD,EAAA,IAAI,WAAA,GAAwB,IAAA;AAK5B,EAAA,IAAI,YAAA,GAAe,KAAA;AAEnB,EAAA,SAAS,WAAA,GAAoB;AAC3B,IAAA,IAAI,WAAW,IAAA,EAAM;AACnB,MAAA,YAAA,CAAa,MAAM,CAAA;AACnB,MAAA,MAAA,GAAS,IAAA;AAAA,IACX;AAAA,EACF;AAKA,EAAA,SAAS,cAAA,GAAuB;AAC9B,IAAA,MAAA,GAAS,IAAA;AAQT,IAAA,IAAI,QAAA,IAAY,gBAAgB,IAAA,EAAM;AACpC,MAAA,MAAM,IAAA,GAAO,WAAA;AACb,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IACZ;AAEA,IAAA,YAAA,GAAe,KAAA;AAAA,EACjB;AAEA,EAAA,MAAM,SAAA,GAAY,IAAI,IAAA,KAAkB;AAItC,IAAA,MAAM,eAAe,MAAA,KAAW,IAAA;AAChC,IAAA,IAAI,OAAA,IAAW,YAAA,IAAgB,CAAC,YAAA,EAAc;AAC5C,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IACZ,WAAW,QAAA,EAAU;AAGnB,MAAA,WAAA,GAAc,IAAA;AAAA,IAChB;AAKA,IAAA,WAAA,EAAY;AACZ,IAAA,MAAA,GAAS,UAAA,CAAW,gBAAgB,EAAE,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,SAAA,CAAU,SAAS,MAAY;AAC7B,IAAA,WAAA,EAAY;AACZ,IAAA,WAAA,GAAc,IAAA;AACd,IAAA,YAAA,GAAe,KAAA;AAAA,EACjB,CAAA;AAEA,EAAA,SAAA,CAAU,QAAQ,MAAY;AAI5B,IAAA,IAAI,gBAAgB,IAAA,EAAM;AACxB,MAAA,MAAM,IAAA,GAAO,WAAA;AACb,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,WAAA,EAAY;AAEZ,MAAA,YAAA,GAAe,KAAA;AACf,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IACZ,CAAA,MAAO;AAGL,MAAA,WAAA,EAAY;AACZ,MAAA,YAAA,GAAe,KAAA;AAAA,IACjB;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,SAAA;AACT","file":"index.js","sourcesContent":["/**\n * @demlik/tea/debounce — a timer-based call transformer that coalesces a burst\n * of calls into a single fire.\n *\n * Sibling of `@demlik/tea/throttle` — the two share a SYMMETRIC surface\n * (`Debounced<A>` / `Throttled<A>`, both callable + `.cancel()` + `.flush()`,\n * both `{ leading?, trailing? }`) so they read as a pair. They are kept in\n * separate files with NO cross-import: each is a self-contained timer, and the\n * coalesce-vs-drop policies are different enough that sharing code would hide\n * the distinction. Read one to understand the other; do not couple them.\n *\n * WHAT THIS IS NOT — and why it lives outside `update`:\n *\n * - NOT a reducer / Transitions cell. Reducers are PURE (invariant 2 — same\n * inputs → same outputs, no `Date.now`, no timers). A debounce holds a live\n * `setTimeout` handle and mutable pending-args state; folding it into\n * `update` would smuggle a timer into the pure core.\n * - NOT a Sub. A Sub is a continuous SOURCE of Msgs the substrate reconciles\n * by id every transition (invariant 4). A debounce is a SINK transformer:\n * it wraps a `void`-returning handler (the canonical one is\n * `Runtime.dispatch`) so bursty HOST events coalesce BEFORE they ever\n * become a Msg.\n *\n * It is a higher-order function at the host boundary (pattern 14 — TEA owns\n * state, JS owns timing). The host attaches the wrapped function to a noisy\n * event source (`scroll`, `resize`, `input`, a ResizeObserver) and the\n * substrate downstream only ever sees the coalesced Msg:\n *\n * const onResized = debounce(\n * () => runtime.dispatch({ type: \"BrowserResizedWindow\" }),\n * 150,\n * );\n * window.addEventListener(\"resize\", onResized);\n * // cleanup: onResized.cancel(); window.removeEventListener(\"resize\", onResized);\n *\n * NOT exported from the package root — reached via the `@demlik/tea/debounce`\n * subpath, same one-shape-per-package rule as `rate-limit`, `deadline`, and\n * `work-queue`.\n *\n * Uses the universal `setTimeout` / `clearTimeout` timer idiom (same discipline\n * as `subs/from-timeout.ts`): exactly one live handle at a time, every arm\n * clears the previous, every fire / cancel clears the handle and nulls it so a\n * stale callback can never run.\n */\n\n/**\n * A debounced wrapper around `fn`. Callable with the SAME argument tuple as\n * `fn` — `(...args: A) => void` — plus two control methods that share names and\n * shapes with `Throttled<A>` so the two modules read as a symmetric pair.\n */\nexport interface Debounced<A extends readonly unknown[]> {\n /** Schedule (or re-schedule) a fire of the wrapped `fn`. See `debounce`. */\n (...args: A): void;\n /**\n * Drop any pending trailing fire WITHOUT invoking `fn`. Clears the timer and\n * forgets the captured args. Idempotent — calling it with nothing pending is\n * a no-op. The leading-edge latch (if `leading` is enabled) also resets, so\n * the next call after `cancel` is treated as a fresh burst.\n *\n * The host-cleanup partner of `removeEventListener`: cancel the pending fire\n * when the component unmounts / the listener detaches, so a queued\n * `dispatch` can't land after teardown.\n */\n cancel(): void;\n /**\n * Fire any pending trailing call IMMEDIATELY with its captured args, then\n * clear the timer. No-op when nothing is pending. Use to force the last\n * coalesced call out early — e.g. flush a debounced save on `beforeunload`,\n * or flush a debounced search when the user presses Enter.\n */\n flush(): void;\n}\n\n/**\n * Wrap `fn` so a BURST of calls collapses to a single invocation.\n *\n * Trailing edge (the default): each call (re)starts a `ms` timer; `fn` fires\n * once with the LAST args after `ms` of quiet. Every call within `ms` of the\n * previous resets the timer — the classic \"wait until the user stops typing\"\n * behavior.\n *\n * @param fn The handler to coalesce. Typically `(...) => runtime.dispatch(...)`,\n * but any `(...args: A) => void` works. The return value is\n * ignored — debounce is a fire transformer, not a value pipe.\n * @param ms Quiet window in milliseconds. Calls closer together than `ms`\n * coalesce; a gap of at least `ms` lets the trailing fire through.\n * @param opts.leading Fire on the FIRST call of a burst (the leading edge),\n * with that first call's args. Default `false`.\n * @param opts.trailing Fire on the trailing edge with the LAST call's args.\n * Default `true`.\n *\n * Edge combinations (matching lodash's settled semantics, the de-facto\n * standard the ecosystem rediscovered):\n *\n * - `{ trailing: true }` (default): one fire, after the burst, last args.\n * - `{ leading: true, trailing: false }`: one fire, at the burst START,\n * first args. Subsequent calls within the window are swallowed; the timer\n * only re-opens the \"can lead again\" latch after `ms` of quiet.\n * - `{ leading: true, trailing: true }`: fires at the start AND end of a\n * burst — BUT the trailing fire is SUPPRESSED for a burst of exactly ONE\n * call (the leading fire already covered it, so a lone call doesn't\n * double-fire). This matches lodash and is the behavior tests pin.\n * - `{ leading: false, trailing: false }`: never fires. Degenerate but legal;\n * we don't throw — the caller asked for a no-op transformer.\n *\n * WHY a default of trailing-only: a debounce's whole job is \"act after the\n * activity settles.\" The trailing edge IS that semantic — fire with the final\n * state of the burst (the last keystroke, the final scroll position). Leading\n * is the opt-in for \"respond instantly, then go quiet.\"\n */\nexport function debounce<A extends readonly unknown[]>(\n fn: (...args: A) => void,\n ms: number,\n opts?: { leading?: boolean; trailing?: boolean },\n): Debounced<A> {\n const leading = opts?.leading ?? false;\n const trailing = opts?.trailing ?? true;\n\n // Exactly one live handle at a time (the from-timeout discipline). `null`\n // when no fire is pending — the single source of truth for \"is a timer\n // armed?\". Every arm clears the previous handle before opening a new one, so\n // we never leak overlapping timers across a burst.\n let handle: ReturnType<typeof setTimeout> | null = null;\n // The most recent args, held only while a trailing fire is pending. Captured\n // on every call (the trailing edge fires with the LAST args), cleared the\n // instant we fire / cancel so a stale tuple can never leak into a later\n // burst. `null` (not `undefined`) is the empty sentinel because `A` may\n // itself contain `undefined` elements — a present-but-empty tuple is still a\n // pending call, distinct from \"nothing pending\".\n let pendingArgs: A | null = null;\n // Leading-edge latch: true once a leading fire has happened for the current\n // burst, reset to false after `ms` of quiet (the timer's expiry) or on\n // `cancel`. Gates the \"fire on first call of a burst, then stay quiet\" rule\n // so mid-burst calls don't re-trigger the leading fire.\n let leadingFired = false;\n\n function clearHandle(): void {\n if (handle !== null) {\n clearTimeout(handle);\n handle = null;\n }\n }\n\n // Timer expiry: the burst has gone quiet for `ms`. Fire the trailing edge if\n // one is owed, then reset all burst state so the NEXT call starts a fresh\n // burst (re-arming the leading latch, clearing pending args).\n function onTimerExpired(): void {\n handle = null;\n // Suppress the trailing fire for a lone leading-only-eligible call:\n // when `leadingFired` is true AND trailing is enabled, a burst of exactly\n // one call already fired on the leading edge — `pendingArgs` was cleared\n // at that point, so there is nothing to fire here. A burst of 2+ calls\n // re-captured `pendingArgs` after the leading fire, so the trailing fire\n // lands with the last args. The `pendingArgs !== null` guard encodes\n // exactly that distinction; no separate call-count needed.\n if (trailing && pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n fn(...args);\n }\n // Burst over — re-open the leading latch for the next burst.\n leadingFired = false;\n }\n\n const debounced = (...args: A): void => {\n // Leading edge: the FIRST call of a burst (no timer armed yet, latch open).\n // Fire immediately with this call's args; do NOT stash them as pending so a\n // lone call won't also fire on the trailing edge.\n const isBurstStart = handle === null;\n if (leading && isBurstStart && !leadingFired) {\n leadingFired = true;\n fn(...args);\n } else if (trailing) {\n // Capture the latest args for the trailing fire. Overwrites any prior\n // pending tuple — the trailing edge fires with the LAST args, by design.\n pendingArgs = args;\n }\n\n // (Re)arm the quiet-window timer. Clearing the previous handle is what\n // makes the window SLIDE: every call within `ms` pushes the trailing fire\n // out to `ms` after THIS call.\n clearHandle();\n handle = setTimeout(onTimerExpired, ms);\n };\n\n debounced.cancel = (): void => {\n clearHandle();\n pendingArgs = null;\n leadingFired = false;\n };\n\n debounced.flush = (): void => {\n // Force the pending trailing fire out now. Mirrors `onTimerExpired`'s fire\n // branch but is caller-driven rather than timer-driven, so it also tears\n // down the armed timer (a timer-driven expiry already nulled `handle`).\n if (pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n clearHandle();\n // A flushed trailing fire ends the burst, same as a natural expiry.\n leadingFired = false;\n fn(...args);\n } else {\n // Nothing pending — still tear down a lingering leading-only timer so a\n // post-flush call leads cleanly.\n clearHandle();\n leadingFired = false;\n }\n };\n\n return debounced;\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -111,7 +111,7 @@ interface StateInspectorProps {
111
111
  */
112
112
  className?: string;
113
113
  }
114
- declare function StateInspector({ state, flashKey, className }: StateInspectorProps): react_jsx_runtime.JSX.Element;
114
+ declare function StateInspector({ state, flashKey, className, }: StateInspectorProps): react_jsx_runtime.JSX.Element;
115
115
 
116
116
  /**
117
117
  * Wrap a `dispatch` so every Msg it sees lands in a bounded history buffer.