@demlik/tea 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/README.md +11 -9
  2. package/dist/agent/index.d.ts +638 -45
  3. package/dist/agent/index.js +9 -19
  4. package/dist/{chunk-RH5EFRVJ.js → chunk-22POWGFA.js} +31 -5
  5. package/dist/chunk-22POWGFA.js.map +1 -0
  6. package/dist/{chunk-DMYGQB4O.js → chunk-3B5X7SBB.js} +23 -6
  7. package/dist/chunk-3B5X7SBB.js.map +1 -0
  8. package/dist/chunk-66PY5WYF.js +1816 -0
  9. package/dist/chunk-66PY5WYF.js.map +1 -0
  10. package/dist/{chunk-25LBDCBC.js → chunk-6IQPODUF.js} +2 -2
  11. package/dist/{chunk-25LBDCBC.js.map → chunk-6IQPODUF.js.map} +1 -1
  12. package/dist/chunk-AYS5N6WY.js +505 -0
  13. package/dist/chunk-AYS5N6WY.js.map +1 -0
  14. package/dist/{chunk-EWJP3YYP.js → chunk-HWVW4AXC.js} +3 -3
  15. package/dist/chunk-HWVW4AXC.js.map +1 -0
  16. package/dist/{chunk-O4MVQTEJ.js → chunk-L3TND7LA.js} +20 -16
  17. package/dist/chunk-L3TND7LA.js.map +1 -0
  18. package/dist/{chunk-J6527VRQ.js → chunk-LFZUK6J2.js} +3 -3
  19. package/dist/chunk-LFZUK6J2.js.map +1 -0
  20. package/dist/{chunk-D736QUGF.js → chunk-MYXFNWAJ.js} +4 -5
  21. package/dist/chunk-MYXFNWAJ.js.map +1 -0
  22. package/dist/{chunk-K575PT4S.js → chunk-PR6TB3BL.js} +15 -4
  23. package/dist/chunk-PR6TB3BL.js.map +1 -0
  24. package/dist/{chunk-YUXP55T2.js → chunk-Q5CQTBFA.js} +57 -7
  25. package/dist/chunk-Q5CQTBFA.js.map +1 -0
  26. package/dist/{batch-window/index.js → chunk-QPEVCJ5Q.js} +4 -8
  27. package/dist/chunk-QPEVCJ5Q.js.map +1 -0
  28. package/dist/chunk-TCLBCERH.js +1480 -0
  29. package/dist/chunk-TCLBCERH.js.map +1 -0
  30. package/dist/{chunk-FV3Z5MBE.js → chunk-TXN2KFHF.js} +14 -4
  31. package/dist/chunk-TXN2KFHF.js.map +1 -0
  32. package/dist/{chunk-MUUROGIH.js → chunk-V46HSVCK.js} +62 -12
  33. package/dist/chunk-V46HSVCK.js.map +1 -0
  34. package/dist/{chunk-Q2UM7BXU.js → chunk-VBDU4JL6.js} +2 -2
  35. package/dist/chunk-VBDU4JL6.js.map +1 -0
  36. package/dist/{chunk-ZVJZ72HY.js → chunk-VTQI2NHP.js} +25 -11
  37. package/dist/chunk-VTQI2NHP.js.map +1 -0
  38. package/dist/{core-Co0LWkIA.d.ts → core-DnzqWy6u.d.ts} +72 -23
  39. package/dist/devtools/index.d.ts +2 -2
  40. package/dist/devtools/index.js +207 -2
  41. package/dist/devtools/index.js.map +1 -1
  42. package/dist/do/index.d.ts +43 -29
  43. package/dist/do/index.js +36 -25
  44. package/dist/do/index.js.map +1 -1
  45. package/dist/{durable-effects-C6uwzjb5.d.ts → durable-effects-CWzA-o2B.d.ts} +2 -2
  46. package/dist/extension/index.d.ts +300 -3
  47. package/dist/extension/index.js +596 -5
  48. package/dist/extension/index.js.map +1 -1
  49. package/dist/flow/index.d.ts +1284 -0
  50. package/dist/flow/index.js +732 -0
  51. package/dist/flow/index.js.map +1 -0
  52. package/dist/idempotency/index.d.ts +90 -116
  53. package/dist/idempotency/index.js +4 -1
  54. package/dist/index-Bc40JWVl.d.ts +131 -0
  55. package/dist/{batch-window/index.d.ts → index-C2DwkjlP.d.ts} +7 -12
  56. package/dist/index-CA_7I5g9.d.ts +806 -0
  57. package/dist/index-CSyMYDAB.d.ts +69 -0
  58. package/dist/{fan-out/index.d.ts → index-CuNO9m8i.d.ts} +7 -9
  59. package/dist/{debounce/index.d.ts → index-DHSUmQTR.d.ts} +7 -7
  60. package/dist/{recorder/index.d.ts → index-DrIRb_Ch.d.ts} +4 -7
  61. package/dist/index-HSYgG2Yt.d.ts +380 -0
  62. package/dist/{monitored-run/index.d.ts → index-KXqTRJyb.d.ts} +37 -13
  63. package/dist/{snapshot/index.d.ts → index-ZCMhnVKN.d.ts} +15 -78
  64. package/dist/index.d.ts +771 -6
  65. package/dist/index.js +1 -2
  66. package/dist/jev/index.d.ts +646 -0
  67. package/dist/jev/index.js +485 -0
  68. package/dist/jev/index.js.map +1 -0
  69. package/dist/machine-DGlPWCWI.d.ts +1781 -0
  70. package/dist/machine-viz/index.d.ts +1 -1
  71. package/dist/machine-viz/index.js +12 -5
  72. package/dist/machine-viz/index.js.map +1 -1
  73. package/dist/mem/index.d.ts +18 -3
  74. package/dist/mem/index.js +22 -2
  75. package/dist/mem/index.js.map +1 -1
  76. package/dist/node/index.d.ts +104 -6
  77. package/dist/node/index.js +132 -9
  78. package/dist/node/index.js.map +1 -1
  79. package/dist/{paginator → paginate}/index.d.ts +140 -6
  80. package/dist/paginate/index.js +7 -0
  81. package/dist/parity/index.d.ts +4 -10
  82. package/dist/parity/index.js +4 -7
  83. package/dist/parity/index.js.map +1 -1
  84. package/dist/pbt/index.d.ts +247 -5
  85. package/dist/pbt/index.js +124 -5
  86. package/dist/pbt/index.js.map +1 -1
  87. package/dist/{trace-replay → persistence}/index.d.ts +7 -5
  88. package/dist/persistence/index.js +6 -0
  89. package/dist/{protocol-B5YmJr0d.d.ts → protocol-D8VbH-VH.d.ts} +10 -1
  90. package/dist/react/index.d.ts +2 -2
  91. package/dist/react/index.js +1 -2
  92. package/dist/react/index.js.map +1 -1
  93. package/dist/resilience/index.d.ts +744 -0
  94. package/dist/resilience/index.js +657 -0
  95. package/dist/resilience/index.js.map +1 -0
  96. package/dist/retry-backoff/index.d.ts +3 -2
  97. package/dist/retry-backoff/index.js +1 -1
  98. package/dist/runtime-types-CMF13BBh.d.ts +998 -0
  99. package/dist/testing/index.d.ts +147 -3
  100. package/dist/testing/index.js +95 -3
  101. package/dist/testing/index.js.map +1 -1
  102. package/dist/{throttled-input → timing}/index.d.ts +143 -12
  103. package/dist/{throttled-input → timing}/index.js +57 -12
  104. package/dist/timing/index.js.map +1 -0
  105. package/dist/{types-XS_miRnB.d.ts → types-BseNPIhr.d.ts} +1 -1
  106. package/dist/work-queue/index.d.ts +130 -54
  107. package/dist/work-queue/index.js +3 -2
  108. package/dist/work-queue/index.js.map +1 -1
  109. package/package.json +45 -218
  110. package/dist/authed-call/index.d.ts +0 -120
  111. package/dist/authed-call/index.js +0 -134
  112. package/dist/authed-call/index.js.map +0 -1
  113. package/dist/await-terminal/index.d.ts +0 -101
  114. package/dist/await-terminal/index.js +0 -76
  115. package/dist/await-terminal/index.js.map +0 -1
  116. package/dist/batch-window/index.js.map +0 -1
  117. package/dist/cache/index.d.ts +0 -152
  118. package/dist/cache/index.js +0 -8
  119. package/dist/chart/index.d.ts +0 -218
  120. package/dist/chart/index.js +0 -16
  121. package/dist/chart/index.js.map +0 -1
  122. package/dist/chart/inspect/index.d.ts +0 -86
  123. package/dist/chart/inspect/index.js +0 -5
  124. package/dist/chart/inspect/react.d.ts +0 -113
  125. package/dist/chart/inspect/react.js +0 -11
  126. package/dist/chart/inspect/react.js.map +0 -1
  127. package/dist/chart/inspect/styles.css +0 -286
  128. package/dist/chart/lane/index.d.ts +0 -362
  129. package/dist/chart/lane/index.js +0 -9
  130. package/dist/chart/lane/index.js.map +0 -1
  131. package/dist/chart/lane/react.d.ts +0 -84
  132. package/dist/chart/lane/react.js +0 -559
  133. package/dist/chart/lane/react.js.map +0 -1
  134. package/dist/chart/lane/server.d.ts +0 -148
  135. package/dist/chart/lane/server.js +0 -209
  136. package/dist/chart/lane/server.js.map +0 -1
  137. package/dist/chart/lane/styles.css +0 -498
  138. package/dist/chart/lane/viewer/assets/abnfDiagram-N423BO3Z-C729LI31.js +0 -1
  139. package/dist/chart/lane/viewer/assets/architectureDiagram-T3A2C74G-C729LI31.js +0 -1
  140. package/dist/chart/lane/viewer/assets/blockDiagram-VBNYF7ZC-C729LI31.js +0 -1
  141. package/dist/chart/lane/viewer/assets/c4Diagram-5PPSVZJV-C729LI31.js +0 -1
  142. package/dist/chart/lane/viewer/assets/chunk-2GRJ4B5K-HdDf8SI3.js +0 -1
  143. package/dist/chart/lane/viewer/assets/chunk-5RXB4S5H-CcNtpUd7.js +0 -231
  144. package/dist/chart/lane/viewer/assets/chunk-5VM5RSS4-BP4fRS_5.js +0 -15
  145. package/dist/chart/lane/viewer/assets/chunk-KBJHAD2P-DZOw7xU6.js +0 -1
  146. package/dist/chart/lane/viewer/assets/chunk-NSK5VX7P-CBCyTZMs.js +0 -128
  147. package/dist/chart/lane/viewer/assets/classDiagram-JCYQIIEL-C729LI31.js +0 -1
  148. package/dist/chart/lane/viewer/assets/classDiagram-v2-OCEON4UE-D4uAiFr8.js +0 -206
  149. package/dist/chart/lane/viewer/assets/cose-bilkent-JH36ORCC-DOwCrGXH.js +0 -1
  150. package/dist/chart/lane/viewer/assets/cynefinDiagram-MW4NZA55-C729LI31.js +0 -1
  151. package/dist/chart/lane/viewer/assets/dagre-K_PewWfS.js +0 -1
  152. package/dist/chart/lane/viewer/assets/dagre-VZM6K2ZE-831qZcA8.js +0 -4
  153. package/dist/chart/lane/viewer/assets/diagram-7IWD3JNH-C729LI31.js +0 -1
  154. package/dist/chart/lane/viewer/assets/diagram-B4RE2ZJO-C729LI31.js +0 -1
  155. package/dist/chart/lane/viewer/assets/diagram-LBJQPF4R-C729LI31.js +0 -1
  156. package/dist/chart/lane/viewer/assets/diagram-Q27KOJAE-C729LI31.js +0 -1
  157. package/dist/chart/lane/viewer/assets/diagram-UB23O5K3-C729LI31.js +0 -1
  158. package/dist/chart/lane/viewer/assets/ebnfDiagram-BXEA7PRR-C729LI31.js +0 -1
  159. package/dist/chart/lane/viewer/assets/erDiagram-JOGREHBK-C729LI31.js +0 -1
  160. package/dist/chart/lane/viewer/assets/flowDiagram-UKHOOZJN-C729LI31.js +0 -1
  161. package/dist/chart/lane/viewer/assets/ganttDiagram-PKOTCBZU-C729LI31.js +0 -1
  162. package/dist/chart/lane/viewer/assets/gitGraphDiagram-DS77QQ5N-C729LI31.js +0 -1
  163. package/dist/chart/lane/viewer/assets/index-9SkH5NA3.css +0 -1
  164. package/dist/chart/lane/viewer/assets/index-Bgt4sXpW.js +0 -195
  165. package/dist/chart/lane/viewer/assets/infoDiagram-6WML65LV-C729LI31.js +0 -1
  166. package/dist/chart/lane/viewer/assets/ishikawaDiagram-WSZJBQD7-C729LI31.js +0 -1
  167. package/dist/chart/lane/viewer/assets/journeyDiagram-NVQOT4AX-C729LI31.js +0 -1
  168. package/dist/chart/lane/viewer/assets/kanban-definition-27J2QSJJ-G7PaPzy-.js +0 -89
  169. package/dist/chart/lane/viewer/assets/katex-DOwCrGXH.js +0 -1
  170. package/dist/chart/lane/viewer/assets/mindmap-definition-FAOFIHXS-CGj9nrVk.js +0 -96
  171. package/dist/chart/lane/viewer/assets/path-fybaL0A-.js +0 -1
  172. package/dist/chart/lane/viewer/assets/pegDiagram-VL7TDLO6-C729LI31.js +0 -1
  173. package/dist/chart/lane/viewer/assets/pieDiagram-7S7Q4E2Y-C729LI31.js +0 -1
  174. package/dist/chart/lane/viewer/assets/quadrantDiagram-CIZ2JOQS-C729LI31.js +0 -1
  175. package/dist/chart/lane/viewer/assets/railroadDiagram-AXF67PYL-C729LI31.js +0 -1
  176. package/dist/chart/lane/viewer/assets/requirementDiagram-LRYGKXZP-C729LI31.js +0 -1
  177. package/dist/chart/lane/viewer/assets/sankeyDiagram-W5VNT64P-C729LI31.js +0 -1
  178. package/dist/chart/lane/viewer/assets/sequenceDiagram-SI44F4Z6-C729LI31.js +0 -1
  179. package/dist/chart/lane/viewer/assets/stateDiagram-OKZ733FA-B_88AzBS.js +0 -1
  180. package/dist/chart/lane/viewer/assets/stateDiagram-v2-UEYNNEHI-CR5gcgq6.js +0 -1
  181. package/dist/chart/lane/viewer/assets/swimlanes-SLNWSIFB-B4xK-rwg.js +0 -1
  182. package/dist/chart/lane/viewer/assets/swimlanesDiagram-ULZ7WXOC-C729LI31.js +0 -1
  183. package/dist/chart/lane/viewer/assets/timeline-definition-Z64GVDOM-CCv52MfY.js +0 -120
  184. package/dist/chart/lane/viewer/assets/vennDiagram-T6HMQDX7-C729LI31.js +0 -1
  185. package/dist/chart/lane/viewer/assets/wardleyDiagram-T6FBY63Y-C729LI31.js +0 -1
  186. package/dist/chart/lane/viewer/assets/xychartDiagram-ELKLHX3M-C729LI31.js +0 -1
  187. package/dist/chart/lane/viewer/index.html +0 -15
  188. package/dist/chart/report/index.d.ts +0 -117
  189. package/dist/chart/report/index.js +0 -32
  190. package/dist/chart/report/index.js.map +0 -1
  191. package/dist/chunk-2YFLOCWX.js +0 -15
  192. package/dist/chunk-2YFLOCWX.js.map +0 -1
  193. package/dist/chunk-3I52QZZ5.js +0 -519
  194. package/dist/chunk-3I52QZZ5.js.map +0 -1
  195. package/dist/chunk-3ZAFFRRB.js +0 -457
  196. package/dist/chunk-3ZAFFRRB.js.map +0 -1
  197. package/dist/chunk-4EQZ76JC.js +0 -33
  198. package/dist/chunk-4EQZ76JC.js.map +0 -1
  199. package/dist/chunk-4KLG43GY.js +0 -223
  200. package/dist/chunk-4KLG43GY.js.map +0 -1
  201. package/dist/chunk-7FRZ2CT2.js +0 -424
  202. package/dist/chunk-7FRZ2CT2.js.map +0 -1
  203. package/dist/chunk-AZZLE7XR.js +0 -62
  204. package/dist/chunk-AZZLE7XR.js.map +0 -1
  205. package/dist/chunk-BYQUGDD3.js +0 -16
  206. package/dist/chunk-BYQUGDD3.js.map +0 -1
  207. package/dist/chunk-CIQG5L42.js +0 -685
  208. package/dist/chunk-CIQG5L42.js.map +0 -1
  209. package/dist/chunk-CLM6ERNO.js +0 -61
  210. package/dist/chunk-CLM6ERNO.js.map +0 -1
  211. package/dist/chunk-COS5MSTO.js +0 -88
  212. package/dist/chunk-COS5MSTO.js.map +0 -1
  213. package/dist/chunk-CRPZJVD2.js +0 -71
  214. package/dist/chunk-CRPZJVD2.js.map +0 -1
  215. package/dist/chunk-D62FDQKE.js +0 -53
  216. package/dist/chunk-D62FDQKE.js.map +0 -1
  217. package/dist/chunk-D736QUGF.js.map +0 -1
  218. package/dist/chunk-DGE4PV4A.js +0 -26
  219. package/dist/chunk-DGE4PV4A.js.map +0 -1
  220. package/dist/chunk-DMYGQB4O.js.map +0 -1
  221. package/dist/chunk-EWJP3YYP.js.map +0 -1
  222. package/dist/chunk-FV3Z5MBE.js.map +0 -1
  223. package/dist/chunk-HCEVHFJ7.js +0 -54
  224. package/dist/chunk-HCEVHFJ7.js.map +0 -1
  225. package/dist/chunk-J6527VRQ.js.map +0 -1
  226. package/dist/chunk-JELSZN25.js +0 -47
  227. package/dist/chunk-JELSZN25.js.map +0 -1
  228. package/dist/chunk-K575PT4S.js.map +0 -1
  229. package/dist/chunk-L57ZOT5K.js +0 -184
  230. package/dist/chunk-L57ZOT5K.js.map +0 -1
  231. package/dist/chunk-MU6MCIAC.js +0 -15
  232. package/dist/chunk-MU6MCIAC.js.map +0 -1
  233. package/dist/chunk-MUUROGIH.js.map +0 -1
  234. package/dist/chunk-NR7BU22N.js +0 -758
  235. package/dist/chunk-NR7BU22N.js.map +0 -1
  236. package/dist/chunk-O4MVQTEJ.js.map +0 -1
  237. package/dist/chunk-OOYD44JI.js +0 -317
  238. package/dist/chunk-OOYD44JI.js.map +0 -1
  239. package/dist/chunk-Q2UM7BXU.js.map +0 -1
  240. package/dist/chunk-RH5EFRVJ.js.map +0 -1
  241. package/dist/chunk-S2EGRPCM.js +0 -47
  242. package/dist/chunk-S2EGRPCM.js.map +0 -1
  243. package/dist/chunk-SINXE47Y.js +0 -55
  244. package/dist/chunk-SINXE47Y.js.map +0 -1
  245. package/dist/chunk-UF4MZSLT.js +0 -53
  246. package/dist/chunk-UF4MZSLT.js.map +0 -1
  247. package/dist/chunk-UOC2PL46.js +0 -13
  248. package/dist/chunk-UOC2PL46.js.map +0 -1
  249. package/dist/chunk-VYFAZN3F.js +0 -372
  250. package/dist/chunk-VYFAZN3F.js.map +0 -1
  251. package/dist/chunk-WRAA7GVZ.js +0 -209
  252. package/dist/chunk-WRAA7GVZ.js.map +0 -1
  253. package/dist/chunk-XGELA764.js +0 -163
  254. package/dist/chunk-XGELA764.js.map +0 -1
  255. package/dist/chunk-YUXP55T2.js.map +0 -1
  256. package/dist/chunk-YWIKDKHY.js +0 -307
  257. package/dist/chunk-YWIKDKHY.js.map +0 -1
  258. package/dist/chunk-ZJ5AOYBH.js +0 -8
  259. package/dist/chunk-ZJ5AOYBH.js.map +0 -1
  260. package/dist/chunk-ZSUUOEPL.js +0 -731
  261. package/dist/chunk-ZSUUOEPL.js.map +0 -1
  262. package/dist/chunk-ZVJZ72HY.js.map +0 -1
  263. package/dist/circuit-breaker/index.d.ts +0 -197
  264. package/dist/circuit-breaker/index.js +0 -3
  265. package/dist/circuit-breaker/index.js.map +0 -1
  266. package/dist/deadline/index.d.ts +0 -199
  267. package/dist/deadline/index.js +0 -6
  268. package/dist/deadline/index.js.map +0 -1
  269. package/dist/debounce/index.js +0 -3
  270. package/dist/debounce/index.js.map +0 -1
  271. package/dist/extension/react.d.ts +0 -121
  272. package/dist/extension/react.js +0 -75
  273. package/dist/extension/react.js.map +0 -1
  274. package/dist/extension/subs/index.d.ts +0 -45
  275. package/dist/extension/subs/index.js +0 -80
  276. package/dist/extension/subs/index.js.map +0 -1
  277. package/dist/extension/test-utils.d.ts +0 -128
  278. package/dist/extension/test-utils.js +0 -277
  279. package/dist/extension/test-utils.js.map +0 -1
  280. package/dist/fan-out/index.js +0 -5
  281. package/dist/fan-out/index.js.map +0 -1
  282. package/dist/fold-BJPdjc9w.d.ts +0 -435
  283. package/dist/graph-B3L5e0N1.d.ts +0 -923
  284. package/dist/idempotency/adapter.d.ts +0 -62
  285. package/dist/idempotency/adapter.js +0 -4
  286. package/dist/idempotency/adapter.js.map +0 -1
  287. package/dist/idempotent-intake/index.d.ts +0 -188
  288. package/dist/idempotent-intake/index.js +0 -7
  289. package/dist/idempotent-intake/index.js.map +0 -1
  290. package/dist/journal/index.d.ts +0 -373
  291. package/dist/journal/index.js +0 -3
  292. package/dist/journal/index.js.map +0 -1
  293. package/dist/llm-call/index.d.ts +0 -311
  294. package/dist/llm-call/index.js +0 -16
  295. package/dist/llm-call/index.js.map +0 -1
  296. package/dist/machine-BtFWQmmq.d.ts +0 -804
  297. package/dist/monitored-run/index.js +0 -10
  298. package/dist/monitored-run/index.js.map +0 -1
  299. package/dist/paginated-walk/index.d.ts +0 -235
  300. package/dist/paginated-walk/index.js +0 -16
  301. package/dist/paginated-walk/index.js.map +0 -1
  302. package/dist/paginator/index.js +0 -3
  303. package/dist/paginator/index.js.map +0 -1
  304. package/dist/pbt/arbitraries/index.d.ts +0 -136
  305. package/dist/pbt/arbitraries/index.js +0 -3
  306. package/dist/pbt/arbitraries/index.js.map +0 -1
  307. package/dist/pbt/runners/index.d.ts +0 -118
  308. package/dist/pbt/runners/index.js +0 -5
  309. package/dist/pbt/runners/index.js.map +0 -1
  310. package/dist/poller/index.d.ts +0 -352
  311. package/dist/poller/index.js +0 -115
  312. package/dist/poller/index.js.map +0 -1
  313. package/dist/prediction/index.d.ts +0 -131
  314. package/dist/prediction/index.js +0 -4
  315. package/dist/prediction/index.js.map +0 -1
  316. package/dist/pure/index.d.ts +0 -3
  317. package/dist/pure/index.js +0 -4
  318. package/dist/pure/index.js.map +0 -1
  319. package/dist/rate-limit/index.d.ts +0 -115
  320. package/dist/rate-limit/index.js +0 -3
  321. package/dist/rate-limit/index.js.map +0 -1
  322. package/dist/reconciler/index.d.ts +0 -193
  323. package/dist/reconciler/index.js +0 -164
  324. package/dist/reconciler/index.js.map +0 -1
  325. package/dist/recorder/index.js +0 -3
  326. package/dist/recorder/index.js.map +0 -1
  327. package/dist/reducer-4GNR6b8B.d.ts +0 -447
  328. package/dist/resilient-call/index.d.ts +0 -320
  329. package/dist/resilient-call/index.js +0 -14
  330. package/dist/resilient-call/index.js.map +0 -1
  331. package/dist/retry-to-success/index.d.ts +0 -109
  332. package/dist/retry-to-success/index.js +0 -37
  333. package/dist/retry-to-success/index.js.map +0 -1
  334. package/dist/run-pAcmBpah.d.ts +0 -713
  335. package/dist/runtime-types-CNLmhWif.d.ts +0 -517
  336. package/dist/saga/index.d.ts +0 -241
  337. package/dist/saga/index.js +0 -150
  338. package/dist/saga/index.js.map +0 -1
  339. package/dist/samples-B3My5oh2.d.ts +0 -59
  340. package/dist/snapshot/index.js +0 -5
  341. package/dist/snapshot/index.js.map +0 -1
  342. package/dist/subs/index.d.ts +0 -472
  343. package/dist/subs/index.js +0 -7
  344. package/dist/subs/index.js.map +0 -1
  345. package/dist/throttle/index.d.ts +0 -134
  346. package/dist/throttle/index.js +0 -3
  347. package/dist/throttle/index.js.map +0 -1
  348. package/dist/throttled-input/index.js.map +0 -1
  349. package/dist/token-refresh/index.d.ts +0 -216
  350. package/dist/token-refresh/index.js +0 -6
  351. package/dist/token-refresh/index.js.map +0 -1
  352. package/dist/trace-replay/index.js +0 -6
  353. package/dist/trace-replay/index.js.map +0 -1
  354. package/dist/with-deadline/index.d.ts +0 -197
  355. package/dist/with-deadline/index.js +0 -111
  356. package/dist/with-deadline/index.js.map +0 -1
  357. package/dist/with-resilience/index.d.ts +0 -225
  358. package/dist/with-resilience/index.js +0 -240
  359. package/dist/with-resilience/index.js.map +0 -1
  360. package/dist/with-telemetry/index.d.ts +0 -150
  361. package/dist/with-telemetry/index.js +0 -70
  362. package/dist/with-telemetry/index.js.map +0 -1
  363. package/dist/work-queue/adapter.d.ts +0 -84
  364. package/dist/work-queue/adapter.js +0 -4
  365. package/dist/work-queue/adapter.js.map +0 -1
  366. package/dist/work-queue/ops.d.ts +0 -71
  367. package/dist/work-queue/ops.js +0 -3
  368. package/dist/work-queue/ops.js.map +0 -1
  369. package/dist/workflow/index.d.ts +0 -485
  370. package/dist/workflow/index.js +0 -244
  371. package/dist/workflow/index.js.map +0 -1
  372. /package/dist/{cache → paginate}/index.js.map +0 -0
  373. /package/dist/{chart/inspect → persistence}/index.js.map +0 -0
@@ -3,7 +3,7 @@ function at(arr, index) {
3
3
  return index < 0 || index >= arr.length ? void 0 : arr[index];
4
4
  }
5
5
 
6
- // src/work-queue/ops.ts
6
+ // src/internal/work-queue/ops.ts
7
7
  function enqueueOp(queue, input, now, id) {
8
8
  const item = {
9
9
  id,
@@ -52,6 +52,16 @@ function removeOp(queue, id) {
52
52
  return { next, changed: next.length !== queue.length };
53
53
  }
54
54
 
55
- export { at, claimNextOp, enqueueOp, markDoneOp, patchItemOp, removeOp, resetRunningOp };
56
- //# sourceMappingURL=chunk-FV3Z5MBE.js.map
57
- //# sourceMappingURL=chunk-FV3Z5MBE.js.map
55
+ // src/internal/work-queue/adapter.ts
56
+ function queueAdapter() {
57
+ return {
58
+ enqueue: (queue, input, now, id) => enqueueOp(queue, input, now, id),
59
+ claim: (queue, now) => claimNextOp(queue, now),
60
+ markDone: (queue, id) => markDoneOp(queue, id),
61
+ patch: (queue, id, patch) => patchItemOp(queue, id, patch)
62
+ };
63
+ }
64
+
65
+ export { at, claimNextOp, enqueueOp, markDoneOp, patchItemOp, queueAdapter, removeOp, resetRunningOp };
66
+ //# sourceMappingURL=chunk-TXN2KFHF.js.map
67
+ //# sourceMappingURL=chunk-TXN2KFHF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/at.ts","../src/internal/work-queue/ops.ts","../src/internal/work-queue/adapter.ts"],"names":[],"mappings":"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","file":"chunk-TXN2KFHF.js","sourcesContent":["// ---------------------------------------------------------------------------\n// at — indexed array read that returns `T | undefined`, stating this package's\n// findIndex-narrowing convention once.\n//\n// Without `noUncheckedIndexedAccess`, `arr[idx]` is typed `T` even when `idx`\n// is `-1` (findIndex's \"no match\") or past the end — so the recurring\n// \"locate by findIndex, then use the element\" pattern has to hand-collapse\n// `idx === -1 ? undefined : arr[idx]` to get an honest `T | undefined` and\n// early-out without a non-null assertion. That collapse (plus a justifying\n// comment) was re-derived at every such site (work-queue/ops, fan-out). `at`\n// owns the convention: pass the `findIndex` result straight in, branch on the\n// `undefined` it returns, and keep `idx` around for any subsequent splice.\n// ---------------------------------------------------------------------------\n\nexport function at<T>(arr: readonly T[], index: number): T | undefined {\n return index < 0 || index >= arr.length ? undefined : arr[index];\n}\n","/**\n * Pure queue-lifecycle ops over `QueueItem<I>[]` — the BLESSED delegation\n * surface for this package.\n *\n * Each op is `(queue, ...args) => { next, result? }`. No I/O, no Date.now\n * indirection beyond a plain call site — the adapter in `./index.ts` injects\n * `Store<QueueItem<I>[]>` and orchestrates `load → mutate → save`.\n *\n * Substrate-agnostic by construction: nothing here references chrome.storage,\n * Cloudflare, or any concrete persistence layer. The audit-specific shape\n * (journeyId, projectId, ...) is lifted into the caller's `I` parameter.\n *\n * Importable two ways. The `createQueue(...)` adapter in `./index.ts` binds\n * these ops to a `Store` for in-process use (same one-shape-per-package rule\n * as `@demlik/tea/do`). L2 compositions that own their own Model slice\n * (`monitored-run`, `idempotent-intake`, …) instead reach these ops directly\n * from `internal/work-queue/ops`, so a status flip is delegated\n * here rather than re-rolled per call site.\n */\n\nimport { at } from \"../../at\";\nimport type { QueueItem } from \"./index\";\n\n/** Append a new pending item. Returns the new queue and the created item. */\nexport function enqueueOp<I>(\n queue: readonly QueueItem<I>[],\n input: I,\n now: number,\n id: string,\n): { next: QueueItem<I>[]; item: QueueItem<I> } {\n const item: QueueItem<I> = {\n id,\n status: \"pending\",\n input,\n enqueuedAt: now,\n };\n return { next: [...queue, item], item };\n}\n\n/**\n * Claim the first pending item by flipping it to `running` and stamping\n * `startedAt`. Returns the new queue and the claimed item, or `null` if no\n * pending item exists.\n */\nexport function claimNextOp<I>(\n queue: readonly QueueItem<I>[],\n now: number,\n): { next: QueueItem<I>[]; claimed: QueueItem<I> } | null {\n const idx = queue.findIndex((i) => i.status === \"pending\");\n // `at` collapses \"no pending item\" (idx === -1) into the same `undefined`\n // early-out, so the element is typed `QueueItem<I>` below without a non-null\n // assertion; `idx` stays around for the write.\n const target = at(queue, idx);\n if (target === undefined) return null;\n const claimed: QueueItem<I> = {\n ...target,\n status: \"running\",\n startedAt: now,\n };\n const next = queue.slice();\n next[idx] = claimed;\n return { next, claimed };\n}\n\n/**\n * Done items leave the queue — completed runs live wherever the caller\n * persists their output. Returns the new queue (or the original reference if\n * no item matched, so the adapter can skip a save).\n */\nexport function markDoneOp<I>(\n queue: readonly QueueItem<I>[],\n id: string,\n): { next: QueueItem<I>[]; changed: boolean } {\n const next = queue.filter((i) => i.id !== id);\n return { next, changed: next.length !== queue.length };\n}\n\n/**\n * Generic single-item status transition. Used by `markFailed`,\n * `markCancelled`, `retryItem`, `bindOutput`. The patch closure receives the\n * matched item and returns its replacement; returning the same reference is\n * fine (the adapter only saves on `changed`).\n */\nexport function patchItemOp<I>(\n queue: readonly QueueItem<I>[],\n id: string,\n patch: (item: QueueItem<I>) => QueueItem<I>,\n): { next: QueueItem<I>[]; changed: boolean } {\n const idx = queue.findIndex((i) => i.id === id);\n // `at` collapses the no-match case into an `undefined` early-out, narrowing\n // `target` to `QueueItem<I>` for `patch` without a non-null assertion; `idx`\n // stays around for the write.\n const target = at(queue, idx);\n if (target === undefined) return { next: queue.slice(), changed: false };\n const next = queue.slice();\n next[idx] = patch(target);\n return { next, changed: true };\n}\n\n/** Reset every `running` item back to `pending`, clearing `startedAt`. */\nexport function resetRunningOp<I>(queue: readonly QueueItem<I>[]): {\n next: QueueItem<I>[];\n changed: boolean;\n} {\n let changed = false;\n const next = queue.map((i): QueueItem<I> => {\n if (i.status !== \"running\") return i;\n changed = true;\n return { ...i, status: \"pending\", startedAt: undefined };\n });\n return { next, changed };\n}\n\n/** Hard-delete an item regardless of status. */\nexport function removeOp<I>(\n queue: readonly QueueItem<I>[],\n id: string,\n): { next: QueueItem<I>[]; changed: boolean } {\n const next = queue.filter((i) => i.id !== id);\n return { next, changed: next.length !== queue.length };\n}\n","/**\n * internal/work-queue/adapter — the verb seam over the pure queue ops.\n *\n * The pure ops in `./ops` are the BLESSED transitions, but their names and\n * argument tuples (`enqueueOp(queue, input, now, id)`, `claimNextOp(queue,\n * now)`, …) and their `{ next, item }` / `{ next, changed }` result shapes are\n * the STORE's vocabulary, not the consumer's. An L2 composition that owns a\n * `QueueItem<I>[]` slice (`monitored-run`, `saga`, `idempotent-intake`,\n * `fan-out`) used to import those ops by name and call them directly, so a\n * change to the op surface — or to `QueueItem`'s fields — cascaded to every\n * call site.\n *\n * `QueueAdapter<I>` is the insulating interface: four VERBS (`enqueue` /\n * `claim` / `markDone` / `patch`) that consumers depend on instead of the raw\n * ops. The verbs keep the ops' pure `(queue, …) => { next, … }` contract — they\n * are pure array transforms, no I/O, no `Date.now` (the clock is injected as\n * `now`, exactly as the ops require it) — so a consumer folding the adapter\n * through its reducer stays inside invariant 2 (pure transitions).\n *\n * `queueAdapter<I>()` is the canonical implementation, a thin binding of the\n * blessed ops. Because the verbs are the only surface consumers touch, a\n * `QueueItem` shape change (a new field, a renamed status, a different op\n * signature) is now absorbed HERE — re-bind the verb once — rather than at the\n * 4 consumer call sites. The `patch` verb deliberately takes the same\n * `(item) => item` closure `patchItemOp` does, so the one place a `QueueItem`\n * literal is spread stays the consumer's patch closure, never a re-rolled\n * record.\n *\n * This is an INTERNAL seam (no public-API port): consumers import the adapter\n * from this subpath the same way they imported the ops. The ops remain\n * exported for the in-process `createQueue` adapter (`./index`) and for any\n * call site that genuinely wants the op directly.\n */\n\nimport type { QueueItem } from \"./index\";\nimport { claimNextOp, enqueueOp, markDoneOp, patchItemOp } from \"./ops\";\n\n/**\n * The verb interface over a `QueueItem<I>[]` slice. Each verb mirrors a blessed\n * op's pure contract but speaks the consumer's lifecycle vocabulary, so the\n * consumer never names a `*Op` function or reaches into a `QueueItem` field it\n * did not put there.\n */\nexport interface QueueAdapter<I> {\n /** Append a new `pending` item stamped `now` under `id`. */\n enqueue(\n queue: readonly QueueItem<I>[],\n input: I,\n now: number,\n id: string,\n ): { next: QueueItem<I>[]; item: QueueItem<I> };\n\n /**\n * Claim the first `pending` item — flip it `running`, stamp `startedAt` to\n * `now` — or `null` if nothing is pending.\n */\n claim(\n queue: readonly QueueItem<I>[],\n now: number,\n ): { next: QueueItem<I>[]; claimed: QueueItem<I> } | null;\n\n /** Drop the item `id` from the queue (completed runs leave the queue). */\n markDone(\n queue: readonly QueueItem<I>[],\n id: string,\n ): { next: QueueItem<I>[]; changed: boolean };\n\n /**\n * Single-item transition: replace the item `id` with `patch(item)`. The\n * closure is the only place a `QueueItem` literal is constructed, so the\n * field shape stays out of the consumer's hands except through this verb.\n */\n patch(\n queue: readonly QueueItem<I>[],\n id: string,\n patch: (item: QueueItem<I>) => QueueItem<I>,\n ): { next: QueueItem<I>[]; changed: boolean };\n}\n\n/**\n * The canonical `QueueAdapter<I>`, binding each verb to its blessed op. Pure\n * and stateless — call once at module scope in a consumer and fold its verbs\n * through the reducer. A `QueueItem` shape change is absorbed by re-binding a\n * verb here, not at the consumers.\n */\nexport function queueAdapter<I>(): QueueAdapter<I> {\n return {\n enqueue: (queue, input, now, id) => enqueueOp<I>(queue, input, now, id),\n claim: (queue, now) => claimNextOp<I>(queue, now),\n markDone: (queue, id) => markDoneOp<I>(queue, id),\n patch: (queue, id, patch) => patchItemOp<I>(queue, id, patch),\n };\n}\n"]}
@@ -1,4 +1,7 @@
1
- // src/recorder/index.ts
1
+ import { typeOf, indexPath, unionKeys, keyPath } from './chunk-HWVW4AXC.js';
2
+ import { replay } from './chunk-66PY5WYF.js';
3
+
4
+ // src/internal/persistence/recorder/index.ts
2
5
  function recorder(runtime, opts) {
3
6
  const rawRate = opts?.sampleRate;
4
7
  const sampleRate = rawRate === void 0 || Number.isNaN(rawRate) || rawRate <= 0 ? 1 : Math.min(rawRate, 1);
@@ -24,7 +27,7 @@ function recorder(runtime, opts) {
24
27
  dump() {
25
28
  if (finalState === void 0) {
26
29
  throw new Error(
27
- "@demlik/tea/recorder: dump() called before any transition was observed. Await `runtime.ready` (boot fires the first observe) before dumping."
30
+ "tea recorder: dump() called before any transition was observed. Await `runtime.ready` (boot fires the first observe) before dumping."
28
31
  );
29
32
  }
30
33
  return {
@@ -41,7 +44,7 @@ function recorder(runtime, opts) {
41
44
  toJSONL() {
42
45
  if (finalState === void 0) {
43
46
  throw new Error(
44
- "@demlik/tea/recorder: toJSONL() called before any transition was observed. Await `runtime.ready` (boot fires the first observe) before serializing."
47
+ "tea recorder: toJSONL() called before any transition was observed. Await `runtime.ready` (boot fires the first observe) before serializing."
45
48
  );
46
49
  }
47
50
  return traceToJSONL(this.dump());
@@ -70,13 +73,13 @@ function parseJSONL(jsonl) {
70
73
  parsed = JSON.parse(line);
71
74
  } catch (err) {
72
75
  throw new Error(
73
- `@demlik/tea/recorder: parseJSONL failed on line ${i + 1}: not valid JSON. ${String(err)}`
76
+ `tea recorder: parseJSONL failed on line ${i + 1}: not valid JSON. ${String(err)}`
74
77
  );
75
78
  }
76
79
  if (parsed.kind === "boot") {
77
80
  if (sawBoot) {
78
81
  throw new Error(
79
- `@demlik/tea/recorder: parseJSONL saw a second boot header on line ${i + 1}. A trace has exactly one boot line.`
82
+ `tea recorder: parseJSONL saw a second boot header on line ${i + 1}. A trace has exactly one boot line.`
80
83
  );
81
84
  }
82
85
  sawBoot = true;
@@ -86,7 +89,7 @@ function parseJSONL(jsonl) {
86
89
  } else if (parsed.kind === "step") {
87
90
  if (!sawBoot) {
88
91
  throw new Error(
89
- `@demlik/tea/recorder: parseJSONL saw a step line before the boot header on line ${i + 1}.`
92
+ `tea recorder: parseJSONL saw a step line before the boot header on line ${i + 1}.`
90
93
  );
91
94
  }
92
95
  msgs.push(parsed.msg);
@@ -97,18 +100,18 @@ function parseJSONL(jsonl) {
97
100
  }
98
101
  } else {
99
102
  throw new Error(
100
- `@demlik/tea/recorder: parseJSONL saw an unknown line kind on line ${i + 1}: ` + JSON.stringify(parsed.kind)
103
+ `tea recorder: parseJSONL saw an unknown line kind on line ${i + 1}: ` + JSON.stringify(parsed.kind)
101
104
  );
102
105
  }
103
106
  }
104
107
  if (!sawBoot) {
105
108
  throw new Error(
106
- '@demlik/tea/recorder: parseJSONL found no boot header. A trace must start with { kind: "boot", state }.'
109
+ 'tea recorder: parseJSONL found no boot header. A trace must start with { kind: "boot", state }.'
107
110
  );
108
111
  }
109
112
  if (!finalKnown || finalState === null) {
110
113
  throw new Error(
111
- "@demlik/tea/recorder: parseJSONL could not determine finalState \u2014 the boot state was null and no step carried a non-null state. The trace is unusable for replay."
114
+ "tea recorder: parseJSONL could not determine finalState \u2014 the boot state was null and no step carried a non-null state. The trace is unusable for replay."
112
115
  );
113
116
  }
114
117
  return { loaded, msgs, finalState, steps };
@@ -153,6 +156,53 @@ function traceToJSONL(trace) {
153
156
  return lines.join("\n");
154
157
  }
155
158
 
156
- export { breadcrumbsFromTrace, parseJSONL, recorder, traceAttachment };
157
- //# sourceMappingURL=chunk-MUUROGIH.js.map
158
- //# sourceMappingURL=chunk-MUUROGIH.js.map
159
+ // src/internal/persistence/trace-replay/index.ts
160
+ function replayTrace(machine, trace, ctx) {
161
+ const result = replay(machine, {
162
+ msgs: trace.msgs,
163
+ ctx,
164
+ loaded: trace.loaded
165
+ });
166
+ const actual = result.state;
167
+ const expected = trace.finalState;
168
+ const divergence = firstDivergence(expected, actual, "state");
169
+ if (divergence === null) {
170
+ return { matches: true, expected, actual };
171
+ }
172
+ return { matches: false, expected, actual, divergence };
173
+ }
174
+ function deepEqual(a, b) {
175
+ return firstDivergence(a, b, "state") === null;
176
+ }
177
+ function firstDivergence(expected, actual, path) {
178
+ if (Object.is(expected, actual)) return null;
179
+ const te = typeOf(expected);
180
+ const ta = typeOf(actual);
181
+ if (te !== ta) return { path, expected, actual };
182
+ if (te === "array") {
183
+ const ae = expected;
184
+ const aa = actual;
185
+ if (ae.length !== aa.length) {
186
+ return { path, expected, actual };
187
+ }
188
+ for (let i = 0; i < ae.length; i++) {
189
+ const d = firstDivergence(ae[i], aa[i], indexPath(path, i));
190
+ if (d !== null) return d;
191
+ }
192
+ return null;
193
+ }
194
+ if (te === "object") {
195
+ const oe = expected;
196
+ const oa = actual;
197
+ for (const key of unionKeys(oe, oa)) {
198
+ const d = firstDivergence(oe[key], oa[key], keyPath(path, key));
199
+ if (d !== null) return d;
200
+ }
201
+ return null;
202
+ }
203
+ return { path, expected, actual };
204
+ }
205
+
206
+ export { breadcrumbsFromTrace, deepEqual, parseJSONL, recorder, replayTrace, traceAttachment };
207
+ //# sourceMappingURL=chunk-V46HSVCK.js.map
208
+ //# sourceMappingURL=chunk-V46HSVCK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/persistence/recorder/index.ts","../src/internal/persistence/trace-replay/index.ts"],"names":[],"mappings":"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","file":"chunk-V46HSVCK.js","sourcesContent":["// ---------------------------------------------------------------------------\n// recorder — capture a Runtime's Msg sequence for later replay.\n//\n// `historyTracker` (in ../index) is the exemplar: a BOUNDED, in-memory ring\n// buffer over `runtime.observe`. The recorder is its UNBOUNDED, SERIALIZABLE\n// cousin. Where historyTracker exists to answer \"what were the last N\n// transitions?\", the recorder exists to answer \"reproduce this entire run\n// locally against the current reducer.\"\n//\n// The killer feature this enables: record a prod Msg sequence, dump it as\n// JSONL, ship the file home, and feed it to `../trace-replay`'s `replayTrace`\n// — which folds the SAME msgs through `replay(machine, ...)` and diffs the\n// resulting state against the recorded final state. If they diverge, the\n// reducer changed behavior for that input. Regression-as-a-trace.\n//\n// What `replay` needs to reproduce a run (see `replay` in ../index):\n// - `loaded` — the value `init` was booted with. Captured at the boot\n// observe, where `msg === null` and `state` is the post-init\n// initial state. (We capture the post-init state, not the raw\n// storage value, because replay re-runs `init(loaded, ctx)`\n// itself — and for a faithful re-run we feed it that initial\n// state as `loaded` so `init` returns `[loaded, []]`. This is\n// the rehydrate path, which the substrate guarantees is a pure\n// passthrough; see Invariant 2.)\n// - `msgs` — every non-null msg, in dispatch order.\n// - `ctx` — NOT captured here. Ctx is host wiring (sibling runtimes,\n// clients), supplied fresh at replay time. The trace is the\n// INPUT (loaded + msgs); ctx is the ENVIRONMENT.\n//\n// This module OWNS the `Trace` type. `../trace-replay` imports it.\n// ---------------------------------------------------------------------------\n\nimport type { BootingRuntime } from \"../../../index\";\n\n/**\n * A recorded run, sufficient to reproduce it via `../trace-replay`.\n *\n * The recorder OWNS this type; `../trace-replay` imports it. It is the wire\n * contract between the two halves of the record/replay feature.\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Trace<S, M> {\n /**\n * The initial state captured at the boot observe (the transition where\n * `msg === null`). Fed back to replay as `loaded` so `init(loaded, ctx)`\n * reproduces the exact starting point. `null` only if the recorder attached\n * AFTER boot (it missed the boot observe) — replay then re-runs fresh `init`.\n */\n readonly loaded: S | null;\n /** The ordered, non-null msgs as observed. The replay input sequence. */\n readonly msgs: readonly M[];\n /**\n * The last observed state. The oracle: `replayTrace` deep-compares its\n * recomputed final state against this. Equal ⇒ the reducer still produces\n * the same result for this input.\n */\n readonly finalState: S;\n /**\n * Per-transition `(msg, post-state)` pairs, in dispatch order, for human\n * inspection (e.g. \"show me the state after step 3\"). When present, there\n * is exactly one entry per recorded msg and `state` is the POST-transition\n * state for that msg (so `steps[i].msg === msgs[i]` and `steps.at(-1).state`\n * deep-equals `finalState`). NOT required for replay — replay only needs\n * `loaded` + `msgs` + `finalState`.\n *\n * Present iff the recorder was created with `captureSteps: true`, OR the\n * Trace was re-hydrated from JSONL via {@link parseJSONL} (the JSONL format\n * carries per-step state, so the parser populates `steps` whenever a step\n * line had a non-null state). A trace dumped with `captureSteps: false` has\n * `steps === undefined`.\n */\n readonly steps?: readonly { readonly msg: M; readonly state: S }[];\n}\n\n/** Options for {@link recorder}. */\nexport interface RecorderOptions {\n /**\n * Fraction of msgs to record, in `(0, 1]`. Default `1` (record everything).\n *\n * IMPORTANT — sampling breaks replay fidelity. A sampled trace DROPS msgs,\n * so its `msgs` array is no longer the complete input sequence and\n * `replayTrace` will NOT reproduce `finalState`. Sampling exists for\n * VOLUME STATISTICS (how much traffic, msg-type distribution), not for\n * faithful replay. Use the default `1` for any trace you intend to replay.\n *\n * The boot observe (`msg === null`) is ALWAYS recorded regardless of\n * `sampleRate` — `loaded` and `finalState` are anchors, not samples.\n */\n readonly sampleRate?: number;\n /**\n * When `true`, retain a per-transition `(msg, post-state)` pair in\n * {@link Trace.steps} for EVERY recorded transition. Default `false`.\n *\n * Size vs. fidelity — the precise trade-off:\n *\n * - `captureSteps: false` (default) — the MINIMAL replay-faithful trace.\n * The recorder keeps `loaded` (boot state), the ordered `msgs`, and\n * `finalState`. That is exactly what `../trace-replay` needs to refold\n * the run and diff the result. Memory is O(msgs) — one msg ref each, no\n * per-step state snapshot. `dump().steps` is `undefined`; in JSONL,\n * interior step lines carry `state: null` (the post-states were never\n * retained) and only the LAST step line carries `finalState`. A trace\n * dumped this way REPLAYS correctly but cannot answer \"what was the\n * state after step 3?\".\n *\n * - `captureSteps: true` — the FULL-fidelity trace. The recorder also\n * snapshots the post-transition state for every msg, so `dump().steps`\n * holds `{ msg, state }` for every transition (post-state) and\n * `toJSONL()` emits a non-null `state` on every step line. Memory is\n * O(msgs × |state|) — a state ref per transition (refs, not deep\n * copies; TEA states are conventionally immutable, so the runtime's\n * post-transition state object is retained by reference). This is the\n * mode for per-step inspection and for a JSONL file that round-trips\n * `steps` exactly via {@link parseJSONL}.\n *\n * Replay fidelity is identical in both modes — `../trace-replay` reads\n * `loaded` + `msgs` + `finalState` only and never touches `steps`. The\n * choice is purely \"do I also want to inspect intermediate states?\".\n */\n readonly captureSteps?: boolean;\n}\n\n/**\n * A live recorder attached to a Runtime. Mirrors `historyTracker`'s\n * stop/snapshot semantics: `dump()` works at any time (including after\n * `stop()`), `stop()` is idempotent and detaches the observer.\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Recorder<S, M> {\n /**\n * Snapshot the recording so far as a {@link Trace}. Returns a fresh object\n * with copied arrays on every call (callers may slice/replay without\n * affecting the recorder's buffers). Entry values are references to the\n * originals (TEA states/msgs are conventionally immutable).\n *\n * Throws only if called before the boot observe has fired AND no msgs have\n * been recorded — i.e. there is no `finalState` yet. In practice callers\n * `await runtime.ready` before `dump()`, after which boot has fired.\n */\n dump(): Trace<S, M>;\n /**\n * Serialize the recording as JSONL (one JSON object per line) — a streamable\n * format that appends cleanly and survives truncation (a partial tail line\n * is the only loss). The first line is the boot header\n * `{ kind: \"boot\", state }`; each subsequent line is\n * `{ kind: \"step\", msg, state }` for one recorded transition.\n *\n * Re-hydrate with {@link parseJSONL}.\n *\n * Round-trip fidelity depends on `captureSteps`:\n * - `captureSteps: true` — every step line carries its non-null\n * post-state, so `parseJSONL(rec.toJSONL())` reconstructs `steps`\n * EXACTLY and deep-equals `rec.dump()` (which also has `steps`). Full\n * per-step fidelity round-trips.\n * - `captureSteps: false` (default) — interior step lines carry\n * `state: null` (those post-states were never retained); only the last\n * step line carries `finalState`. `parseJSONL` then populates `steps`\n * with just the entries that had a non-null state (the final one), so\n * the re-hydrated Trace is NOT deep-equal to `rec.dump()` (which has no\n * `steps`). It is still fully replay-faithful: `loaded`, `msgs`, and\n * `finalState` are intact. Record with `captureSteps: true` if you need\n * interior states to survive the round-trip.\n */\n toJSONL(): string;\n /**\n * Detach the underlying observer. After `stop()`:\n * - no new transitions are recorded;\n * - `dump()` / `toJSONL()` keep working against the buffered contents;\n * - the recorder holds no observer reference into the runtime.\n *\n * Idempotent — subsequent calls are no-ops.\n */\n stop(): void;\n}\n\n/**\n * Attach a recorder to a Runtime. Subscribes via `runtime.observe`, capturing\n * the boot state as `loaded` and every subsequent non-null msg in order.\n *\n * One-line creation, mirroring `historyTracker`:\n *\n * const rec = recorder(runtime);\n * // ... drive the runtime ...\n * await runtime.ready; // boot observe has fired\n * const trace = rec.dump(); // { loaded, msgs, finalState }\n *\n * @param runtime - any `BootingRuntime<S, M>` (a full `Runtime` satisfies it).\n * The recorder uses only `observe`, which is total before boot\n * — so you attach it to the synchronous `run()` handle to\n * capture the boot transition as `loaded`.\n * @param opts - see {@link RecorderOptions}. Default records everything,\n * no step retention.\n */\nexport function recorder<S, M extends { type: string }>(\n runtime: BootingRuntime<S, M>,\n opts?: RecorderOptions,\n): Recorder<S, M> {\n // Clamp sampleRate into (0, 1]. A non-positive or NaN rate would record\n // nothing (and break replay anchors); default it to 1 (record all). Rates\n // above 1 are meaningless — clamp to 1.\n const rawRate = opts?.sampleRate;\n const sampleRate =\n rawRate === undefined || Number.isNaN(rawRate) || rawRate <= 0\n ? 1\n : Math.min(rawRate, 1);\n const captureSteps = opts?.captureSteps ?? false;\n\n // `loaded` is the post-init state captured at the boot observe. `undefined`\n // sentinel = boot not yet seen; resolved to `S | null` in dump() (null when\n // the recorder attached after boot and never observed msg === null).\n let loaded: S | undefined;\n let sawBoot = false;\n // The ordered, recorded msgs. Unbounded (the recorder's whole point vs the\n // bounded historyTracker).\n const msgs: M[] = [];\n // Per-step (msg, state) pairs — only populated when captureSteps is on.\n const steps: { msg: M; state: S }[] = [];\n // The last observed state — boot state until the first step, then advances\n // with every recorded OR dropped transition (finalState must reflect the\n // true latest state even under sampling, so it stays a valid replay oracle\n // for the unsampled case and an honest \"where the run ended\" marker).\n let finalState: S | undefined;\n let stopped = false;\n\n // The boot transition — the post-init initial state the old `observe(msg ===\n // null)` arm captured as `loaded` — now arrives via `onBoot` (#47). Always\n // captured (boot is the replay anchor). It is also the first `finalState`\n // until the first applied transition advances it.\n const unboot = runtime.onBoot((state) => {\n finalState = state;\n loaded = state;\n sawBoot = true;\n });\n\n const unobserve = runtime.observe((msg, state) => {\n // finalState tracks the latest state regardless of sampling — it is the\n // run's endpoint, an anchor, not a sample.\n finalState = state;\n\n // Sampling decision. sampleRate === 1 short-circuits to always-record so\n // the common (faithful) path pays no RNG cost. Math.random() < rate keeps\n // a `rate` fraction in expectation.\n if (sampleRate < 1 && Math.random() >= sampleRate) return;\n\n msgs.push(msg);\n if (captureSteps) steps.push({ msg, state });\n });\n\n return {\n dump(): Trace<S, M> {\n if (finalState === undefined) {\n throw new Error(\n \"tea recorder: dump() called before any transition was observed. \" +\n \"Await `runtime.ready` (boot fires the first observe) before dumping.\",\n );\n }\n return {\n // sawBoot ⇒ `loaded` is the captured boot state; otherwise the\n // recorder attached post-boot and replay must re-run fresh init.\n loaded: sawBoot ? (loaded as S) : null,\n msgs: msgs.slice(),\n finalState,\n // Only attach `steps` when captureSteps was requested — keeps the\n // dumped Trace shape honest about what was retained.\n ...(captureSteps ? { steps: steps.slice() } : {}),\n };\n },\n toJSONL(): string {\n if (finalState === undefined) {\n throw new Error(\n \"tea recorder: toJSONL() called before any transition was observed. \" +\n \"Await `runtime.ready` (boot fires the first observe) before serializing.\",\n );\n }\n // Serialize the live snapshot via the shared Trace→JSONL writer so the\n // recorder and the standalone `traceAttachment` path emit byte-identical\n // wire format. `dump()` resolves the captureSteps logic (steps present\n // iff captureSteps was on); `traceToJSONL` then emits a non-null state\n // on every step line when steps exist, or `null` interior + finalState\n // on the last line when they don't. The replay path never reads step\n // states, so the captureSteps:false null-interiors never affect replay.\n return traceToJSONL(this.dump());\n },\n stop(): void {\n if (stopped) return;\n stopped = true;\n unobserve();\n unboot();\n },\n };\n}\n\n/** One line of the JSONL format produced by {@link Recorder.toJSONL}. */\ntype JsonlLine<S, M> =\n | { readonly kind: \"boot\"; readonly state: S | null }\n | { readonly kind: \"step\"; readonly msg: M; readonly state: S | null };\n\n/**\n * Re-hydrate a {@link Trace} from the JSONL produced by\n * {@link Recorder.toJSONL}. The inverse of `toJSONL`, so\n * `parseJSONL(rec.toJSONL())` deep-equals `rec.dump()`.\n *\n * Parsing rules:\n * - The first non-blank line MUST be the boot header `{ kind: \"boot\", state }`.\n * Its `state` becomes `Trace.loaded`.\n * - Each `{ kind: \"step\", msg, state }` line contributes `msg` to\n * `Trace.msgs` and `(msg, state)` to `Trace.steps`.\n * - `Trace.finalState` is the `state` of the LAST step line, or the boot\n * `state` when there are no steps (a boot-only trace).\n *\n * Blank lines are skipped (tolerant of trailing newlines / hand-edited files).\n * A malformed line, a missing/duplicate boot header, or a step with `null`\n * final state throws — the trace is unusable for replay and silent corruption\n * is worse than a loud parse error (errors are data; surface them).\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function parseJSONL<S, M>(jsonl: string): Trace<S, M> {\n const rawLines = jsonl.split(\"\\n\");\n let loaded: S | null = null;\n let sawBoot = false;\n const msgs: M[] = [];\n const steps: { msg: M; state: S }[] = [];\n let finalState: S | null = null;\n let finalKnown = false;\n\n for (const [i, rawLine] of rawLines.entries()) {\n const line = rawLine.trim();\n if (line === \"\") continue; // tolerate blank / trailing-newline lines\n\n let parsed: JsonlLine<S, M>;\n try {\n parsed = JSON.parse(line) as JsonlLine<S, M>;\n } catch (err) {\n throw new Error(\n `tea recorder: parseJSONL failed on line ${i + 1}: not valid JSON. ${String(err)}`,\n );\n }\n\n if (parsed.kind === \"boot\") {\n if (sawBoot) {\n throw new Error(\n `tea recorder: parseJSONL saw a second boot header on line ${i + 1}. ` +\n \"A trace has exactly one boot line.\",\n );\n }\n sawBoot = true;\n loaded = parsed.state;\n // The boot state is the finalState until a step supersedes it (boot-only\n // trace case).\n finalState = parsed.state;\n finalKnown = parsed.state !== null;\n } else if (parsed.kind === \"step\") {\n if (!sawBoot) {\n throw new Error(\n `tea recorder: parseJSONL saw a step line before the boot header on line ${i + 1}.`,\n );\n }\n msgs.push(parsed.msg);\n if (parsed.state !== null) {\n steps.push({ msg: parsed.msg, state: parsed.state });\n finalState = parsed.state;\n finalKnown = true;\n }\n } else {\n throw new Error(\n `tea recorder: parseJSONL saw an unknown line kind on line ${i + 1}: ` +\n JSON.stringify((parsed as { kind?: unknown }).kind),\n );\n }\n }\n\n if (!sawBoot) {\n throw new Error(\n \"tea recorder: parseJSONL found no boot header. A trace must start with \" +\n '{ kind: \"boot\", state }.',\n );\n }\n if (!finalKnown || finalState === null) {\n throw new Error(\n \"tea recorder: parseJSONL could not determine finalState — the boot state \" +\n \"was null and no step carried a non-null state. The trace is unusable for replay.\",\n );\n }\n\n return { loaded, msgs, finalState, steps };\n}\n\n// ---------------------------------------------------------------------------\n// Sentry crash→trace adapters — pure, zero-dependency, Sentry-SHAPED.\n//\n// The killer DevX flow: a machine crashes in prod → its recorded Trace rides\n// along on the Sentry event → one click later you `parseJSONL` it and replay\n// the exact run locally against the (possibly fixed) reducer. These two\n// adapters are the bridge from a `Trace` to the two Sentry surfaces that carry\n// it: an Attachment (the full replayable trace) and Breadcrumbs (a human-\n// readable timeline of the msgs that led to the crash).\n//\n// They take NO dependency on `@sentry/*`. They emit PLAIN OBJECTS whose shapes\n// match Sentry's public `Attachment` and `Breadcrumb` types, and the consumer\n// wires them at their own Sentry boundary (browser or node). This keeps the\n// recorder runnable anywhere a machine runs (a Worker, a Node script, a test)\n// and lets the consumer pin whatever Sentry SDK version they already ship.\n// ---------------------------------------------------------------------------\n\n/**\n * A Sentry-shaped attachment carrying the full {@link Trace} as JSONL. The\n * shape matches Sentry's `Attachment` type so the consumer can hand it\n * straight to `addAttachment(...)` without any adaptation.\n *\n * - `filename` — `\"tea-trace.jsonl\"`. The name Sentry shows in the UI and\n * uses for download; `.jsonl` signals the streamable format.\n * - `data` — the trace serialized via {@link Recorder.toJSONL}'s format\n * (re-hydrate downstream with {@link parseJSONL}).\n * - `contentType` — `\"application/x-ndjson\"`, the standard MIME for\n * newline-delimited JSON.\n */\nexport interface TraceAttachment {\n readonly filename: string;\n readonly data: string;\n readonly contentType: string;\n}\n\n/**\n * Serialize a {@link Trace} into a Sentry-shaped attachment so a crash event\n * carries the full, replayable run. Pure — no Sentry import, no I/O. The\n * returned object matches Sentry's `Attachment` shape exactly.\n *\n * The `data` field is JSONL in the same format {@link Recorder.toJSONL}\n * produces, so the home-side flow is `parseJSONL(attachment.data)` →\n * `replayTrace(machine, trace, ctx)`.\n *\n * Consumer wiring (no Sentry import here — the consumer owns the boundary):\n *\n * ```ts\n * import * as Sentry from \"@sentry/browser\"; // or \"@sentry/node\"\n * import { traceAttachment } from \"./internal/persistence/recorder\"; // internal module\n * Sentry.getCurrentScope().addAttachment(traceAttachment(rec.dump()));\n * ```\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function traceAttachment<S, M extends { type: string }>(\n trace: Trace<S, M>,\n): TraceAttachment {\n return {\n filename: \"tea-trace.jsonl\",\n data: traceToJSONL(trace),\n contentType: \"application/x-ndjson\",\n };\n}\n\n/** A Sentry-shaped breadcrumb. Matches the fields of Sentry's `Breadcrumb`. */\nexport interface TraceBreadcrumb {\n readonly category: string;\n readonly type: string;\n readonly level: string;\n readonly message: string;\n readonly data?: Record<string, unknown>;\n}\n\n/** Options for {@link breadcrumbsFromTrace}. */\nexport interface BreadcrumbsOptions<M> {\n /**\n * Max breadcrumbs to emit. Default `100` (Sentry's own default breadcrumb\n * cap). When the trace has more msgs than `limit`, the OLDEST are dropped —\n * the returned list is the newest-last tail, matching how a Sentry\n * breadcrumb ring buffer behaves (the crash context is the recent past).\n */\n readonly limit?: number;\n /**\n * Customize each breadcrumb's `message`. Default uses `msg.type`. Provide a\n * serializer to surface a richer one-liner (e.g. include an id). The msg is\n * ALSO always attached verbatim under `data.msg` regardless of this option,\n * so the structured payload is never lost to the string rendering.\n */\n readonly serialize?: (msg: M) => string;\n}\n\n/**\n * Project a {@link Trace}'s recorded msgs into Sentry-shaped breadcrumbs — a\n * human-readable timeline of \"what happened before the crash\". Pure — no\n * Sentry import, no I/O. Each returned object matches Sentry's `Breadcrumb`\n * shape, so the consumer can loop and call `addBreadcrumb(...)`.\n *\n * One breadcrumb per recorded msg, in dispatch order (NEWEST LAST — Sentry\n * renders breadcrumbs oldest-first and the crash is the most recent event).\n * Each breadcrumb:\n * - `category` — `\"tea\"` (groups them in the Sentry breadcrumb panel).\n * - `type` — `\"default\"` (the generic Sentry breadcrumb type).\n * - `level` — `\"info\"` (msgs are normal flow; the crash is the event).\n * - `message` — `msg.type`, or `serialize(msg)` when provided.\n * - `data.msg` — the full msg object (structured detail kept verbatim).\n *\n * Capped at `opts.limit` (default 100). When the trace has more msgs than the\n * cap, the OLDEST are dropped and the newest `limit` are kept — the recent\n * lead-up to the crash is the useful context.\n *\n * Consumer wiring (no Sentry import here — the consumer owns the boundary):\n *\n * ```ts\n * import * as Sentry from \"@sentry/browser\"; // or \"@sentry/node\"\n * import { breadcrumbsFromTrace } from \"./internal/persistence/recorder\"; // internal module\n * for (const b of breadcrumbsFromTrace(rec.dump())) Sentry.addBreadcrumb(b);\n * ```\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport function breadcrumbsFromTrace<S, M extends { type: string }>(\n trace: Trace<S, M>,\n opts?: BreadcrumbsOptions<M>,\n): TraceBreadcrumb[] {\n const rawLimit = opts?.limit;\n // Clamp limit to a non-negative integer. `undefined`/NaN → 100 (Sentry's\n // default). A negative or zero limit yields an empty list (the caller asked\n // for no breadcrumbs); fractional values floor (you can't emit half a crumb).\n const limit =\n rawLimit === undefined || Number.isNaN(rawLimit)\n ? 100\n : Math.max(0, Math.floor(rawLimit));\n const serialize = opts?.serialize;\n\n // Keep the NEWEST `limit` msgs (the recent lead-up to the crash). Slicing\n // from `length - limit` (clamped to 0) preserves dispatch order within the\n // kept window. Note we do NOT use `slice(-limit)`: `slice(-0)` is `slice(0)`\n // (JS treats -0 as 0), which would return the WHOLE array for `limit: 0`.\n const start = Math.max(0, trace.msgs.length - limit);\n const kept = start === 0 ? trace.msgs : trace.msgs.slice(start);\n\n const crumbs: TraceBreadcrumb[] = [];\n for (const msg of kept) {\n crumbs.push({\n category: \"tea\",\n type: \"default\",\n level: \"info\",\n message: serialize ? serialize(msg) : msg.type,\n // Full msg kept verbatim so the structured payload survives even when\n // `message` is a terse one-liner. Cast: a Msg is a `{ type: string }`\n // record, which is a `Record<string, unknown>` for Sentry's `data`.\n data: { msg: msg as unknown as Record<string, unknown> },\n });\n }\n return crumbs;\n}\n\n// ---------------------------------------------------------------------------\n// traceToJSONL — serialize a STANDALONE Trace to JSONL (the dump→file path).\n//\n// `Recorder.toJSONL()` serializes the recorder's live buffers. `traceAttachment`\n// works over a plain `Trace` value (a `dump()` snapshot or a `parseJSONL`\n// result), which has no recorder behind it — so it needs a Trace→JSONL\n// function. This is the same wire format `Recorder.toJSONL` emits and\n// `parseJSONL` reads, so `parseJSONL(traceToJSONL(trace))` round-trips.\n//\n// When `trace.steps` is present (captureSteps was on, or the trace came from\n// parseJSONL), every step line carries its post-state. When absent, interior\n// step states are unknown — emitted as `null`, with `finalState` on the LAST\n// step line (identical to the captureSteps:false path in `Recorder.toJSONL`).\n// ---------------------------------------------------------------------------\nfunction traceToJSONL<S, M>(trace: Trace<S, M>): string {\n const lines: string[] = [];\n lines.push(JSON.stringify({ kind: \"boot\", state: trace.loaded }));\n\n const steps = trace.steps;\n for (let i = 0; i < trace.msgs.length; i++) {\n const msg = trace.msgs[i];\n const stepState = steps\n ? steps[i]?.state\n : i === trace.msgs.length - 1\n ? trace.finalState\n : null;\n lines.push(JSON.stringify({ kind: \"step\", msg, state: stepState }));\n }\n return lines.join(\"\\n\");\n}\n","// ---------------------------------------------------------------------------\n// trace-replay — replay a recorded Trace against a machine and diff the state.\n//\n// This is the back half of the record/replay feature. `../recorder` captures\n// a run (`loaded` + `msgs` + `finalState`); this module feeds that input\n// through the core `replay(machine, { msgs, ctx, loaded })` fold and asks:\n//\n// does the current reducer still produce the SAME final state for this\n// recorded input?\n//\n// Equal ⇒ the reducer is behavior-compatible with the recording. Diverged ⇒\n// the reducer changed for that input, and the divergence path\n// (`state.queue[2].status`) points at exactly the cell that drifted. This is\n// the regression / production-repro tool: record a prod incident, replay it\n// against the fixed reducer locally, confirm the fix changes the state at the\n// path you expected — or catch an unintended drift before it ships.\n//\n// Standalone by design. It imports ONLY the `Trace` type from `../recorder`\n// and the `replay` engine from `../index`. The deep-equal + first-divergence\n// path finder is self-contained (no external diff lib) — a richer pretty-diff\n// lands in `../devtools` later; trace-replay stays dependency-free so it can\n// run anywhere a machine runs (CI, a Worker, a Node script).\n// ---------------------------------------------------------------------------\n\nimport { type Cmd, type Machine, replay, type Sub } from \"../../../index\";\nimport type { Trace } from \"../recorder\";\nimport { indexPath, keyPath, typeOf, unionKeys } from \"./state-walk\";\n\n/**\n * The outcome of {@link replayTrace}.\n *\n * @typeParam S - the machine's State type.\n */\nexport interface ReplayResult<S> {\n /** `true` iff the recomputed final state deep-equals the trace's `finalState`. */\n matches: boolean;\n /** The trace's recorded `finalState` — what the run originally produced. */\n expected: S;\n /** The state the current reducer produced for the same input. */\n actual: S;\n /**\n * Present iff `matches` is `false`. The FIRST point of divergence found by\n * a deterministic pre-order walk:\n * - `path` — a dotted/indexed access path from the state root, e.g.\n * `state.queue[2].status` or `state` for a root-level\n * scalar mismatch.\n * - `expected` — the recorded value at that path.\n * - `actual` — the recomputed value at that path.\n */\n divergence?: { path: string; expected: unknown; actual: unknown };\n}\n\n/**\n * Replay a recorded {@link Trace} against `machine` and diff the recomputed\n * final state against the recorded one.\n *\n * Runs the core pure fold `replay(machine, { msgs: trace.msgs, ctx,\n * loaded: trace.loaded })` — init + update only, never `interpret`, never a\n * `Store`, never a live subscription (see `replay` in ../index). The `ctx` is\n * supplied fresh by the caller: the trace is the INPUT (loaded + msgs); ctx is\n * the ENVIRONMENT the reducer runs against now.\n *\n * @param machine - the machine to replay against. Mutating its reducer (the\n * whole point of a regression check) is what makes a recorded\n * trace diverge.\n * @param trace - a Trace from `../recorder` (live `dump()` or a re-hydrated\n * `parseJSONL(...)`).\n * @param ctx - the runtime ctx, supplied fresh at replay time.\n */\nexport function replayTrace<\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 trace: Trace<S, M>,\n ctx: Ctx,\n): ReplayResult<S> {\n const result = replay(machine, {\n msgs: trace.msgs,\n ctx,\n loaded: trace.loaded,\n });\n const actual = result.state;\n const expected = trace.finalState;\n\n const divergence = firstDivergence(expected, actual, \"state\");\n if (divergence === null) {\n return { matches: true, expected, actual };\n }\n return { matches: false, expected, actual, divergence };\n}\n\n/**\n * Order-insensitive structural deep-equality — the comparator the\n * record/replay lane owns, exposed so consumers (e.g. `@demlik/tea/parity`'s\n * `parityEqual`) reuse it rather than adding a fourth deep-compare. It is one\n * of three structural walkers in the package, divergent on purpose:\n * this `deepEqual` returns a boolean verdict (order-insensitive, for\n * replay/parity equality), `devtools`'s `diffState` returns a change list (for\n * a human-readable diff view), and `testing`'s `__deepEqual` is the assertion\n * helpers' internal compare — same shape, three jobs, not one to collapse.\n * `true` iff `a` and `b` are structurally equal under {@link firstDivergence}'s\n * walk (object keys compared order-insensitively, arrays index-by-index).\n */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n return firstDivergence(a, b, \"state\") === null;\n}\n\n// === deep-equal + first-divergence path finder ===\n//\n// One walk does both jobs: it returns the FIRST point where `expected` and\n// `actual` differ (as a path + the two values), or `null` when they are\n// deeply equal. \"First\" is defined by a deterministic pre-order traversal:\n// the node itself, then its children in a stable key order (array index\n// order; object keys sorted so the result does not depend on insertion\n// order — two states with the same data but different key order are equal,\n// and the reported path for a real mismatch is reproducible run-to-run).\n//\n// The path grammar (`typeOf`, `indexPath`/`keyPath` segments, `unionKeys`\n// order) is shared with devtools' `diffState` via `./state-walk`, so the two\n// walkers print the SAME path for the same cell by construction. Only the leaf\n// policy differs, on purpose: first-hit here, collect-all there.\n//\n// Scope: TEA state is plain JSON-shaped data (the substrate's __DEV__ guards\n// reject closures in Cmds, and states are conventionally serializable). So\n// the walk handles: primitives, null/undefined, arrays, and plain objects.\n// It treats other object kinds (Date, Map, Set, class instances) by reference\n// identity + a structural fallback via Object.entries — sufficient for the\n// JSON-shaped states this tool targets; a richer differ lives in ../devtools.\n\n/** A divergence record, or `null` when the two values are deeply equal. */\ntype Divergence = { path: string; expected: unknown; actual: unknown } | null;\n\nfunction firstDivergence(\n expected: unknown,\n actual: unknown,\n path: string,\n): Divergence {\n // Identity / primitive fast path. Handles same-reference, and equal\n // primitives. `Object.is` so NaN === NaN and +0 ≠ -0 (matches structural\n // equality intent better than ===).\n if (Object.is(expected, actual)) return null;\n\n const te = typeOf(expected);\n const ta = typeOf(actual);\n // Different kinds at this node (e.g. object vs array, number vs string,\n // present vs null) — this node IS the divergence.\n if (te !== ta) return { path, expected, actual };\n\n if (te === \"array\") {\n const ae = expected as unknown[];\n const aa = actual as unknown[];\n if (ae.length !== aa.length) {\n // Length mismatch reported at the array node itself — the shape differs,\n // which is more informative than an arbitrary element index.\n return { path, expected, actual };\n }\n for (let i = 0; i < ae.length; i++) {\n const d = firstDivergence(ae[i], aa[i], indexPath(path, i));\n if (d !== null) return d;\n }\n return null;\n }\n\n if (te === \"object\") {\n const oe = expected as Record<string, unknown>;\n const oa = actual as Record<string, unknown>;\n // Sorted union of keys for deterministic traversal. A key present in one\n // but absent in the other diverges at that child path (its value is\n // `undefined` on the missing side).\n for (const key of unionKeys(oe, oa)) {\n const d = firstDivergence(oe[key], oa[key], keyPath(path, key));\n if (d !== null) return d;\n }\n return null;\n }\n\n // Primitives (string/number/boolean/bigint/symbol/function) that failed the\n // Object.is fast path, or null/undefined that differ — this node diverges.\n return { path, expected, actual };\n}\n"]}
@@ -56,5 +56,5 @@ function nextDelayMs(state, policy, rng = defaultRng) {
56
56
  }
57
57
 
58
58
  export { asRng, backoffDelay, defaultRetryPolicy, defaultRng, initRetry, nextDelayMs, recordFailure, retryElapsedMs, shouldRetry };
59
- //# sourceMappingURL=chunk-Q2UM7BXU.js.map
60
- //# sourceMappingURL=chunk-Q2UM7BXU.js.map
59
+ //# sourceMappingURL=chunk-VBDU4JL6.js.map
60
+ //# sourceMappingURL=chunk-VBDU4JL6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/retry-backoff/index.ts"],"names":[],"mappings":"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","file":"chunk-VBDU4JL6.js","sourcesContent":["/**\n * @packageDocumentation\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 * the internal work queue (`src/internal/work-queue/ops.ts`): a state type\n * 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 * and the same wiring under a wall-clock outage budget, where the failure Msg\n * carries the instant it was observed:\n *\n * // policy: { baseMs: 250, factor: 2, capMs: 4_000, maxElapsedMs: 1_050_000, jitter: \"full\" }\n * const retry = recordFailure(state.retry, msg.error, msg.at);\n * if (!shouldRetry(retry, policy, msg.at)) return [toGaveUp(state, retryElapsedMs(retry, msg.at)), []];\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 * ── THE BOUND MAY BE A DURATION, NOT ONLY A COUNT ──────────────────────────\n * `maxAttempts` was the module's only terminal bound, and a count is the wrong\n * bound whenever this ladder nests inside somebody else's ladder. A downstream\n * consumer proved it in production: 4 restarts over a 250ms→4s ladder is ~3.75s\n * of backoff, which nested under a client's own ~2.5s ladder came to roughly\n * SIXTEEN SECONDS of total patience — against a peer that waits ~17.5 MINUTES\n * before it gives up on the other side. A routine deploy outran the budget and\n * killed 170 runs in 7 days that the peer would have resumed from a checkpoint.\n *\n * The failure a count cannot express is that the QUESTION is temporal. \"How\n * long do we tolerate an unreachable peer\" has an answer in seconds, usually\n * derived from the peer's own give-up window; what a count of attempts costs in\n * seconds depends on how long each attempt takes, which is the carrier's\n * business and not the policy's. So a policy may now declare `maxElapsedMs` —\n * a wall-clock outage budget measured from the FIRST failure of the streak —\n * instead of, or in addition to, `maxAttempts`. When both are declared, retry\n * continues only while EVERY declared bound still permits it.\n *\n * TIME IS AN INPUT, NOT AN AMBIENT READ (invariant 2). The streak's origin is\n * supplied by the caller to `recordFailure`, and `shouldRetry` is told `nowMs`;\n * this module still never reads a clock, exactly as it never reads the RNG.\n * The types make the wiring the only wiring that compiles: a duration-bounded\n * policy only satisfies the `shouldRetry` overload that demands a\n * {@link TimedRetryState} *and* a `nowMs`, so \"declared an outage budget, never\n * fed it a clock\" is a type error rather than a bound that quietly never fires.\n *\n * A policy with NO bound at all is likewise unrepresentable by accident — the\n * {@link RetryBudget} union has no \"neither\" arm; forever-retry must be spelled\n * `unbounded: true` ({@link UnboundedRetryPolicy}).\n *\n * Strengthens invariant 2 (transitions are pure — the clock enters as an\n * argument, never as a `Date.now()` inside the fold) and invariant 5\n * (composition is by reduction — the new bound is an additive arm on the policy\n * union and an optional argument on the verbs, so every existing count-bounded\n * call site keeps compiling untouched).\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 * The shape of the delay ladder, with no terminal bound attached — `baseMs`,\n * `factor`, `capMs`, `jitter`. Split out from the policy because the curve and\n * the bound answer different questions: the curve is \"how fast do we come\n * back\", the bound is \"when do we stop\". `backoffDelay` needs only the curve.\n */\nexport interface BackoffCurve {\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 /** Jitter strategy applied to the capped exponential delay. */\n readonly jitter: Jitter;\n}\n\n/**\n * Bound by ATTEMPT COUNT — the original bound, and the right one when each\n * attempt costs about the same and the caller is the outermost ladder.\n *\n * `maxElapsedMs: never` is what keeps this arm honest: a count-bounded policy\n * is *declared* to have no outage budget, so it satisfies the clock-free\n * `shouldRetry` overload. Add `maxElapsedMs` and it becomes a\n * {@link DurationRetryPolicy}, which no longer does.\n */\nexport interface CountBound {\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 readonly maxElapsedMs?: never;\n readonly unbounded?: never;\n}\n\n/**\n * Bound by WALL-CLOCK OUTAGE DURATION, optionally also by count.\n *\n * `maxElapsedMs` is measured from the FIRST failure of the current streak\n * ({@link TimedRetryState.firstFailureAtMs}), not from the last attempt: the\n * question it answers is \"how long has the far side been unreachable\", which is\n * the question a nesting caller's own patience is denominated in. Derive it\n * from the peer's give-up window rather than restating a guess — two sides\n * holding different answers to one ordered fact is the defect this bound\n * exists to prevent.\n *\n * `maxAttempts` stays available here, and when both are present retry continues\n * only while BOTH still permit it (see {@link shouldRetry}). Adding a count to a\n * duration budget is a belt-and-braces guard against a pathologically fast\n * failure loop, not a substitute for the duration.\n */\nexport interface DurationBound {\n /**\n * Outage budget in wall-clock milliseconds. `shouldRetry` permits another\n * attempt while `nowMs - firstFailureAtMs < maxElapsedMs`, however many\n * attempts that takes.\n */\n readonly maxElapsedMs: number;\n /** Optional secondary count bound; omitted means \"no count bound\". */\n readonly maxAttempts?: number;\n readonly unbounded?: never;\n}\n\n/**\n * No bound at all — retry forever. Deliberately verbose and deliberately the\n * only way to say it: {@link RetryBudget} has no \"neither bound declared\" arm,\n * so an infinite retry loop is something a policy author must write down, never\n * something a forgotten field produces.\n */\nexport interface Unbounded {\n readonly unbounded: true;\n readonly maxAttempts?: never;\n readonly maxElapsedMs?: never;\n}\n\n/**\n * The terminal bound a policy declares: a count, a duration (optionally with a\n * count), or an explicit opt-in to neither. Exactly one arm applies, and there\n * is no arm for \"I forgot\".\n */\nexport type RetryBudget = CountBound | DurationBound | Unbounded;\n\n/**\n * A retry policy is pure configuration — the shape of the backoff curve plus\n * the bound that ends the retrying. It carries no mutable state; `RetryState`\n * does. One policy is shared across all the attempts of one logical operation\n * (often a module-scope const).\n *\n * `RetryPolicy` is the COUNT-bounded shape, unchanged since the module's first\n * release: every existing `{ baseMs, factor, capMs, maxAttempts, jitter }`\n * literal still satisfies it, `policy.maxAttempts` is still a `number` (not\n * `number | undefined`), and `shouldRetry(state, policy)` still takes no clock.\n * Reach for {@link DurationRetryPolicy} when the bound should be temporal, or\n * {@link AnyRetryPolicy} when writing code generic over the bound.\n */\nexport type RetryPolicy = BackoffCurve & CountBound;\n\n/** A curve bounded by wall-clock outage duration (and optionally by count). */\nexport type DurationRetryPolicy = BackoffCurve & DurationBound;\n\n/** A curve with the explicit opt-in to unbounded retry. */\nexport type UnboundedRetryPolicy = BackoffCurve & Unbounded;\n\n/**\n * Any well-formed policy. The parameter type for code that must work whatever\n * bound the caller declared — `shouldRetry`'s implementation, wrappers, and\n * consumers that take a policy from configuration.\n */\nexport type AnyRetryPolicy = BackoffCurve & RetryBudget;\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: BackoffCurve,\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/**\n * A retry state that also knows WHEN its failure streak began — the origin a\n * duration bound is measured from. Minted by passing the observation instant to\n * {@link recordFailure}; there is no other way to obtain one, so a streak with\n * an outage budget but no origin is not a value that can be written down.\n *\n * The origin is the FIRST failure of the streak and is preserved across\n * subsequent failures. A success resets the streak the way it always did — by\n * returning to `initRetry()` — which drops the origin with it, so an operation\n * that fails occasionally never accumulates outage and pays nothing for a wide\n * `maxElapsedMs`; only an unbroken run of failures grows toward the bound.\n */\nexport interface TimedRetryState extends RetryState {\n /** When the streak's first failure was observed (caller's clock, epoch ms). */\n readonly firstFailureAtMs: number;\n}\n\n/** The starting state: zero attempts, no error yet, no streak. */\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 *\n * `atMs` is the instant the failure was OBSERVED, supplied by the caller (the\n * `interpret` handler owns the clock; this module never reads one — invariant\n * 2). Passing it starts the streak clock on the first failure and preserves the\n * original origin on every failure after it, yielding a {@link TimedRetryState}\n * that a duration-bounded policy can be evaluated against. Omit it and the\n * count-bounded behaviour is bit-for-bit what it always was.\n */\nexport function recordFailure(\n state: TimedRetryState,\n error: unknown,\n atMs?: number,\n): TimedRetryState;\nexport function recordFailure(\n state: RetryState,\n error: unknown,\n atMs: number,\n): TimedRetryState;\nexport function recordFailure(state: RetryState, error: unknown): RetryState;\nexport function recordFailure(\n state: RetryState & { readonly firstFailureAtMs?: number },\n error: unknown,\n atMs?: number,\n): RetryState | TimedRetryState {\n const attempt = state.attempt + 1;\n // The streak's origin is set once, by whichever failure started it.\n const firstFailureAtMs = state.firstFailureAtMs ?? atMs;\n return firstFailureAtMs === undefined\n ? { attempt, lastError: error }\n : { attempt, lastError: error, firstFailureAtMs };\n}\n\n/**\n * How long the current failure streak has lasted, in milliseconds. PURE.\n *\n * The honest number for a give-up log — a count of attempts says nothing about\n * how much patience it actually spent. Clamped at 0 so a caller's clock going\n * backwards (NTP step, a rehydrated state whose origin is from a different\n * host) reads as \"no outage yet\" rather than a negative duration.\n */\nexport function retryElapsedMs(state: TimedRetryState, nowMs: number): number {\n const elapsed = nowMs - state.firstFailureAtMs;\n return elapsed > 0 ? elapsed : 0;\n}\n\n/**\n * Whether another attempt is permitted under `policy`. PURE.\n *\n * Every bound the policy DECLARES must still permit the attempt; a bound it\n * does not declare is not consulted. Concretely:\n *\n * - `maxAttempts` — `attempt` is the number of failures recorded so far (= the\n * index of the next attempt); we may retry while that index is below\n * `maxAttempts`, i.e. attempts `0 .. maxAttempts-1` are allowed and the\n * `maxAttempts`-th is refused. Unchanged.\n * - `maxElapsedMs` — we may retry while `nowMs - firstFailureAtMs` is below\n * the budget, *however many attempts that takes*. A streak's first failure\n * measures a zero-length outage and is therefore always retried, unless the\n * budget is itself `0` (the \"no patience\" setting).\n * - `unbounded: true` — always permitted.\n *\n * The overloads are the wiring contract. A count-bounded or explicitly\n * unbounded policy needs no clock, so the two-argument call every existing\n * caller already writes keeps type-checking. A policy carrying `maxElapsedMs`\n * matches only the second overload, which demands a {@link TimedRetryState} and\n * a `nowMs` — so declaring an outage budget and then never feeding it a clock\n * fails to compile instead of silently producing a bound that never fires.\n */\nexport function shouldRetry(\n state: RetryState,\n policy: BackoffCurve & (CountBound | Unbounded),\n nowMs?: number,\n): boolean;\nexport function shouldRetry(\n state: TimedRetryState,\n policy: AnyRetryPolicy,\n nowMs: number,\n): boolean;\nexport function shouldRetry(\n state: RetryState & { readonly firstFailureAtMs?: number },\n policy: AnyRetryPolicy,\n nowMs?: number,\n): boolean {\n if (policy.maxAttempts !== undefined && state.attempt >= policy.maxAttempts) {\n return false;\n }\n if (policy.maxElapsedMs === undefined) return true;\n // Reachable only from JS callers or an `as`-cast: the overloads make the\n // origin and the clock mandatory whenever `maxElapsedMs` is declared. Fail to\n // the visibly-broken side — refuse the retry — rather than treating an\n // unmeasurable outage as \"still inside the budget\" and retrying forever.\n if (state.firstFailureAtMs === undefined || nowMs === undefined) return false;\n return nowMs - state.firstFailureAtMs < policy.maxElapsedMs;\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: BackoffCurve,\n rng: Rng = defaultRng,\n): number {\n return backoffDelay(state.attempt, policy, rng);\n}\n"]}
@@ -1,11 +1,28 @@
1
- import { idempotencyMemory } from './chunk-BYQUGDD3.js';
2
- import { queueAdapter } from './chunk-MU6MCIAC.js';
1
+ import { idempotencyMemory } from './chunk-PR6TB3BL.js';
2
+ import { queueAdapter } from './chunk-TXN2KFHF.js';
3
+ import { Cmd } from './chunk-66PY5WYF.js';
4
+ import { z } from 'zod';
3
5
 
4
- // src/idempotent-intake/index.ts
6
+ function intakeProcessDef() {
7
+ return Cmd.define("intake:process", {
8
+ input: z.custom(),
9
+ ok: z.void(),
10
+ err: []
11
+ });
12
+ }
13
+ function intakeReplayDef() {
14
+ return Cmd.define("intake:replay", {
15
+ input: z.custom(),
16
+ ok: z.void(),
17
+ err: []
18
+ });
19
+ }
5
20
  function createIntake(config) {
6
21
  const { keyOf, ttlMs } = config;
7
22
  const queue = queueAdapter();
8
23
  const memory = idempotencyMemory();
24
+ const intakeProcess = intakeProcessDef();
25
+ const intakeReplay = intakeReplayDef();
9
26
  function init() {
10
27
  return {
11
28
  seen: memory.init(),
@@ -28,7 +45,7 @@ function createIntake(config) {
28
45
  const entry = liveEntry(state.seen, key, at);
29
46
  if (entry !== void 0) {
30
47
  if (entry.phase === "done") {
31
- return [state, [{ type: "intake:replay", key, result: entry.result }]];
48
+ return [state, [intakeReplay({ key, result: entry.result })]];
32
49
  }
33
50
  return [state, []];
34
51
  }
@@ -40,10 +57,7 @@ function createIntake(config) {
40
57
  id
41
58
  );
42
59
  const nextState = { seen: seenNext, queue: queueNext };
43
- return [
44
- nextState,
45
- [{ type: "intake:process", key, itemId: item.id, payload }]
46
- ];
60
+ return [nextState, [intakeProcess({ key, itemId: item.id, payload })]];
47
61
  }
48
62
  function complete(state, key, result, at) {
49
63
  const seenNext = memory.remember(
@@ -86,6 +100,6 @@ function createIntake(config) {
86
100
  return { init, receive, complete, boot, claimNext };
87
101
  }
88
102
 
89
- export { createIntake };
90
- //# sourceMappingURL=chunk-ZVJZ72HY.js.map
91
- //# sourceMappingURL=chunk-ZVJZ72HY.js.map
103
+ export { createIntake, intakeProcessDef, intakeReplayDef };
104
+ //# sourceMappingURL=chunk-VTQI2NHP.js.map
105
+ //# sourceMappingURL=chunk-VTQI2NHP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/idempotency/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-VTQI2NHP.js","sourcesContent":["/**\n * internal/idempotency/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `../idempotency` and `../../work-queue`\n * and thread them by hand.\n */\n\nimport { z } from \"zod\";\nimport { Cmd, type CmdOf } from \"../../../index\";\nimport type { QueueItem } from \"../../work-queue\";\nimport { queueAdapter } from \"../../work-queue/adapter\";\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n//\n// Both are `Cmd.define`d (ADR 0014). The payload `P` / result `R` are the\n// knob's own type parameters, so each def is minted per knob by a factory\n// (`createIntake` calls it once) rather than a module-scope constant — the\n// same shape `resilient-call`'s `runCmdDef<I, R>()` takes. The host performs\n// them and settles nothing back through intake, hence `ok: z.void()` and an\n// empty `err` list.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport function intakeProcessDef<P>() {\n return Cmd.define(\"intake:process\", {\n input: z.custom<{\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n }>(),\n ok: z.void(),\n err: [],\n });\n}\nexport type IntakeProcessCmd<P> = CmdOf<ReturnType<typeof intakeProcessDef<P>>>;\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport function intakeReplayDef<R>() {\n return Cmd.define(\"intake:replay\", {\n input: z.custom<{\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n }>(),\n ok: z.void(),\n err: [],\n });\n}\nexport type IntakeReplayCmd<R> = CmdOf<ReturnType<typeof intakeReplayDef<R>>>;\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n // The two Cmd constructors, minted once for this knob's `P`/`R`.\n const intakeProcess = intakeProcessDef<P>();\n const intakeReplay = intakeReplayDef<R>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [intakeReplay({ key, result: entry.result })]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [nextState, [intakeProcess({ key, itemId: item.id, payload })]];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}