@demlik/tea 0.1.0 → 0.1.2

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 (243) 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-27N2SPTN.js +222 -0
  18. package/dist/chunk-27N2SPTN.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-3XJWC7VW.js +61 -0
  24. package/dist/chunk-3XJWC7VW.js.map +1 -0
  25. package/dist/chunk-5DEQ5I3U.js +132 -0
  26. package/dist/chunk-5DEQ5I3U.js.map +1 -0
  27. package/dist/chunk-5VNWZADP.js +49 -0
  28. package/dist/chunk-5VNWZADP.js.map +1 -0
  29. package/dist/chunk-6RGBKODV.js +268 -0
  30. package/dist/chunk-6RGBKODV.js.map +1 -0
  31. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  32. package/dist/chunk-7FYQGDMF.js.map +1 -0
  33. package/dist/chunk-7W4LMPUW.js +186 -0
  34. package/dist/chunk-7W4LMPUW.js.map +1 -0
  35. package/dist/chunk-BYQUGDD3.js +16 -0
  36. package/dist/chunk-BYQUGDD3.js.map +1 -0
  37. package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
  38. package/dist/chunk-DILMKOZ7.js.map +1 -0
  39. package/dist/chunk-EHIX7NYZ.js +93 -0
  40. package/dist/chunk-EHIX7NYZ.js.map +1 -0
  41. package/dist/chunk-HCEVHFJ7.js +54 -0
  42. package/dist/chunk-HCEVHFJ7.js.map +1 -0
  43. package/dist/chunk-HJO6VNQI.js +15 -0
  44. package/dist/chunk-HJO6VNQI.js.map +1 -0
  45. package/dist/chunk-IN33Q4VJ.js +52 -0
  46. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  47. package/dist/chunk-J6527VRQ.js +56 -0
  48. package/dist/chunk-J6527VRQ.js.map +1 -0
  49. package/dist/chunk-JELSZN25.js +47 -0
  50. package/dist/chunk-JELSZN25.js.map +1 -0
  51. package/dist/chunk-K575PT4S.js +58 -0
  52. package/dist/chunk-K575PT4S.js.map +1 -0
  53. package/dist/chunk-K7Q23EQ4.js +20 -0
  54. package/dist/chunk-K7Q23EQ4.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-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
  58. package/dist/chunk-LP7VQBRG.js.map +1 -0
  59. package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
  60. package/dist/chunk-MMH7CW5K.js.map +1 -0
  61. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  62. package/dist/chunk-NSDFA6M6.js.map +1 -0
  63. package/dist/chunk-ON24RKSE.js +60 -0
  64. package/dist/chunk-ON24RKSE.js.map +1 -0
  65. package/dist/chunk-PS23H6KQ.js +87 -0
  66. package/dist/chunk-PS23H6KQ.js.map +1 -0
  67. package/dist/chunk-QLA76GIT.js +162 -0
  68. package/dist/chunk-QLA76GIT.js.map +1 -0
  69. package/dist/chunk-S3FQN4SL.js +506 -0
  70. package/dist/chunk-S3FQN4SL.js.map +1 -0
  71. package/dist/chunk-SC3FAFZN.js +53 -0
  72. package/dist/chunk-SC3FAFZN.js.map +1 -0
  73. package/dist/chunk-SINXE47Y.js +55 -0
  74. package/dist/chunk-SINXE47Y.js.map +1 -0
  75. package/dist/chunk-SQTCS44W.js +33 -0
  76. package/dist/chunk-SQTCS44W.js.map +1 -0
  77. package/dist/chunk-TGG26XMK.js +52 -0
  78. package/dist/chunk-TGG26XMK.js.map +1 -0
  79. package/dist/chunk-UV6UWGEO.js +147 -0
  80. package/dist/chunk-UV6UWGEO.js.map +1 -0
  81. package/dist/chunk-UYWUVEP6.js +158 -0
  82. package/dist/chunk-UYWUVEP6.js.map +1 -0
  83. package/dist/chunk-VLGARN6H.js +202 -0
  84. package/dist/chunk-VLGARN6H.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-BoB6t7dB.d.ts +188 -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.d.ts +6 -0
  92. package/dist/debounce/index.js +1 -54
  93. package/dist/debounce/index.js.map +1 -1
  94. package/dist/devtools/index.d.ts +1 -1
  95. package/dist/devtools/index.js +18 -6
  96. package/dist/devtools/index.js.map +1 -1
  97. package/dist/do/index.d.ts +1723 -92
  98. package/dist/do/index.js +673 -31
  99. package/dist/do/index.js.map +1 -1
  100. package/dist/durable-effects-CO9Uh971.d.ts +180 -0
  101. package/dist/extension/index.d.ts +31 -13
  102. package/dist/extension/index.js +5 -3
  103. package/dist/extension/index.js.map +1 -1
  104. package/dist/extension/react.js +8 -3
  105. package/dist/extension/react.js.map +1 -1
  106. package/dist/extension/subs/index.d.ts +2 -2
  107. package/dist/extension/subs/index.js.map +1 -1
  108. package/dist/extension/test-utils.js +11 -3
  109. package/dist/extension/test-utils.js.map +1 -1
  110. package/dist/fan-out/index.d.ts +239 -0
  111. package/dist/fan-out/index.js +5 -0
  112. package/dist/fan-out/index.js.map +1 -0
  113. package/dist/idempotency/adapter.d.ts +62 -0
  114. package/dist/idempotency/adapter.js +4 -0
  115. package/dist/idempotency/adapter.js.map +1 -0
  116. package/dist/idempotency/index.js +1 -56
  117. package/dist/idempotency/index.js.map +1 -1
  118. package/dist/idempotent-intake/index.d.ts +186 -0
  119. package/dist/idempotent-intake/index.js +7 -0
  120. package/dist/idempotent-intake/index.js.map +1 -0
  121. package/dist/index.d.ts +341 -191
  122. package/dist/index.js +2 -1
  123. package/dist/llm-call/index.d.ts +309 -0
  124. package/dist/llm-call/index.js +14 -0
  125. package/dist/llm-call/index.js.map +1 -0
  126. package/dist/machine-viz/index.d.ts +4 -4
  127. package/dist/machine-viz/index.js +5 -9
  128. package/dist/machine-viz/index.js.map +1 -1
  129. package/dist/mem/index.d.ts +1 -0
  130. package/dist/monitored-run/index.d.ts +253 -0
  131. package/dist/monitored-run/index.js +10 -0
  132. package/dist/monitored-run/index.js.map +1 -0
  133. package/dist/node/index.d.ts +5 -5
  134. package/dist/node/index.js.map +1 -1
  135. package/dist/paginated-walk/index.d.ts +234 -0
  136. package/dist/paginated-walk/index.js +15 -0
  137. package/dist/paginated-walk/index.js.map +1 -0
  138. package/dist/paginator/index.d.ts +271 -0
  139. package/dist/paginator/index.js +3 -0
  140. package/dist/paginator/index.js.map +1 -0
  141. package/dist/parity/index.d.ts +92 -0
  142. package/dist/parity/index.js +78 -0
  143. package/dist/parity/index.js.map +1 -0
  144. package/dist/pbt/arbitraries/index.d.ts +1 -1
  145. package/dist/pbt/arbitraries/index.js +1 -1
  146. package/dist/pbt/index.d.ts +15 -8
  147. package/dist/pbt/index.js +6 -11
  148. package/dist/pbt/index.js.map +1 -1
  149. package/dist/pbt/runners/index.d.ts +1 -1
  150. package/dist/pbt/runners/index.js +3 -2
  151. package/dist/poller/index.d.ts +288 -0
  152. package/dist/poller/index.js +79 -0
  153. package/dist/poller/index.js.map +1 -0
  154. package/dist/prediction/index.d.ts +130 -0
  155. package/dist/prediction/index.js +4 -0
  156. package/dist/prediction/index.js.map +1 -0
  157. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  158. package/dist/pure/index.d.ts +2 -0
  159. package/dist/pure/index.js +4 -0
  160. package/dist/pure/index.js.map +1 -0
  161. package/dist/raft/demo.d.ts +105 -0
  162. package/dist/raft/demo.js +172 -0
  163. package/dist/raft/demo.js.map +1 -0
  164. package/dist/raft/index.d.ts +548 -0
  165. package/dist/raft/index.js +7 -0
  166. package/dist/raft/index.js.map +1 -0
  167. package/dist/raft/sim.d.ts +222 -0
  168. package/dist/raft/sim.js +8 -0
  169. package/dist/raft/sim.js.map +1 -0
  170. package/dist/raft/timeline.d.ts +46 -0
  171. package/dist/raft/timeline.js +62 -0
  172. package/dist/raft/timeline.js.map +1 -0
  173. package/dist/rate-limit/index.js +1 -43
  174. package/dist/rate-limit/index.js.map +1 -1
  175. package/dist/react/index.d.ts +2 -1
  176. package/dist/react/index.js +43 -20
  177. package/dist/react/index.js.map +1 -1
  178. package/dist/reconciler/index.d.ts +192 -0
  179. package/dist/reconciler/index.js +166 -0
  180. package/dist/reconciler/index.js.map +1 -0
  181. package/dist/recorder/index.d.ts +7 -3
  182. package/dist/recorder/index.js +1 -155
  183. package/dist/recorder/index.js.map +1 -1
  184. package/dist/resilient-call/index.d.ts +294 -0
  185. package/dist/resilient-call/index.js +13 -0
  186. package/dist/resilient-call/index.js.map +1 -0
  187. package/dist/retry-backoff/index.d.ts +42 -5
  188. package/dist/retry-backoff/index.js +1 -35
  189. package/dist/retry-backoff/index.js.map +1 -1
  190. package/dist/retry-to-success/index.d.ts +109 -0
  191. package/dist/retry-to-success/index.js +35 -0
  192. package/dist/retry-to-success/index.js.map +1 -0
  193. package/dist/saga/index.d.ts +238 -0
  194. package/dist/saga/index.js +150 -0
  195. package/dist/saga/index.js.map +1 -0
  196. package/dist/snapshot/index.d.ts +334 -0
  197. package/dist/snapshot/index.js +5 -0
  198. package/dist/snapshot/index.js.map +1 -0
  199. package/dist/subs/index.d.ts +48 -10
  200. package/dist/subs/index.js +1 -1
  201. package/dist/testing/index.d.ts +107 -2
  202. package/dist/testing/index.js +204 -6
  203. package/dist/testing/index.js.map +1 -1
  204. package/dist/throttle/index.d.ts +9 -0
  205. package/dist/throttle/index.js +1 -52
  206. package/dist/throttle/index.js.map +1 -1
  207. package/dist/throttled-input/index.d.ts +380 -0
  208. package/dist/throttled-input/index.js +88 -0
  209. package/dist/throttled-input/index.js.map +1 -0
  210. package/dist/token-refresh/index.d.ts +215 -0
  211. package/dist/token-refresh/index.js +6 -0
  212. package/dist/token-refresh/index.js.map +1 -0
  213. package/dist/trace-replay/index.d.ts +11 -2
  214. package/dist/trace-replay/index.js +3 -55
  215. package/dist/trace-replay/index.js.map +1 -1
  216. package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
  217. package/dist/with-deadline/index.d.ts +196 -0
  218. package/dist/with-deadline/index.js +111 -0
  219. package/dist/with-deadline/index.js.map +1 -0
  220. package/dist/with-resilience/index.d.ts +205 -0
  221. package/dist/with-resilience/index.js +251 -0
  222. package/dist/with-resilience/index.js.map +1 -0
  223. package/dist/with-telemetry/index.d.ts +149 -0
  224. package/dist/with-telemetry/index.js +70 -0
  225. package/dist/with-telemetry/index.js.map +1 -0
  226. package/dist/work-queue/adapter.d.ts +82 -0
  227. package/dist/work-queue/adapter.js +4 -0
  228. package/dist/work-queue/adapter.js.map +1 -0
  229. package/dist/work-queue/index.d.ts +25 -11
  230. package/dist/work-queue/index.js +1 -46
  231. package/dist/work-queue/index.js.map +1 -1
  232. package/dist/work-queue/ops.d.ts +69 -0
  233. package/dist/work-queue/ops.js +3 -0
  234. package/dist/work-queue/ops.js.map +1 -0
  235. package/dist/workflow/index.d.ts +450 -0
  236. package/dist/workflow/index.js +225 -0
  237. package/dist/workflow/index.js.map +1 -0
  238. package/package.json +168 -25
  239. package/dist/chunk-CMG2KMWX.js.map +0 -1
  240. package/dist/chunk-PYBQREFU.js.map +0 -1
  241. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  242. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  243. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -0,0 +1,49 @@
1
+ // src/retry-backoff/index.ts
2
+ function asRng(fn, probe = false) {
3
+ if (probe) {
4
+ const sample = fn();
5
+ if (!(sample >= 0 && sample < 1)) {
6
+ throw new RangeError(
7
+ `asRng: generator returned ${sample}, outside the [0, 1) contract`
8
+ );
9
+ }
10
+ }
11
+ return fn;
12
+ }
13
+ var defaultRng = Math.random;
14
+ var defaultRetryPolicy = {
15
+ baseMs: 100,
16
+ factor: 2,
17
+ capMs: 3e4,
18
+ maxAttempts: 5,
19
+ jitter: "full"
20
+ };
21
+ function backoffDelay(attempt, policy, rng = defaultRng) {
22
+ const safeAttempt = attempt < 0 ? 0 : attempt;
23
+ const exponential = policy.baseMs * policy.factor ** safeAttempt;
24
+ const d = Number.isNaN(exponential) ? 0 : Math.min(policy.capMs, exponential);
25
+ switch (policy.jitter) {
26
+ case "none":
27
+ return d;
28
+ case "full":
29
+ return Math.min(d, rng() * d);
30
+ case "equal":
31
+ return Math.min(d, d / 2 + rng() * (d / 2));
32
+ }
33
+ }
34
+ function initRetry() {
35
+ return { attempt: 0 };
36
+ }
37
+ function recordFailure(state, error) {
38
+ return { attempt: state.attempt + 1, lastError: error };
39
+ }
40
+ function shouldRetry(state, policy) {
41
+ return state.attempt < policy.maxAttempts;
42
+ }
43
+ function nextDelayMs(state, policy, rng = defaultRng) {
44
+ return backoffDelay(state.attempt, policy, rng);
45
+ }
46
+
47
+ export { asRng, backoffDelay, defaultRetryPolicy, defaultRng, initRetry, nextDelayMs, recordFailure, shouldRetry };
48
+ //# sourceMappingURL=chunk-5VNWZADP.js.map
49
+ //# sourceMappingURL=chunk-5VNWZADP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/retry-backoff/index.ts"],"names":[],"mappings":";AA8EO,SAAS,KAAA,CAAM,EAAA,EAAkB,KAAA,GAAQ,KAAA,EAAY;AAC1D,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,MAAM,SAAS,EAAA,EAAG;AAClB,IAAA,IAAI,EAAE,MAAA,IAAU,CAAA,IAAK,MAAA,GAAS,CAAA,CAAA,EAAI;AAChC,MAAA,MAAM,IAAI,UAAA;AAAA,QACR,6BAA6B,MAAM,CAAA,6BAAA;AAAA,OACrC;AAAA,IACF;AAAA,EACF;AACA,EAAA,OAAO,EAAA;AACT;AAOO,IAAM,aAAkB,IAAA,CAAK;AA4B7B,IAAM,kBAAA,GAAkC;AAAA,EAC7C,MAAA,EAAQ,GAAA;AAAA,EACR,MAAA,EAAQ,CAAA;AAAA,EACR,KAAA,EAAO,GAAA;AAAA,EACP,WAAA,EAAa,CAAA;AAAA,EACb,MAAA,EAAQ;AACV;AAuBO,SAAS,YAAA,CACd,OAAA,EACA,MAAA,EACA,GAAA,GAAW,UAAA,EACH;AAIR,EAAA,MAAM,WAAA,GAAc,OAAA,GAAU,CAAA,GAAI,CAAA,GAAI,OAAA;AACtC,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,WAAA;AAMrD,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,KAAA,CAAM,WAAW,CAAA,GAAI,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,KAAA,EAAO,WAAW,CAAA;AAE5E,EAAA,QAAQ,OAAO,MAAA;AAAQ,IACrB,KAAK,MAAA;AACH,MAAA,OAAO,CAAA;AAAA,IACT,KAAK,MAAA;AAKH,MAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAA,KAAQ,CAAC,CAAA;AAAA,IAC9B,KAAK,OAAA;AAGH,MAAA,OAAO,IAAA,CAAK,IAAI,CAAA,EAAG,CAAA,GAAI,IAAI,GAAA,EAAI,IAAK,IAAI,CAAA,CAAE,CAAA;AAAA;AAEhD;AAgBO,SAAS,SAAA,GAAwB;AACtC,EAAA,OAAO,EAAE,SAAS,CAAA,EAAE;AACtB;AAMO,SAAS,aAAA,CAAc,OAAmB,KAAA,EAA4B;AAC3E,EAAA,OAAO,EAAE,OAAA,EAAS,KAAA,CAAM,OAAA,GAAU,CAAA,EAAG,WAAW,KAAA,EAAM;AACxD;AASO,SAAS,WAAA,CAAY,OAAmB,MAAA,EAA8B;AAC3E,EAAA,OAAO,KAAA,CAAM,UAAU,MAAA,CAAO,WAAA;AAChC;AASO,SAAS,WAAA,CACd,KAAA,EACA,MAAA,EACA,GAAA,GAAW,UAAA,EACH;AACR,EAAA,OAAO,YAAA,CAAa,KAAA,CAAM,OAAA,EAAS,MAAA,EAAQ,GAAG,CAAA;AAChD","file":"chunk-5VNWZADP.js","sourcesContent":["/**\n * @demlik/tea/retry-backoff — exponential backoff with jitter + cap, and the\n * retry-attempt state every fallible `interpret` handler folds over.\n *\n * This is a \"pure state + ops\" module in the same mold as\n * `@demlik/tea/work-queue`: a state type plus pure transition functions,\n * storage- and host-agnostic, that a consumer wires into their own reducer /\n * interpret. Nothing here reads the clock or the RNG on its own behalf —\n * randomness is *injected* (`rng`) and time is *computed by the caller* from\n * the millisecond delays this module returns. That keeps every function\n * deterministic and unit-testable, and keeps the substrate's purity invariants\n * intact when the result is threaded through `update` (invariant 2) and the\n * delay is realized as a `Cmd` the runtime performs (invariant 3).\n *\n * Typical wiring:\n *\n * // in `update`, on a failure Msg:\n * const retry = recordFailure(state.retry, msg.error);\n * if (!shouldRetry(retry, policy)) return [toGaveUp(state), []];\n * const delayMs = nextDelayMs(retry, policy); // pure: rng injected at call site\n * return [{ ...state, retry }, [{ type: \"ScheduleRetry\", delayMs, into: TimerFiredRetry }]];\n *\n * The reducer stays pure: `nextDelayMs` is called with the runtime's RNG only\n * at the emit boundary (or the caller defers the whole delay computation to the\n * `interpret` handler and passes `rng` there). Either way the substrate never\n * sees `Math.random()` inside a transition body.\n *\n * NOT a substrate primitive: it depends on nothing from `../index`. It is a\n * leaf utility consumers reach via the `@demlik/tea/retry-backoff` subpath.\n */\n\n/**\n * Jitter strategy applied to the computed backoff delay.\n *\n * Names follow the AWS \"Exponential Backoff And Jitter\" taxonomy (Brooker,\n * 2015), the same vocabulary used by the AWS SDKs and most retry libraries:\n *\n * - `\"none\"` — no jitter; the delay is the raw capped exponential `d`.\n * Deterministic, but synchronizes retries across clients\n * (thundering herd). Use only when callers are already\n * de-correlated by other means.\n * - `\"full\"` — uniform random in `[0, d]`. Maximum spread; best herd\n * avoidance, highest variance in latency.\n * - `\"equal\"` — `d/2 + random(0, d/2)`, i.e. uniform in `[d/2, d]`. Half the\n * delay is fixed, half is jittered: keeps a latency floor while\n * still de-correlating clients. A common default.\n */\nexport type Jitter = \"none\" | \"full\" | \"equal\";\n\ndeclare const RngBrand: unique symbol;\n\n/**\n * A source of uniform randomness in `[0, 1)` — the `Math.random` contract.\n *\n * Nominally branded so the `[0, 1)` range is enforced at *construction*, not\n * by a comment. A raw `() => number` does NOT satisfy `Rng`: a stray `() => 5`\n * or `() => -1` is rejected by the type before it can ever feed the jitter\n * math and breach the `0 <= delay <= capMs` postcondition. The only way to\n * mint an `Rng` is {@link asRng} (or the {@link defaultRng} const), so the\n * range obligation lives at the boundary where randomness enters the module —\n * \"parse, don't validate\" applied to a function value.\n */\nexport type Rng = (() => number) & { readonly [RngBrand]: true };\n\n/**\n * Brand a `[0, 1)` generator as an {@link Rng}. The single entry point for\n * randomness into this module: callers (production wiring and tests alike)\n * pass their raw generator through here, which optionally asserts the contract\n * and tags the value. The probe is a cheap guard against the most common\n * misuse (returning a non-unit value); it does not — and cannot — prove\n * uniformity.\n *\n * PURE with respect to the module: it neither reads the clock nor calls the\n * generator on the module's behalf unless `probe` is set. The probe is opt-in\n * (default off) so wrapping a hot generator stays allocation- and call-free.\n *\n * @throws RangeError if `probe` is enabled and a sampled value is outside `[0, 1)`.\n */\nexport function asRng(fn: () => number, probe = false): Rng {\n if (probe) {\n const sample = fn();\n if (!(sample >= 0 && sample < 1)) {\n throw new RangeError(\n `asRng: generator returned ${sample}, outside the [0, 1) contract`,\n );\n }\n }\n return fn as Rng;\n}\n\n/**\n * The production default: `Math.random`, branded. Used wherever an `Rng` is\n * required but the caller omits one — at the effect boundary, never inside a\n * reducer.\n */\nexport const defaultRng: Rng = Math.random as Rng;\n\n/**\n * A retry policy is pure configuration — the knobs of the backoff curve. It\n * carries no mutable state; `RetryState` does. One policy is shared across all\n * the attempts of one logical operation (often a module-scope const).\n */\nexport interface RetryPolicy {\n /** Delay for attempt 0, in milliseconds, before any jitter or cap. */\n readonly baseMs: number;\n /** Geometric growth factor. `2` doubles the delay each attempt. */\n readonly factor: number;\n /** Hard ceiling, in milliseconds. The capped exponential never exceeds this. */\n readonly capMs: number;\n /**\n * Maximum number of attempts. `shouldRetry` allows attempts `0 .. maxAttempts-1`,\n * so `maxAttempts: 3` means \"try once, then retry at most twice\" (attempts 0, 1, 2).\n */\n readonly maxAttempts: number;\n /** Jitter strategy applied to the capped exponential delay. */\n readonly jitter: Jitter;\n}\n\n/**\n * Sensible defaults: 100ms base, doubling, capped at 30s, up to 5 attempts,\n * full jitter (the herd-avoidance default). Tuned for network calls; override\n * per call site rather than mutating this shared const.\n */\nexport const defaultRetryPolicy: RetryPolicy = {\n baseMs: 100,\n factor: 2,\n capMs: 30_000,\n maxAttempts: 5,\n jitter: \"full\",\n};\n\n/**\n * Compute the delay for a given 0-based `attempt` under `policy`.\n *\n * PURE. The exponential is `baseMs * factor ** attempt`, clamped to `capMs`,\n * then jittered. `rng` is injected so the result is deterministic in tests —\n * pass an `rng` returning a fixed value to pin the jitter; the production\n * default reads `Math.random` only when the caller omits it (at the effect\n * boundary, never inside a reducer).\n *\n * `rng` is an {@link Rng} — a `[0, 1)` generator branded at construction via\n * {@link asRng}, so a raw `() => 5` can't be passed here. The returned delay\n * is always in `[0, capMs]` regardless of jitter strategy:\n * - `\"none\"` → exactly the capped exponential `d`.\n * - `\"full\"` → `rng() * d`, uniform in `[0, d]`.\n * - `\"equal\"` → `d/2 + rng() * d/2`, uniform in `[d/2, d]`.\n *\n * `attempt` is treated as `>= 0`; a negative attempt is clamped to 0 so a\n * caller's off-by-one never produces a fractional exponent or a delay below\n * `baseMs`. `capMs` is also the floor of the clamp, so a misconfigured\n * `capMs < baseMs` still yields a delay within `[0, capMs]`.\n */\nexport function backoffDelay(\n attempt: number,\n policy: RetryPolicy,\n rng: Rng = defaultRng,\n): number {\n // Clamp the attempt to >= 0 so factor ** attempt is never a fractional /\n // negative exponent. The exponential can overflow to Infinity for large\n // attempts; Math.min with capMs collapses that back to the ceiling.\n const safeAttempt = attempt < 0 ? 0 : attempt;\n const exponential = policy.baseMs * policy.factor ** safeAttempt;\n // `factor ** attempt` overflows to Infinity for large attempts. With\n // `baseMs > 0` that's `Infinity`, which Math.min collapses back to `capMs`.\n // With `baseMs === 0` it's `0 * Infinity === NaN`; a zero base means zero\n // delay, so treat NaN as 0 (otherwise NaN poisons the comparison and breaks\n // the `0 <= delay <= capMs` postcondition).\n const d = Number.isNaN(exponential) ? 0 : Math.min(policy.capMs, exponential);\n\n switch (policy.jitter) {\n case \"none\":\n return d;\n case \"full\":\n // Uniform in [0, d]. Math.min re-pins the upper bound: at subnormal /\n // extreme-precision `d`, `rng() * d` can round one ULP above `d`, which\n // would breach the documented `delay <= capMs` postcondition. Clamping to\n // `d` (already <= capMs) makes the invariant hold by construction.\n return Math.min(d, rng() * d);\n case \"equal\":\n // Fixed half + jittered half → uniform in [d/2, d]. Same FP-rounding clamp\n // as \"full\": `d/2 + rng()*(d/2)` can round above `d` for subnormal `d`.\n return Math.min(d, d / 2 + rng() * (d / 2));\n }\n}\n\n/**\n * Per-operation retry state: how many attempts have failed so far, and the most\n * recent error. `lastError` is `unknown` because the failure can be anything a\n * boundary surfaces (an `Error`, a tagged result, a string) — this module never\n * inspects it, it only carries it forward for the consumer's reducer to read.\n */\nexport interface RetryState {\n /** Count of failures recorded so far. Also the 0-based index of the next attempt. */\n readonly attempt: number;\n /** The error from the most recent failure, if any. Carried, never interpreted. */\n readonly lastError?: unknown;\n}\n\n/** The starting state: zero attempts, no error yet. */\nexport function initRetry(): RetryState {\n return { attempt: 0 };\n}\n\n/**\n * Record a failed attempt. PURE — returns a *new* state with `attempt`\n * incremented and `lastError` set; the input `state` is never mutated.\n */\nexport function recordFailure(state: RetryState, error: unknown): RetryState {\n return { attempt: state.attempt + 1, lastError: error };\n}\n\n/**\n * Whether another attempt is permitted under `policy`. PURE.\n *\n * `attempt` is the number of failures recorded so far (= the index of the next\n * attempt). We may retry while that index is below `maxAttempts`, i.e. attempts\n * `0 .. maxAttempts-1` are allowed and the `maxAttempts`-th is refused.\n */\nexport function shouldRetry(state: RetryState, policy: RetryPolicy): boolean {\n return state.attempt < policy.maxAttempts;\n}\n\n/**\n * The delay to wait before the next attempt, given the current state. PURE.\n *\n * Bridges `RetryState` → `backoffDelay` by using `state.attempt` as the 0-based\n * attempt index, so the delay grows with each recorded failure. `rng` is\n * injected for determinism, same contract as `backoffDelay`.\n */\nexport function nextDelayMs(\n state: RetryState,\n policy: RetryPolicy,\n rng: Rng = defaultRng,\n): number {\n return backoffDelay(state.attempt, policy, rng);\n}\n"]}
@@ -0,0 +1,268 @@
1
+ import { deadlineSub } from './chunk-K7Q23EQ4.js';
2
+
3
+ // src/raft/index.ts
4
+ function electionSubId(self) {
5
+ return `raft:election:${self}`;
6
+ }
7
+ function heartbeatSubId(self) {
8
+ return `raft:heartbeat:${self}`;
9
+ }
10
+ function toRaftMsg(self, fired) {
11
+ if (fired.id === electionSubId(self)) {
12
+ return { _tag: "election_timeout_fired", at: fired.atMs };
13
+ }
14
+ if (fired.id === heartbeatSubId(self)) {
15
+ return { _tag: "heartbeat_fired", at: fired.atMs };
16
+ }
17
+ return null;
18
+ }
19
+ function logIsUpToDate(log, candidateLastLogIndex, candidateLastLogTerm) {
20
+ const last = log.at(-1);
21
+ const ourLastTerm = last?.term ?? 0;
22
+ const ourLastIndex = last?.index ?? 0;
23
+ if (candidateLastLogTerm !== ourLastTerm) {
24
+ return candidateLastLogTerm > ourLastTerm;
25
+ }
26
+ return candidateLastLogIndex >= ourLastIndex;
27
+ }
28
+ function majority(peerCount) {
29
+ const clusterSize = peerCount + 1;
30
+ return Math.floor(clusterSize / 2) + 1;
31
+ }
32
+ function createRaftNode(config, rng = Math.random) {
33
+ const { self, peers, electionTimeout, heartbeatMs } = config;
34
+ const quorum = majority(peers.length);
35
+ function lastIndex(state) {
36
+ return state.log.at(-1)?.index ?? 0;
37
+ }
38
+ function lastTerm(state) {
39
+ return state.log.at(-1)?.term ?? 0;
40
+ }
41
+ function adoptHigherTerm(state, term) {
42
+ if (term <= state.currentTerm) return state;
43
+ return {
44
+ ...state,
45
+ currentTerm: term,
46
+ votedFor: null,
47
+ role: { _tag: "follower" }
48
+ };
49
+ }
50
+ function startElection(state) {
51
+ const newTerm = state.currentTerm + 1;
52
+ const lastLogIndex = lastIndex(state);
53
+ const lastLogTerm = lastTerm(state);
54
+ const next = {
55
+ ...state,
56
+ currentTerm: newTerm,
57
+ votedFor: self,
58
+ role: { _tag: "candidate", votesGranted: [self] }
59
+ };
60
+ const requests = peers.map((peer) => ({
61
+ type: "raft:send_request_vote",
62
+ to: peer,
63
+ term: newTerm,
64
+ candidateId: self,
65
+ lastLogIndex,
66
+ lastLogTerm
67
+ }));
68
+ if (next.role._tag === "candidate" && next.role.votesGranted.length >= quorum) {
69
+ return becomeLeader(next);
70
+ }
71
+ return [next, requests];
72
+ }
73
+ function becomeLeader(state) {
74
+ const nextIdx = lastIndex(state) + 1;
75
+ const nextIndex = {};
76
+ const matchIndex = {};
77
+ for (const peer of peers) {
78
+ nextIndex[peer] = nextIdx;
79
+ matchIndex[peer] = 0;
80
+ }
81
+ const leader = {
82
+ ...state,
83
+ role: { _tag: "leader", nextIndex, matchIndex }
84
+ };
85
+ return [leader, replicateAll(leader)];
86
+ }
87
+ function appendEntriesFor(state, peer, next) {
88
+ const prevLogIndex = next - 1;
89
+ const prevLogTerm = prevLogIndex > 0 ? state.log.at(prevLogIndex - 1)?.term ?? 0 : 0;
90
+ const entries = state.log.slice(prevLogIndex);
91
+ return {
92
+ type: "raft:send_append_entries",
93
+ to: peer,
94
+ term: state.currentTerm,
95
+ leaderId: self,
96
+ prevLogIndex,
97
+ prevLogTerm,
98
+ entries,
99
+ leaderCommit: state.commitIndex
100
+ };
101
+ }
102
+ function replicateAll(state) {
103
+ if (state.role._tag !== "leader") return [];
104
+ const { nextIndex } = state.role;
105
+ return peers.map(
106
+ (peer) => appendEntriesFor(state, peer, nextIndex[peer] ?? 1)
107
+ );
108
+ }
109
+ function advanceCommitIndex(state) {
110
+ if (state.role._tag !== "leader") return state.commitIndex;
111
+ const { matchIndex } = state.role;
112
+ const lastLogIndex = lastIndex(state);
113
+ let commit = state.commitIndex;
114
+ for (let n = state.commitIndex + 1; n <= lastLogIndex; n++) {
115
+ if (state.log.at(n - 1)?.term !== state.currentTerm) continue;
116
+ let replicas = 1;
117
+ for (const peer of peers) {
118
+ if ((matchIndex[peer] ?? 0) >= n) replicas++;
119
+ }
120
+ if (replicas >= quorum) commit = n;
121
+ }
122
+ return commit;
123
+ }
124
+ function init() {
125
+ return {
126
+ currentTerm: 0,
127
+ votedFor: null,
128
+ log: [],
129
+ commitIndex: 0,
130
+ role: { _tag: "follower" }
131
+ };
132
+ }
133
+ function onElectionTimeout(state, _at) {
134
+ if (state.role._tag === "leader") return [state, []];
135
+ return startElection(state);
136
+ }
137
+ function onRequestVote(state, req) {
138
+ const base = adoptHigherTerm(state, req.term);
139
+ if (req.term < base.currentTerm) {
140
+ return [base, [voteReply(req.candidateId, base.currentTerm, false)]];
141
+ }
142
+ const free = base.votedFor === null || base.votedFor === req.candidateId;
143
+ const upToDate = logIsUpToDate(base.log, req.lastLogIndex, req.lastLogTerm);
144
+ const grant = free && upToDate;
145
+ if (!grant) {
146
+ return [base, [voteReply(req.candidateId, base.currentTerm, false)]];
147
+ }
148
+ const granted = {
149
+ ...base,
150
+ votedFor: req.candidateId,
151
+ role: { _tag: "follower" }
152
+ };
153
+ return [granted, [voteReply(req.candidateId, base.currentTerm, true)]];
154
+ }
155
+ function onRequestVoteReply(state, reply) {
156
+ if (reply.term > state.currentTerm) {
157
+ return [adoptHigherTerm(state, reply.term), []];
158
+ }
159
+ if (reply.term < state.currentTerm) return [state, []];
160
+ if (state.role._tag !== "candidate") return [state, []];
161
+ if (!reply.voteGranted) return [state, []];
162
+ if (state.role.votesGranted.includes(reply.from)) return [state, []];
163
+ const votesGranted = [...state.role.votesGranted, reply.from];
164
+ const tallied = {
165
+ ...state,
166
+ role: { _tag: "candidate", votesGranted }
167
+ };
168
+ if (votesGranted.length >= quorum) return becomeLeader(tallied);
169
+ return [tallied, []];
170
+ }
171
+ function onAppendEntries(state, req) {
172
+ if (req.term < state.currentTerm) {
173
+ return [state, [appendReply(req.leaderId, state.currentTerm, false, 0)]];
174
+ }
175
+ const adopted = adoptHigherTerm(state, req.term);
176
+ const base = adopted.role._tag === "follower" ? adopted : { ...adopted, role: { _tag: "follower" } };
177
+ const prevOk = req.prevLogIndex === 0 || base.log[req.prevLogIndex - 1]?.term === req.prevLogTerm;
178
+ if (!prevOk) {
179
+ return [base, [appendReply(req.leaderId, base.currentTerm, false, 0)]];
180
+ }
181
+ const kept = base.log.slice(0, req.prevLogIndex);
182
+ const nextLog = [...kept, ...req.entries];
183
+ const lastNew = req.prevLogIndex + req.entries.length;
184
+ const commitIndex = req.leaderCommit > base.commitIndex ? Math.min(req.leaderCommit, lastNew) : base.commitIndex;
185
+ const next = { ...base, log: nextLog, commitIndex };
186
+ return [next, [appendReply(req.leaderId, base.currentTerm, true, lastNew)]];
187
+ }
188
+ function onAppendEntriesReply(state, reply) {
189
+ if (reply.term > state.currentTerm) {
190
+ return [adoptHigherTerm(state, reply.term), []];
191
+ }
192
+ if (state.role._tag !== "leader") return [state, []];
193
+ if (reply.term < state.currentTerm) return [state, []];
194
+ const { nextIndex, matchIndex } = state.role;
195
+ if (!reply.success) {
196
+ const backed = Math.max(1, (nextIndex[reply.from] ?? 1) - 1);
197
+ const retried = {
198
+ ...state,
199
+ role: {
200
+ _tag: "leader",
201
+ nextIndex: { ...nextIndex, [reply.from]: backed },
202
+ matchIndex
203
+ }
204
+ };
205
+ return [retried, [appendEntriesFor(retried, reply.from, backed)]];
206
+ }
207
+ const newMatch = Math.max(matchIndex[reply.from] ?? 0, reply.matchIndex);
208
+ const advanced = {
209
+ ...state,
210
+ role: {
211
+ _tag: "leader",
212
+ nextIndex: { ...nextIndex, [reply.from]: newMatch + 1 },
213
+ matchIndex: { ...matchIndex, [reply.from]: newMatch }
214
+ }
215
+ };
216
+ const commitIndex = advanceCommitIndex(advanced);
217
+ return [{ ...advanced, commitIndex }, []];
218
+ }
219
+ function onClientCommand(state, msg) {
220
+ if (state.role._tag !== "leader") return [state, []];
221
+ const entry = {
222
+ term: state.currentTerm,
223
+ index: lastIndex(state) + 1,
224
+ command: msg.command
225
+ };
226
+ const appended = { ...state, log: [...state.log, entry] };
227
+ return [appended, replicateAll(appended)];
228
+ }
229
+ function onHeartbeat(state, _at) {
230
+ if (state.role._tag !== "leader") return [state, []];
231
+ return [state, replicateAll(state)];
232
+ }
233
+ function subs(state, now) {
234
+ if (state.role._tag === "leader") {
235
+ return [deadlineSub(heartbeatSubId(self), now + heartbeatMs)];
236
+ }
237
+ const span = electionTimeout.maxMs - electionTimeout.minMs;
238
+ const timeout = electionTimeout.minMs + Math.floor(rng() * (span + 1));
239
+ return [deadlineSub(electionSubId(self), now + timeout)];
240
+ }
241
+ function voteReply(to, term, voteGranted) {
242
+ return { type: "raft:send_request_vote_reply", to, term, voteGranted };
243
+ }
244
+ function appendReply(to, term, success, matchIndex) {
245
+ return {
246
+ type: "raft:send_append_entries_reply",
247
+ to,
248
+ term,
249
+ success,
250
+ matchIndex
251
+ };
252
+ }
253
+ return {
254
+ init,
255
+ onElectionTimeout,
256
+ onRequestVote,
257
+ onRequestVoteReply,
258
+ onAppendEntries,
259
+ onAppendEntriesReply,
260
+ onClientCommand,
261
+ onHeartbeat,
262
+ subs
263
+ };
264
+ }
265
+
266
+ export { createRaftNode, electionSubId, heartbeatSubId, logIsUpToDate, toRaftMsg };
267
+ //# sourceMappingURL=chunk-6RGBKODV.js.map
268
+ //# sourceMappingURL=chunk-6RGBKODV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/raft/index.ts"],"names":[],"mappings":"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","file":"chunk-6RGBKODV.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"]}
@@ -58,5 +58,5 @@ function arbGuidedSequence(_machine, _ctx, _cells, _opts) {
58
58
  }
59
59
 
60
60
  export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy };
61
- //# sourceMappingURL=chunk-ZRUST6BK.js.map
62
- //# sourceMappingURL=chunk-ZRUST6BK.js.map
61
+ //# sourceMappingURL=chunk-7FYQGDMF.js.map
62
+ //# sourceMappingURL=chunk-7FYQGDMF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pbt/arbitraries/ctx.ts","../src/pbt/arbitraries/msg.ts","../src/pbt/arbitraries/sequence.ts"],"names":["fc2"],"mappings":";;;AAyCO,SAAS,oBAAA,GAAiC;AAC/C,EAAA,MAAM,OAAA,GAAgC;AAAA,IACpC,GAAA,CAAI,SAAS,IAAA,EAAgC;AAK3C,MAAA,IAAI,OAAO,IAAA,KAAS,QAAA,EAAU,OAAO,MAAA;AAKrC,MAAA,IACE,SAAS,QAAA,IACT,IAAA,KAAS,cACT,IAAA,KAAS,SAAA,IACT,SAAS,MAAA,EACT;AACA,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,oFACgB,IAAI,CAAA,wEAAA;AAAA,OAEtB;AAAA,IACF,CAAA;AAAA,IACA,GAAA,GAAe;AACb,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AAAA,IACA,OAAA,GAAsC;AACpC,MAAA,OAAO,EAAC;AAAA,IACV;AAAA,GACF;AACA,EAAA,OAAO,IAAI,KAAA,CAAM,EAAC,EAAG,OAAO,CAAA;AAC9B;ACfO,SAAS,MAAA,CACd,OACA,IAAA,EACiB;AACjB,EAAA,MAAM,UAAU,IAAA,EAAM,OAAA;AACtB,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA;AACpC,EAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,EAC1D;AACA,EAAA,MAAM,WAAW,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,GAAG,CAAA,MAAO;AAAA,IAC5C,SAAA,EAAW,GAAA;AAAA,IACX,MAAA,EAAQ,OAAA,GAAU,GAAG,CAAA,IAAK;AAAA,GAC5B,CAAE,CAAA;AACF,EAAA,OAAU,EAAA,CAAA,KAAA,CAAM,GAAG,QAAQ,CAAA;AAC7B;AASO,SAAS,eACd,IAAA,EAC2B;AAC3B,EAAA,OAAU,EAAA,CAAA,QAAA,CAAS,EAAE,IAAA,EAAM,CAAA;AAC7B;AAWO,SAAS,YAAA,CACd,MACA,MAAA,EAC+B;AAa/B,EAAA,MAAM,iBAAA,GAA2D;AAAA,IAC/D,IAAA,EAAS,YAAS,IAAI,CAAA;AAAA,IACtB,GAAI;AAAA,GACN;AACA,EAAA,OAAU,UAAO,iBAAiB,CAAA;AAGpC;ACvFO,SAAS,cAAA,CACd,QACA,IAAA,EAK4B;AAC5B,EAAA,MAAM,SAAA,GAAY,MAAM,SAAA,IAAa,CAAA;AACrC,EAAA,MAAM,SAAA,GAAY,MAAM,SAAA,IAAa,EAAA;AACrC,EAAA,MAAM,MAAA,GAAS,IAAA,EAAM,MAAA,IAAU,EAAC;AAChC,EAAA,MAAM,OAAUA,EAAA,CAAA,KAAA,CAAM,MAAA,EAAQ,EAAE,SAAA,EAAW,WAAW,CAAA;AACtD,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAChC,EAAA,OAAO,IAAA,CAAK,IAAI,CAAC,GAAA,KAAQ,CAAC,GAAG,MAAA,EAAQ,GAAG,GAAG,CAAC,CAAA;AAC9C;AAWO,SAAS,iBAAA,CAOd,QAAA,EACA,IAAA,EACA,MAAA,EAKA,KAAA,EAC4B;AAC5B,EAAA,MAAM,IAAI,KAAA;AAAA,IACR;AAAA,GAIF;AACF","file":"chunk-7FYQGDMF.js","sourcesContent":["// ---------------------------------------------------------------------------\n// `stubCtxThrowingProxy<Ctx>()` — a Proxy that satisfies the Ctx type-shape\n// at compile time but throws on every property access at runtime.\n//\n// Why this exists: PBT runs the pure reducer surface ONLY — `replay` never\n// invokes `interpret` and never starts a Sub, so Ctx should never be\n// dereferenced during property generation. But the substrate's `replay`\n// signature requires a `ctx: Ctx` value at compile time (it forwards ctx\n// into `init`, which on some machines IS pure but on others reads ctx). A\n// hand-rolled `{} as Ctx` would silently pass through any accidental ctx\n// access — exactly the silent footgun this proxy catches.\n//\n// Open question 2 (design doc): we ship this as the default. Tests that\n// genuinely need a working ctx (because their `init` reads from it) can\n// pass a real ctx value instead — the proxy is opt-in, not load-bearing.\n//\n// Strengthens invariant 6 (no silent failures — accidental ctx reads\n// produce a useful error, not a `undefined.foo` confusion later).\n// ---------------------------------------------------------------------------\n\n/**\n * Build a Ctx value whose every property access throws. Use as the `ctx`\n * argument to PBT runners when the reducer under test is pure (does not\n * read from ctx). Accidental ctx access during property generation\n * surfaces as a labelled error instead of a silent `undefined` or\n * downstream type-error.\n *\n * Returns a typed `Ctx` (via `as unknown as Ctx`) — the proxy satisfies the\n * type-shape requirement at the call site; the runtime contract is\n * \"throw on first read.\" The `as unknown` cast is the one boundary cast\n * this helper requires; the proxy is structurally a `Ctx` for every\n * property accessor.\n *\n * @example\n * import { stubCtxThrowingProxy } from \"../index\";\n * const ctx = stubCtxThrowingProxy<MyCtx>();\n * propertyTerminates(machine, ctx, ...);\n * // If anything reads ctx.someField during the run, an Error throws:\n * // \"@demlik/tea/pbt: ctx access denied (PBT operates on the pure\n * // reducer; ctx field 'someField' was read)\"\n */\nexport function stubCtxThrowingProxy<Ctx>(): Ctx {\n const handler: ProxyHandler<object> = {\n get(_target, prop: string | symbol): unknown {\n // Allow vitest / fast-check internals to introspect via Symbol-keyed\n // checks (e.g. Symbol.toStringTag, Symbol.iterator). Returning\n // undefined on symbol keys keeps property-tree walks cheap; the\n // throw fires on real domain field reads.\n if (typeof prop === \"symbol\") return undefined;\n // Some shrinkers / pretty-printers in fast-check probe a small set\n // of conventional field names (`toJSON`, `toString`, `valueOf`,\n // `then` — the last for thenable-check). Return undefined for those\n // so the proxy doesn't fail a probe before the property body runs.\n if (\n prop === \"toJSON\" ||\n prop === \"toString\" ||\n prop === \"valueOf\" ||\n prop === \"then\"\n ) {\n return undefined;\n }\n throw new Error(\n `@demlik/tea/pbt: ctx access denied (PBT operates on the pure reducer; ` +\n `ctx field '${prop}' was read). Pass a real ctx value if the reducer's ` +\n `init reads from ctx.`,\n );\n },\n has(): boolean {\n return false;\n },\n ownKeys(): ArrayLike<string | symbol> {\n return [];\n },\n };\n return new Proxy({}, handler) as unknown as Ctx;\n}\n","// ---------------------------------------------------------------------------\n// Msg arbitrary builders.\n//\n// `MsgArbitraryTable<M>` is a strict mapped type — every Msg variant must\n// have an arbitrary entry, keyed by `M[\"type\"]`, narrowing the arbitrary's\n// `T` to `Extract<M, { type: K }>`. This mirrors the load-bearing discipline\n// of `Reducer<S, M, C>` and `Transitions<S, M, C>` in the substrate: adding\n// a Msg variant without an arbitrary fails to compile, not at runtime.\n//\n// `arbMsg(table, opts?)` collapses the table into a single `fc.Arbitrary<M>`\n// via `fc.oneof`. Optional per-variant weighting for sequence-skew tests.\n//\n// `arbConstantMsg(type)` / `arbRecordMsg(type, fields)` are sugar over\n// `fc.constant({ type })` / `fc.record({ type: fc.constant(type), ...fields })`.\n// They exist because every Msg variant arbitrary either (a) carries no\n// payload — `arbConstantMsg(\"stop\")` — or (b) carries a record of fields —\n// `arbRecordMsg(\"tick\", { now: fc.integer() })`. Naming the two shapes\n// prevents callers from hand-rolling `fc.record({ type: fc.constant(\"...\") })`\n// inconsistently across test files.\n//\n// Open question 1 from the design doc (strict vs Partial table): strict by\n// default. The mapped type catches drift at compile time; `arbMsgPartial`\n// sugar is a future addition iff friction becomes real.\n// ---------------------------------------------------------------------------\n\nimport * as fc from \"fast-check\";\n\n/**\n * Strict per-variant Msg arbitrary table. Mapped type: every key in\n * `M[\"type\"]` MUST have an arbitrary entry whose `T` is the narrowed\n * variant. Adding a new Msg variant without an entry is a compile error\n * (same load-bearing discipline as `Reducer<S, M, C>` in the substrate).\n *\n * @example\n * type Msg = { type: \"start\" } | { type: \"tick\"; now: number };\n * const table: MsgArbitraryTable<Msg> = {\n * start: arbConstantMsg(\"start\"),\n * tick: arbRecordMsg(\"tick\", { now: fc.integer({ min: 0 }) }),\n * };\n */\nexport type MsgArbitraryTable<M extends { type: string }> = {\n [K in M[\"type\"]]: fc.Arbitrary<Extract<M, { type: K }>>;\n};\n\n/**\n * Build a single `fc.Arbitrary<M>` from a `MsgArbitraryTable<M>`. Picks one\n * variant per generation via `fc.oneof`; optional per-variant `weights`\n * bias the distribution (default uniform).\n *\n * @param table Per-variant arbitrary table (strict — every variant required).\n * @param opts Optional bias: `weights` is a `Partial<Record<M[\"type\"], number>>`.\n * Variants without an explicit weight default to 1.\n *\n * @example\n * const msgArb = arbMsg<Msg>({\n * start: arbConstantMsg(\"start\"),\n * tick: arbRecordMsg(\"tick\", { now: fc.integer() }),\n * stop: arbConstantMsg(\"stop\"),\n * }, { weights: { tick: 5, start: 1, stop: 1 } });\n */\nexport function arbMsg<M extends { type: string }>(\n table: MsgArbitraryTable<M>,\n opts?: { weights?: Partial<Record<M[\"type\"], number>> },\n): fc.Arbitrary<M> {\n const weights = opts?.weights;\n const entries = Object.entries(table) as [M[\"type\"], fc.Arbitrary<M>][];\n if (entries.length === 0) {\n throw new Error(\"@demlik/tea/pbt: arbMsg table is empty\");\n }\n const weighted = entries.map(([key, arb]) => ({\n arbitrary: arb,\n weight: weights?.[key] ?? 1,\n }));\n return fc.oneof(...weighted);\n}\n\n/**\n * Build a payload-free Msg variant arbitrary. Sugar for\n * `fc.constant({ type })` that narrows the result to `{ type: T }` literal.\n *\n * @example\n * arbConstantMsg(\"stop\") // fc.Arbitrary<{ type: \"stop\" }>\n */\nexport function arbConstantMsg<T extends string>(\n type: T,\n): fc.Arbitrary<{ type: T }> {\n return fc.constant({ type });\n}\n\n/**\n * Build a record-shaped Msg variant arbitrary. Sugar for\n * `fc.record({ type: fc.constant(type), ...fields })` that stamps the\n * discriminator literal and combines with the per-field arbitraries.\n *\n * @example\n * arbRecordMsg(\"tick\", { now: fc.integer({ min: 0 }) })\n * // fc.Arbitrary<{ type: \"tick\"; now: number }>\n */\nexport function arbRecordMsg<T extends string, R>(\n type: T,\n fields: { [K in keyof R]: fc.Arbitrary<R[K]> },\n): fc.Arbitrary<{ type: T } & R> {\n // `fc.record` builds an arbitrary of the merged-shape record. The\n // `type: fc.constant(type)` field stamps the literal discriminator;\n // the spread of `fields` provides every payload field's arbitrary.\n //\n // The intermediate `as Record<string, fc.Arbitrary<unknown>>` is the\n // single load-bearing cast — fc.record's parameter type is invariant in\n // its inferred field shape, and the merged literal-and-generic spread\n // doesn't unify with the strict `{ [K in keyof R]: Arbitrary<R[K]> }`\n // shape TS computes for us. The outer cast to `fc.Arbitrary<{ type: T }\n // & R>` restates the result shape we know to be correct at this\n // boundary. Mirrors the same discipline `fc.record` callers use\n // throughout the prior-art branch.\n const recordArbitraries: Record<string, fc.Arbitrary<unknown>> = {\n type: fc.constant(type),\n ...(fields as Record<string, fc.Arbitrary<unknown>>),\n };\n return fc.record(recordArbitraries) as unknown as fc.Arbitrary<\n { type: T } & R\n >;\n}\n","// ---------------------------------------------------------------------------\n// Msg-sequence arbitraries.\n//\n// `arbMsgSequence(msgArb, opts)` produces `fc.Arbitrary<readonly M[]>` —\n// fast-check's default-shrinker handles the array; the per-element shrinker\n// inside `msgArb` shrinks each variant. Optional `prefix` is prepended\n// verbatim (used by Test A: \"the bug-triggering Msg leads the sequence\").\n//\n// `arbGuidedSequence` is the state-aware sequence builder. In v1 it throws\n// — Phase 6 of the design doc lands the `fc.commands`-backed implementation.\n// The export exists today so the surface is forward-compatible: consumers\n// that need state-aware generation get a useful error pointing at the\n// design doc instead of \"is not a function.\"\n// ---------------------------------------------------------------------------\n\nimport * as fc from \"fast-check\";\nimport type { Cmd, Machine, Sub } from \"../../index\";\n\n/**\n * Build an arbitrary of Msg sequences over a per-Msg arbitrary. Optionally\n * bounded in length and / or prefixed with a fixed sequence.\n *\n * - `minLength` (default 0) and `maxLength` (default 30) bound the random\n * array length. `prefix` is prepended AFTER the random tail is built; the\n * final length is `prefix.length + tail.length`.\n * - `prefix` is the load-bearing escape hatch for \"the first msg is the\n * one that exercises the cell I care about\" (e.g. Test A's\n * `[{ type: \"ws:client:hello\" }]` prefix to land in the\n * `auditing.ws:client:hello` cell on the first transition).\n *\n * @example\n * arbMsgSequence(msgArb, { minLength: 1, maxLength: 30 });\n * arbMsgSequence(msgArb, { prefix: [{ type: \"ws:client:hello\" }], maxLength: 20 });\n */\nexport function arbMsgSequence<M extends { type: string }>(\n msgArb: fc.Arbitrary<M>,\n opts?: {\n readonly minLength?: number;\n readonly maxLength?: number;\n readonly prefix?: readonly NoInfer<M>[];\n },\n): fc.Arbitrary<readonly M[]> {\n const minLength = opts?.minLength ?? 0;\n const maxLength = opts?.maxLength ?? 30;\n const prefix = opts?.prefix ?? [];\n const tail = fc.array(msgArb, { minLength, maxLength });\n if (prefix.length === 0) return tail;\n return tail.map((arr) => [...prefix, ...arr]);\n}\n\n/**\n * State-aware Msg sequence arbitrary — generates only msgs whose\n * precondition holds in the current state. Backed by `fc.commands`\n * internally (Phase 6 of `.patterns/tea/tea-pbt-design.md`).\n *\n * **NOT IMPLEMENTED IN v1.** The export exists today so the surface is\n * forward-compatible; the design doc §7 ships this in Phase 6 alongside\n * shrinkers. Calling it throws.\n */\nexport function arbGuidedSequence<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n _machine: Machine<S, M, C, U, Ctx>,\n _ctx: Ctx,\n _cells: {\n [K in M[\"type\"]]?: (\n state: S,\n ) => fc.Arbitrary<Extract<M, { type: K }>> | null;\n },\n _opts?: { maxLength?: number },\n): fc.Arbitrary<readonly M[]> {\n throw new Error(\n \"@demlik/tea/pbt: arbGuidedSequence is not implemented in v1 — see \" +\n \".patterns/tea/tea-pbt-design.md §7 (Phase 6). Use arbMsgSequence with \" +\n \"an unguided arbMsg table for now; states that don't accept a given \" +\n \"msg should noop in the reducer (mapped-type discipline).\",\n );\n}\n"]}