@demlik/tea 0.12.0 → 0.14.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 (368) 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-25LBDCBC.js → chunk-6IQPODUF.js} +2 -2
  5. package/dist/{chunk-25LBDCBC.js.map → chunk-6IQPODUF.js.map} +1 -1
  6. package/dist/chunk-AQ6ZIMEA.js +421 -0
  7. package/dist/chunk-AQ6ZIMEA.js.map +1 -0
  8. package/dist/{chunk-MUUROGIH.js → chunk-BLJ2JQSH.js} +62 -12
  9. package/dist/chunk-BLJ2JQSH.js.map +1 -0
  10. package/dist/{chunk-7SVIRMC6.js → chunk-F2US5BW6.js} +25 -5
  11. package/dist/chunk-F2US5BW6.js.map +1 -0
  12. package/dist/{batch-window/index.js → chunk-F7FMAWLA.js} +4 -8
  13. package/dist/chunk-F7FMAWLA.js.map +1 -0
  14. package/dist/{chunk-7V4EVLZA.js → chunk-HUKZQX6U.js} +20 -16
  15. package/dist/chunk-HUKZQX6U.js.map +1 -0
  16. package/dist/{chunk-EWJP3YYP.js → chunk-HWVW4AXC.js} +3 -3
  17. package/dist/chunk-HWVW4AXC.js.map +1 -0
  18. package/dist/{chunk-WLGO64WC.js → chunk-HYDWG35Q.js} +23 -6
  19. package/dist/chunk-HYDWG35Q.js.map +1 -0
  20. package/dist/{chunk-I5OV65MS.js → chunk-I5MQMJDW.js} +57 -7
  21. package/dist/chunk-I5MQMJDW.js.map +1 -0
  22. package/dist/{chunk-J6527VRQ.js → chunk-LFZUK6J2.js} +3 -3
  23. package/dist/chunk-LFZUK6J2.js.map +1 -0
  24. package/dist/{chunk-ZVJZ72HY.js → chunk-LYLTKJWM.js} +25 -11
  25. package/dist/chunk-LYLTKJWM.js.map +1 -0
  26. package/dist/{chunk-D736QUGF.js → chunk-MYXFNWAJ.js} +4 -5
  27. package/dist/chunk-MYXFNWAJ.js.map +1 -0
  28. package/dist/{chunk-K575PT4S.js → chunk-PR6TB3BL.js} +15 -4
  29. package/dist/chunk-PR6TB3BL.js.map +1 -0
  30. package/dist/chunk-RQULHGYH.js +1804 -0
  31. package/dist/chunk-RQULHGYH.js.map +1 -0
  32. package/dist/{chunk-FV3Z5MBE.js → chunk-TXN2KFHF.js} +14 -4
  33. package/dist/chunk-TXN2KFHF.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-Z2L3LSNV.js +1470 -0
  37. package/dist/chunk-Z2L3LSNV.js.map +1 -0
  38. package/dist/core-CTzMJ82M.d.ts +562 -0
  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 +48 -31
  43. package/dist/do/index.js +36 -25
  44. package/dist/do/index.js.map +1 -1
  45. package/dist/{durable-effects-D8SqU_if.d.ts → durable-effects-B0-qGQDy.d.ts} +2 -2
  46. package/dist/extension/index.d.ts +301 -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 +1295 -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/{batch-window/index.d.ts → index-4vZzljCD.d.ts} +7 -11
  55. package/dist/{fan-out/index.d.ts → index-6A7dB8f6.d.ts} +7 -8
  56. package/dist/index-B5BZ-_cT.d.ts +616 -0
  57. package/dist/index-Bc40JWVl.d.ts +131 -0
  58. package/dist/{debounce/index.d.ts → index-DHSUmQTR.d.ts} +7 -7
  59. package/dist/index-DJWIER_3.d.ts +69 -0
  60. package/dist/{snapshot/index.d.ts → index-DMCeMPMJ.d.ts} +15 -77
  61. package/dist/{monitored-run/index.d.ts → index-NjrT3fL2.d.ts} +37 -12
  62. package/dist/{recorder/index.d.ts → index-TvRb1KVf.d.ts} +4 -6
  63. package/dist/{deadline/index.d.ts → index-q0hXFeZ0.d.ts} +155 -8
  64. package/dist/index.d.ts +719 -6
  65. package/dist/index.js +1 -2
  66. package/dist/jev/index.d.ts +634 -0
  67. package/dist/jev/index.js +433 -0
  68. package/dist/jev/index.js.map +1 -0
  69. package/dist/machine-CdIcN2Xm.d.ts +1773 -0
  70. package/dist/machine-viz/index.d.ts +2 -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 +19 -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 +130 -4
  77. package/dist/node/index.js +222 -10
  78. package/dist/node/index.js.map +1 -1
  79. package/dist/{paginator → paginate}/index.d.ts +151 -6
  80. package/dist/paginate/index.js +7 -0
  81. package/dist/parity/index.d.ts +5 -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 +248 -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 -4
  88. package/dist/persistence/index.js +6 -0
  89. package/dist/react/index.d.ts +4 -3
  90. package/dist/react/index.js +1 -2
  91. package/dist/react/index.js.map +1 -1
  92. package/dist/resilience/index.d.ts +743 -0
  93. package/dist/resilience/index.js +657 -0
  94. package/dist/resilience/index.js.map +1 -0
  95. package/dist/retry-backoff/index.d.ts +3 -2
  96. package/dist/retry-backoff/index.js +1 -1
  97. package/dist/runtime-types-LWDoi943.d.ts +998 -0
  98. package/dist/testing/index.d.ts +3 -2
  99. package/dist/testing/index.js +1 -2
  100. package/dist/testing/index.js.map +1 -1
  101. package/dist/{throttled-input → timing}/index.d.ts +144 -12
  102. package/dist/{throttled-input → timing}/index.js +57 -12
  103. package/dist/timing/index.js.map +1 -0
  104. package/dist/{types-D7u9DkH7.d.ts → types-DP3f5NIl.d.ts} +1 -1
  105. package/dist/work-queue/index.d.ts +131 -54
  106. package/dist/work-queue/index.js +3 -2
  107. package/dist/work-queue/index.js.map +1 -1
  108. package/package.json +50 -216
  109. package/dist/authed-call/index.d.ts +0 -119
  110. package/dist/authed-call/index.js +0 -134
  111. package/dist/authed-call/index.js.map +0 -1
  112. package/dist/await-terminal/index.d.ts +0 -100
  113. package/dist/await-terminal/index.js +0 -76
  114. package/dist/await-terminal/index.js.map +0 -1
  115. package/dist/batch-window/index.js.map +0 -1
  116. package/dist/cache/index.d.ts +0 -151
  117. package/dist/cache/index.js +0 -8
  118. package/dist/chart/index.d.ts +0 -217
  119. package/dist/chart/index.js +0 -16
  120. package/dist/chart/index.js.map +0 -1
  121. package/dist/chart/inspect/index.d.ts +0 -85
  122. package/dist/chart/inspect/index.js +0 -5
  123. package/dist/chart/inspect/react.d.ts +0 -112
  124. package/dist/chart/inspect/react.js +0 -11
  125. package/dist/chart/inspect/react.js.map +0 -1
  126. package/dist/chart/inspect/styles.css +0 -286
  127. package/dist/chart/lane/index.d.ts +0 -361
  128. package/dist/chart/lane/index.js +0 -9
  129. package/dist/chart/lane/index.js.map +0 -1
  130. package/dist/chart/lane/react.d.ts +0 -83
  131. package/dist/chart/lane/react.js +0 -559
  132. package/dist/chart/lane/react.js.map +0 -1
  133. package/dist/chart/lane/server.d.ts +0 -148
  134. package/dist/chart/lane/server.js +0 -209
  135. package/dist/chart/lane/server.js.map +0 -1
  136. package/dist/chart/lane/styles.css +0 -498
  137. package/dist/chart/lane/viewer/assets/abnfDiagram-N423BO3Z-C729LI31.js +0 -1
  138. package/dist/chart/lane/viewer/assets/architectureDiagram-T3A2C74G-C729LI31.js +0 -1
  139. package/dist/chart/lane/viewer/assets/blockDiagram-VBNYF7ZC-C729LI31.js +0 -1
  140. package/dist/chart/lane/viewer/assets/c4Diagram-5PPSVZJV-C729LI31.js +0 -1
  141. package/dist/chart/lane/viewer/assets/chunk-2GRJ4B5K-HdDf8SI3.js +0 -1
  142. package/dist/chart/lane/viewer/assets/chunk-5RXB4S5H-CcNtpUd7.js +0 -231
  143. package/dist/chart/lane/viewer/assets/chunk-5VM5RSS4-BP4fRS_5.js +0 -15
  144. package/dist/chart/lane/viewer/assets/chunk-KBJHAD2P-DZOw7xU6.js +0 -1
  145. package/dist/chart/lane/viewer/assets/chunk-NSK5VX7P-CBCyTZMs.js +0 -128
  146. package/dist/chart/lane/viewer/assets/classDiagram-JCYQIIEL-C729LI31.js +0 -1
  147. package/dist/chart/lane/viewer/assets/classDiagram-v2-OCEON4UE-D4uAiFr8.js +0 -206
  148. package/dist/chart/lane/viewer/assets/cose-bilkent-JH36ORCC-DOwCrGXH.js +0 -1
  149. package/dist/chart/lane/viewer/assets/cynefinDiagram-MW4NZA55-C729LI31.js +0 -1
  150. package/dist/chart/lane/viewer/assets/dagre-K_PewWfS.js +0 -1
  151. package/dist/chart/lane/viewer/assets/dagre-VZM6K2ZE-831qZcA8.js +0 -4
  152. package/dist/chart/lane/viewer/assets/diagram-7IWD3JNH-C729LI31.js +0 -1
  153. package/dist/chart/lane/viewer/assets/diagram-B4RE2ZJO-C729LI31.js +0 -1
  154. package/dist/chart/lane/viewer/assets/diagram-LBJQPF4R-C729LI31.js +0 -1
  155. package/dist/chart/lane/viewer/assets/diagram-Q27KOJAE-C729LI31.js +0 -1
  156. package/dist/chart/lane/viewer/assets/diagram-UB23O5K3-C729LI31.js +0 -1
  157. package/dist/chart/lane/viewer/assets/ebnfDiagram-BXEA7PRR-C729LI31.js +0 -1
  158. package/dist/chart/lane/viewer/assets/erDiagram-JOGREHBK-C729LI31.js +0 -1
  159. package/dist/chart/lane/viewer/assets/flowDiagram-UKHOOZJN-C729LI31.js +0 -1
  160. package/dist/chart/lane/viewer/assets/ganttDiagram-PKOTCBZU-C729LI31.js +0 -1
  161. package/dist/chart/lane/viewer/assets/gitGraphDiagram-DS77QQ5N-C729LI31.js +0 -1
  162. package/dist/chart/lane/viewer/assets/index-9SkH5NA3.css +0 -1
  163. package/dist/chart/lane/viewer/assets/index-Bgt4sXpW.js +0 -195
  164. package/dist/chart/lane/viewer/assets/infoDiagram-6WML65LV-C729LI31.js +0 -1
  165. package/dist/chart/lane/viewer/assets/ishikawaDiagram-WSZJBQD7-C729LI31.js +0 -1
  166. package/dist/chart/lane/viewer/assets/journeyDiagram-NVQOT4AX-C729LI31.js +0 -1
  167. package/dist/chart/lane/viewer/assets/kanban-definition-27J2QSJJ-G7PaPzy-.js +0 -89
  168. package/dist/chart/lane/viewer/assets/katex-DOwCrGXH.js +0 -1
  169. package/dist/chart/lane/viewer/assets/mindmap-definition-FAOFIHXS-CGj9nrVk.js +0 -96
  170. package/dist/chart/lane/viewer/assets/path-fybaL0A-.js +0 -1
  171. package/dist/chart/lane/viewer/assets/pegDiagram-VL7TDLO6-C729LI31.js +0 -1
  172. package/dist/chart/lane/viewer/assets/pieDiagram-7S7Q4E2Y-C729LI31.js +0 -1
  173. package/dist/chart/lane/viewer/assets/quadrantDiagram-CIZ2JOQS-C729LI31.js +0 -1
  174. package/dist/chart/lane/viewer/assets/railroadDiagram-AXF67PYL-C729LI31.js +0 -1
  175. package/dist/chart/lane/viewer/assets/requirementDiagram-LRYGKXZP-C729LI31.js +0 -1
  176. package/dist/chart/lane/viewer/assets/sankeyDiagram-W5VNT64P-C729LI31.js +0 -1
  177. package/dist/chart/lane/viewer/assets/sequenceDiagram-SI44F4Z6-C729LI31.js +0 -1
  178. package/dist/chart/lane/viewer/assets/stateDiagram-OKZ733FA-B_88AzBS.js +0 -1
  179. package/dist/chart/lane/viewer/assets/stateDiagram-v2-UEYNNEHI-CR5gcgq6.js +0 -1
  180. package/dist/chart/lane/viewer/assets/swimlanes-SLNWSIFB-B4xK-rwg.js +0 -1
  181. package/dist/chart/lane/viewer/assets/swimlanesDiagram-ULZ7WXOC-C729LI31.js +0 -1
  182. package/dist/chart/lane/viewer/assets/timeline-definition-Z64GVDOM-CCv52MfY.js +0 -120
  183. package/dist/chart/lane/viewer/assets/vennDiagram-T6HMQDX7-C729LI31.js +0 -1
  184. package/dist/chart/lane/viewer/assets/wardleyDiagram-T6FBY63Y-C729LI31.js +0 -1
  185. package/dist/chart/lane/viewer/assets/xychartDiagram-ELKLHX3M-C729LI31.js +0 -1
  186. package/dist/chart/lane/viewer/index.html +0 -15
  187. package/dist/chart/report/index.d.ts +0 -116
  188. package/dist/chart/report/index.js +0 -32
  189. package/dist/chart/report/index.js.map +0 -1
  190. package/dist/chunk-2YFLOCWX.js +0 -15
  191. package/dist/chunk-2YFLOCWX.js.map +0 -1
  192. package/dist/chunk-3JU6657S.js +0 -372
  193. package/dist/chunk-3JU6657S.js.map +0 -1
  194. package/dist/chunk-4KTWLPL5.js +0 -33
  195. package/dist/chunk-4KTWLPL5.js.map +0 -1
  196. package/dist/chunk-5EHLEUTL.js +0 -686
  197. package/dist/chunk-5EHLEUTL.js.map +0 -1
  198. package/dist/chunk-5KJKJOFC.js +0 -758
  199. package/dist/chunk-5KJKJOFC.js.map +0 -1
  200. package/dist/chunk-7SVIRMC6.js.map +0 -1
  201. package/dist/chunk-7V4EVLZA.js.map +0 -1
  202. package/dist/chunk-A56OR5RF.js +0 -184
  203. package/dist/chunk-A56OR5RF.js.map +0 -1
  204. package/dist/chunk-AZZLE7XR.js +0 -62
  205. package/dist/chunk-AZZLE7XR.js.map +0 -1
  206. package/dist/chunk-B5VXOFMZ.js +0 -266
  207. package/dist/chunk-B5VXOFMZ.js.map +0 -1
  208. package/dist/chunk-BYQUGDD3.js +0 -16
  209. package/dist/chunk-BYQUGDD3.js.map +0 -1
  210. package/dist/chunk-D736QUGF.js.map +0 -1
  211. package/dist/chunk-E6GEDSF4.js +0 -307
  212. package/dist/chunk-E6GEDSF4.js.map +0 -1
  213. package/dist/chunk-EUF7XOEE.js +0 -223
  214. package/dist/chunk-EUF7XOEE.js.map +0 -1
  215. package/dist/chunk-EWJP3YYP.js.map +0 -1
  216. package/dist/chunk-FV3Z5MBE.js.map +0 -1
  217. package/dist/chunk-HCEVHFJ7.js +0 -54
  218. package/dist/chunk-HCEVHFJ7.js.map +0 -1
  219. package/dist/chunk-I5OV65MS.js.map +0 -1
  220. package/dist/chunk-J6527VRQ.js.map +0 -1
  221. package/dist/chunk-JELSZN25.js +0 -47
  222. package/dist/chunk-JELSZN25.js.map +0 -1
  223. package/dist/chunk-JER5O7PQ.js +0 -416
  224. package/dist/chunk-JER5O7PQ.js.map +0 -1
  225. package/dist/chunk-JGVL3WPL.js +0 -26
  226. package/dist/chunk-JGVL3WPL.js.map +0 -1
  227. package/dist/chunk-JP5D6GSL.js +0 -71
  228. package/dist/chunk-JP5D6GSL.js.map +0 -1
  229. package/dist/chunk-K575PT4S.js.map +0 -1
  230. package/dist/chunk-MU6MCIAC.js +0 -15
  231. package/dist/chunk-MU6MCIAC.js.map +0 -1
  232. package/dist/chunk-MUUROGIH.js.map +0 -1
  233. package/dist/chunk-ND3DH6KM.js +0 -88
  234. package/dist/chunk-ND3DH6KM.js.map +0 -1
  235. package/dist/chunk-Q2UM7BXU.js.map +0 -1
  236. package/dist/chunk-S2EGRPCM.js +0 -47
  237. package/dist/chunk-S2EGRPCM.js.map +0 -1
  238. package/dist/chunk-SINXE47Y.js +0 -55
  239. package/dist/chunk-SINXE47Y.js.map +0 -1
  240. package/dist/chunk-SUST46KO.js +0 -53
  241. package/dist/chunk-SUST46KO.js.map +0 -1
  242. package/dist/chunk-TP5UPGLY.js +0 -61
  243. package/dist/chunk-TP5UPGLY.js.map +0 -1
  244. package/dist/chunk-UF4MZSLT.js +0 -53
  245. package/dist/chunk-UF4MZSLT.js.map +0 -1
  246. package/dist/chunk-UOC2PL46.js +0 -13
  247. package/dist/chunk-UOC2PL46.js.map +0 -1
  248. package/dist/chunk-WLGO64WC.js.map +0 -1
  249. package/dist/chunk-WRAA7GVZ.js +0 -209
  250. package/dist/chunk-WRAA7GVZ.js.map +0 -1
  251. package/dist/chunk-ZCL7SCBK.js +0 -685
  252. package/dist/chunk-ZCL7SCBK.js.map +0 -1
  253. package/dist/chunk-ZD6PNZBK.js +0 -457
  254. package/dist/chunk-ZD6PNZBK.js.map +0 -1
  255. package/dist/chunk-ZF6WDK2D.js +0 -519
  256. package/dist/chunk-ZF6WDK2D.js.map +0 -1
  257. package/dist/chunk-ZJ5AOYBH.js +0 -8
  258. package/dist/chunk-ZJ5AOYBH.js.map +0 -1
  259. package/dist/chunk-ZVJZ72HY.js.map +0 -1
  260. package/dist/circuit-breaker/index.d.ts +0 -197
  261. package/dist/circuit-breaker/index.js +0 -3
  262. package/dist/circuit-breaker/index.js.map +0 -1
  263. package/dist/core-Bjc0bS8j.d.ts +0 -326
  264. package/dist/deadline/index.js +0 -6
  265. package/dist/deadline/index.js.map +0 -1
  266. package/dist/debounce/index.js +0 -3
  267. package/dist/debounce/index.js.map +0 -1
  268. package/dist/extension/react.d.ts +0 -121
  269. package/dist/extension/react.js +0 -75
  270. package/dist/extension/react.js.map +0 -1
  271. package/dist/extension/subs/index.d.ts +0 -44
  272. package/dist/extension/subs/index.js +0 -80
  273. package/dist/extension/subs/index.js.map +0 -1
  274. package/dist/extension/test-utils.d.ts +0 -128
  275. package/dist/extension/test-utils.js +0 -277
  276. package/dist/extension/test-utils.js.map +0 -1
  277. package/dist/fan-out/index.js +0 -5
  278. package/dist/fan-out/index.js.map +0 -1
  279. package/dist/fold-Blbhru48.d.ts +0 -435
  280. package/dist/graph-CcjWZVrd.d.ts +0 -923
  281. package/dist/idempotency/adapter.d.ts +0 -62
  282. package/dist/idempotency/adapter.js +0 -4
  283. package/dist/idempotency/adapter.js.map +0 -1
  284. package/dist/idempotent-intake/index.d.ts +0 -187
  285. package/dist/idempotent-intake/index.js +0 -7
  286. package/dist/idempotent-intake/index.js.map +0 -1
  287. package/dist/llm-call/index.d.ts +0 -310
  288. package/dist/llm-call/index.js +0 -16
  289. package/dist/llm-call/index.js.map +0 -1
  290. package/dist/machine-0OmPvsy_.d.ts +0 -804
  291. package/dist/monitored-run/index.js +0 -10
  292. package/dist/monitored-run/index.js.map +0 -1
  293. package/dist/paginated-walk/index.d.ts +0 -234
  294. package/dist/paginated-walk/index.js +0 -16
  295. package/dist/paginated-walk/index.js.map +0 -1
  296. package/dist/paginator/index.js +0 -3
  297. package/dist/paginator/index.js.map +0 -1
  298. package/dist/pbt/arbitraries/index.d.ts +0 -135
  299. package/dist/pbt/arbitraries/index.js +0 -3
  300. package/dist/pbt/arbitraries/index.js.map +0 -1
  301. package/dist/pbt/runners/index.d.ts +0 -117
  302. package/dist/pbt/runners/index.js +0 -5
  303. package/dist/pbt/runners/index.js.map +0 -1
  304. package/dist/poller/index.d.ts +0 -351
  305. package/dist/poller/index.js +0 -115
  306. package/dist/poller/index.js.map +0 -1
  307. package/dist/prediction/index.d.ts +0 -130
  308. package/dist/prediction/index.js +0 -4
  309. package/dist/prediction/index.js.map +0 -1
  310. package/dist/protocol-B5YmJr0d.d.ts +0 -45
  311. package/dist/pure/index.d.ts +0 -2
  312. package/dist/pure/index.js +0 -4
  313. package/dist/pure/index.js.map +0 -1
  314. package/dist/rate-limit/index.d.ts +0 -115
  315. package/dist/rate-limit/index.js +0 -3
  316. package/dist/rate-limit/index.js.map +0 -1
  317. package/dist/reconciler/index.d.ts +0 -192
  318. package/dist/reconciler/index.js +0 -164
  319. package/dist/reconciler/index.js.map +0 -1
  320. package/dist/recorder/index.js +0 -3
  321. package/dist/recorder/index.js.map +0 -1
  322. package/dist/reducer-BqyoVyBG.d.ts +0 -447
  323. package/dist/resilient-call/index.d.ts +0 -319
  324. package/dist/resilient-call/index.js +0 -14
  325. package/dist/resilient-call/index.js.map +0 -1
  326. package/dist/retry-to-success/index.d.ts +0 -109
  327. package/dist/retry-to-success/index.js +0 -37
  328. package/dist/retry-to-success/index.js.map +0 -1
  329. package/dist/run-BVeqJDBF.d.ts +0 -713
  330. package/dist/runtime-types-Bxc637bp.d.ts +0 -502
  331. package/dist/saga/index.d.ts +0 -240
  332. package/dist/saga/index.js +0 -150
  333. package/dist/saga/index.js.map +0 -1
  334. package/dist/samples-DCt6nF8H.d.ts +0 -59
  335. package/dist/snapshot/index.js +0 -5
  336. package/dist/snapshot/index.js.map +0 -1
  337. package/dist/subs/index.d.ts +0 -471
  338. package/dist/subs/index.js +0 -7
  339. package/dist/subs/index.js.map +0 -1
  340. package/dist/throttle/index.d.ts +0 -134
  341. package/dist/throttle/index.js +0 -3
  342. package/dist/throttle/index.js.map +0 -1
  343. package/dist/throttled-input/index.js.map +0 -1
  344. package/dist/token-refresh/index.d.ts +0 -215
  345. package/dist/token-refresh/index.js +0 -6
  346. package/dist/token-refresh/index.js.map +0 -1
  347. package/dist/trace-replay/index.js +0 -6
  348. package/dist/trace-replay/index.js.map +0 -1
  349. package/dist/with-deadline/index.d.ts +0 -196
  350. package/dist/with-deadline/index.js +0 -111
  351. package/dist/with-deadline/index.js.map +0 -1
  352. package/dist/with-resilience/index.d.ts +0 -224
  353. package/dist/with-resilience/index.js +0 -240
  354. package/dist/with-resilience/index.js.map +0 -1
  355. package/dist/with-telemetry/index.d.ts +0 -149
  356. package/dist/with-telemetry/index.js +0 -70
  357. package/dist/with-telemetry/index.js.map +0 -1
  358. package/dist/work-queue/adapter.d.ts +0 -83
  359. package/dist/work-queue/adapter.js +0 -4
  360. package/dist/work-queue/adapter.js.map +0 -1
  361. package/dist/work-queue/ops.d.ts +0 -70
  362. package/dist/work-queue/ops.js +0 -3
  363. package/dist/work-queue/ops.js.map +0 -1
  364. package/dist/workflow/index.d.ts +0 -484
  365. package/dist/workflow/index.js +0 -244
  366. package/dist/workflow/index.js.map +0 -1
  367. /package/dist/{cache → paginate}/index.js.map +0 -0
  368. /package/dist/{chart/inspect → persistence}/index.js.map +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/pbt/runners/replay-fold.ts","../src/pbt/runners/property.ts"],"names":[],"mappings":"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","file":"chunk-JP5D6GSL.js","sourcesContent":["// ---------------------------------------------------------------------------\n// `foldEvents` — pure deterministic fold over a Msg sequence.\n//\n// PBT needs the per-step (prev/msg/next/cmds) structure to assert\n// step-shaped invariants. The substrate's `replay(machine, opts)` returns\n// only final state + cumulative cmds — insufficient. We use `replay` for\n// the boot step (calls `machine.init(loaded, ctx)` faithfully) then walk\n// msgs ourselves through `applyCell`, the substrate's own cell dispatch.\n//\n// Why walk cells directly instead of looping `replay` once per msg with\n// `loaded: prevState`? Because `init(loaded)` runs every call, and on\n// machines with non-trivial init transforms (e.g. the audit machine's\n// `auditing → attach_debugger` resume cell) that would either (a) re-fire\n// the transform on every step, or (b) require fragile cmd-subtraction to\n// isolate this step's cmds. Calling the cells directly keeps the fold pure\n// and the cmds per-step exactly.\n//\n// Dispatches through `applyCell` — the single primitive `run`/`replay` use —\n// so form classification agrees with production by construction (#275).\n//\n// `foldEvents` does NOT call `interpret`, does NOT touch a Store, does NOT\n// start Subs. The reducer surface is the contract.\n// ---------------------------------------------------------------------------\n\nimport {\n applyCell,\n type Cmd,\n type Machine,\n replay,\n type Sub,\n} from \"../../index\";\n\n/**\n * One unit in a fold trace — the four pieces of data a property\n * invariant typically asserts on. `prev` is the state before the msg\n * applied; `next` is the state after; `cmds` is the cmds emitted by this\n * single step (NOT the running total).\n */\nexport interface Step<S, M, C> {\n readonly prev: S;\n readonly msg: M;\n readonly next: S;\n readonly cmds: readonly C[];\n}\n\n/**\n * Fold a Msg sequence through a machine's pure reducer, returning the\n * per-step trace, every state in order, and the final state. Initial\n * state comes from `replay(machine, { msgs: [], ctx, loaded })` — i.e.\n * `machine.init(loaded ?? null, ctx)` faithfully. Init's emitted cmds\n * are NOT included in any `Step` (they're the boot moment, not a Msg).\n *\n * @param machine The machine under test.\n * @param ctx The Ctx — typically `stubCtxThrowingProxy<Ctx>()` when\n * init is pure. Pass a real ctx if init reads from it.\n * @param loaded Optional pre-init snapshot (the `loaded` arg to\n * `machine.init`). Defaults to `null` (fresh boot).\n * @param msgs The Msg sequence to fold.\n *\n * @example\n * const { states, steps, finalState } = foldEvents(machine, ctx, null, [\n * { type: \"start\" }, { type: \"tick\" }, { type: \"stop\" },\n * ]);\n * // steps[1] = { prev: { type: \"counting\", n: 0 }, msg: { type: \"tick\" }, ... }\n */\nexport function foldEvents<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n loaded: NoInfer<S> | null,\n msgs: readonly NoInfer<M>[],\n): {\n readonly states: readonly S[];\n readonly steps: readonly Step<S, M, C>[];\n readonly finalState: S;\n} {\n // Boot through `replay` so `init` runs faithfully and we honor the\n // substrate's loaded-snapshot contract (init may transform the loaded\n // value before the first Msg lands — e.g. the audit machine flips\n // `debuggerAttached: false` on resume).\n const { state: initialState } = replay(machine, {\n msgs: [],\n ctx,\n loaded: loaded ?? null,\n });\n\n // Per-msg dispatch goes through `applyCell` — the same primitive `run` and\n // `replay` use — so this runner classifies the update form identically to\n // production (`formOf` honors the `__form` tag; #275).\n const states: S[] = [initialState];\n const steps: Step<S, M, C>[] = [];\n let current = initialState;\n\n for (const msg of msgs) {\n const [next, emitted] = applyCell<S, M, C>(machine, current, msg);\n steps.push({ prev: current, msg, next, cmds: [...emitted] });\n states.push(next);\n current = next;\n }\n\n return { states, steps, finalState: current };\n}\n","// ---------------------------------------------------------------------------\n// Property runners — three thin wrappers over `fc.assert(fc.property(...))`\n// that fold a generated Msg sequence through the machine via `foldEvents`\n// and pipe the result into a TEA-shaped predicate.\n//\n// - `propertyTerminates` — every sequence ends in a terminal state.\n// - `propertyInvariant` — predicate holds on every (prev, msg, next, cmds) step.\n// - `propertyTrace` — predicate holds on the full (trace, final) shape.\n//\n// Each runner accepts `numRuns` (passed through to fast-check), an optional\n// `loaded` snapshot (forwarded to `foldEvents`), and the Msg-sequence\n// arbitrary. They return `void`; failures throw via fast-check's standard\n// shrinking output.\n//\n// Default shrinker is fast-check's built-in (open question 4): drop-msg /\n// prefix shrinkers can be layered later but are opt-in, not default. The\n// built-in shrinker handles array-shape shrinking + per-element variant\n// shrinking automatically.\n// ---------------------------------------------------------------------------\n\nimport * as fc from \"fast-check\";\nimport type { Cmd, Machine, Sub } from \"../../index\";\nimport { foldEvents, type Step } from \"./replay-fold\";\n\n/** Common options shared by every property runner. */\ninterface PropertyOpts<S> {\n /** Pre-init snapshot — passed to `machine.init(loaded, ctx)`. Default `null`. */\n readonly loaded?: S | null;\n /** Number of generated sequences to test. Forwarded to `fc.assert`. */\n readonly numRuns?: number;\n /** Optional fast-check seed for reproducibility. */\n readonly seed?: number;\n}\n\n/** The `foldEvents` result a runner's `check` inspects. */\ntype FoldResult<S, M, C> = {\n readonly states: readonly S[];\n readonly steps: readonly Step<S, M, C>[];\n readonly finalState: S;\n};\n\n/**\n * Build the fast-check parameters shared by every runner: the 200-default\n * `numRuns` plus an optional `seed`. Single source of truth so a change to\n * the default lands in one place.\n */\nfunction resolveFcParams<M>(\n opts: PropertyOpts<unknown> | undefined,\n): fc.Parameters<[readonly M[]]> {\n return {\n numRuns: opts?.numRuns ?? 200,\n ...(opts?.seed !== undefined ? { seed: opts.seed } : {}),\n };\n}\n\n/**\n * The fold+assert core every runner shares: assert `check` over each\n * generated Msg sequence, folded once through the machine via `foldEvents`.\n * `check` throws on failure so the error message carries the offending\n * shape into fast-check's shrunk counter-example.\n */\nfunction runProperty<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: Ctx,\n seqArb: fc.Arbitrary<readonly M[]>,\n opts: PropertyOpts<S> | undefined,\n check: (result: FoldResult<S, M, C>) => void,\n): void {\n fc.assert(\n fc.property(seqArb, (msgs) => {\n check(foldEvents(machine, ctx, opts?.loaded ?? null, msgs));\n }),\n resolveFcParams<M>(opts),\n );\n}\n\n/**\n * Assert every generated Msg sequence ends in a terminal state. `terminal`\n * is the predicate that names the acceptable final states (often\n * `(s) => s.type === \"done\" || s.type === \"idle\"`).\n *\n * The \"every sequence\" property catches stuck states (machine gets to a\n * non-terminal state and no further msg can escape) AND silent loops\n * (every msg variant is a noop from a given state).\n *\n * @example\n * propertyTerminates(\n * auditBackgroundMachine,\n * stubCtxThrowingProxy<BackgroundCtx>(),\n * arbMsgSequence(msgArb, { minLength: 1, maxLength: 30 }),\n * (s) => s.type === \"done\" || s.type === \"idle\",\n * { loaded: auditingState, numRuns: 500 },\n * );\n */\nexport function propertyTerminates<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n seqArb: fc.Arbitrary<readonly NoInfer<M>[]>,\n terminal: (state: NoInfer<S>) => boolean,\n opts?: NoInfer<PropertyOpts<S>>,\n): void {\n runProperty(machine, ctx, seqArb, opts, ({ finalState }) => {\n if (!terminal(finalState)) {\n // Throwing here (vs returning false) lets the error message carry\n // the offending final state — fast-check pretty-prints it inside\n // the shrunk counter-example.\n throw new Error(\n `propertyTerminates: final state did not match terminal predicate. ` +\n `Final: ${JSON.stringify(finalState)}`,\n );\n }\n });\n}\n\n/**\n * Assert a predicate holds on EVERY transition step in a generated\n * sequence. The predicate sees `{ prev, msg, next, cmds }` per step;\n * return true to accept, false (or throw) to fail.\n *\n * Use to pin a \"when X happens in state Y, cmd Z must be emitted\" rule.\n * The shrinker minimizes the offending sequence to the smallest Msg list\n * that still violates the predicate.\n *\n * @example\n * // Pin the resume-handshake fix: `ws:client:hello` in auditing must\n * // emit `send_ws { client:ready }`.\n * propertyInvariant(\n * auditBackgroundMachine, ctx,\n * arbMsgSequence(msgArb, { prefix: [{ type: \"ws:client:hello\" }] }),\n * ({ prev, msg, cmds }) =>\n * !(prev.type === \"auditing\" && msg.type === \"ws:client:hello\") ||\n * cmds.some((c) => c.type === \"send_ws\" && c.msg.type === \"client:ready\"),\n * { loaded: auditingState, numRuns: 200 },\n * );\n */\nexport function propertyInvariant<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n seqArb: fc.Arbitrary<readonly NoInfer<M>[]>,\n invariant: (step: Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>) => boolean,\n opts?: NoInfer<PropertyOpts<S>>,\n): void {\n runProperty(machine, ctx, seqArb, opts, ({ steps }) => {\n for (const step of steps) {\n if (!invariant(step)) {\n throw new Error(\n `propertyInvariant: failed at step msg=${JSON.stringify(step.msg)} ` +\n `prev=${JSON.stringify(step.prev)} next=${JSON.stringify(step.next)} ` +\n `cmds=${JSON.stringify(step.cmds)}`,\n );\n }\n }\n });\n}\n\n/**\n * Assert a predicate over the full trace plus the final state. Use when\n * the property is \"some Msg appeared in the trace before reaching the\n * final state\" or \"no two consecutive steps had the same state.type\" —\n * predicates that need the full sequence, not a single step.\n *\n * @example\n * propertyTrace(\n * machine, ctx, seqArb,\n * (steps, final) =>\n * final.type !== \"done\" ||\n * steps.some((step) => step.msg.type === \"ws:audit:done\"),\n * { loaded: auditingState, numRuns: 500 },\n * );\n */\nexport function propertyTrace<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n ctx: NoInfer<Ctx>,\n seqArb: fc.Arbitrary<readonly NoInfer<M>[]>,\n predicate: (\n steps: readonly Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>[],\n finalState: NoInfer<S>,\n ) => boolean,\n opts?: NoInfer<PropertyOpts<S>>,\n): void {\n runProperty(machine, ctx, seqArb, opts, ({ steps, finalState }) => {\n if (!predicate(steps, finalState)) {\n throw new Error(\n `propertyTrace: predicate failed on trace length=${steps.length}, ` +\n `final=${JSON.stringify(finalState)}`,\n );\n }\n });\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/idempotency/index.ts"],"names":[],"mappings":";AAuFO,SAAS,UAAa,IAAA,EAGL;AACtB,EAAA,OAAO;AAAA,IACL,SAAS,EAAC;AAAA,IACV,OAAO,EAAC;AAAA,IACR,UAAU,IAAA,EAAM,QAAA;AAAA,IAChB,OAAO,IAAA,EAAM;AAAA,GACf;AACF;AAUA,SAAS,SAAA,CACP,KAAA,EACA,KAAA,EACA,KAAA,EACS;AACT,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,KAAA,GAAQ,MAAM,IAAA,IAAQ,KAAA;AAC/B;AAQO,SAAS,IAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAA,EAAO,MAAM,KAAK,CAAA;AAC7C;AASO,SAAS,MAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAA,EAAO,MAAM,KAAK,CAAA;AAC5D,IAAA,OAAO,MAAA;AACT,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAQO,SAAS,YAAA,CACd,OACA,KAAA,EACqB;AACrB,EAAA,IAAI,KAAA,CAAM,KAAA,KAAU,MAAA,EAAW,OAAO,KAAA;AACtC,EAAA,MAAM,cAAmD,EAAC;AAC1D,EAAA,MAAM,YAAsB,EAAC;AAG7B,EAAA,KAAA,MAAW,GAAA,IAAO,MAAM,KAAA,EAAO;AAC7B,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,IAAA,IAAI,UAAU,MAAA,EAAW;AACzB,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAA,EAAO,KAAA,CAAM,KAAK,CAAA,EAAG;AAC1C,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AACnB,IAAA,SAAA,CAAU,KAAK,GAAG,CAAA;AAAA,EACpB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAa,OAAO,SAAA,EAAU;AAC5D;AAoBO,SAAS,QAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,KAAA,EACqB;AAErB,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,KAAA,EAAO,KAAK,CAAA;AAKxC,EAAA,MAAM,WAAA,GAAmD;AAAA,IACvD,GAAG,MAAA,CAAO;AAAA,GACZ;AACA,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,MAAM,KAAA,EAAM;AACxC,EAAA,MAAM,YAAY,MAAA,CAAO,KAAA,CAAM,OAAO,CAAC,CAAA,KAAM,MAAM,GAAG,CAAA;AACtD,EAAA,SAAA,CAAU,KAAK,GAAG,CAAA;AAKlB,EAAA,IAAI,KAAA,CAAM,aAAa,MAAA,EAAW;AAChC,IAAA,OAAO,SAAA,CAAU,MAAA,GAAS,KAAA,CAAM,QAAA,EAAU;AACxC,MAAA,MAAM,MAAA,GAAS,UAAU,KAAA,EAAM;AAC/B,MAAA,IAAI,WAAW,MAAA,EAAW;AAC1B,MAAA,OAAO,YAAY,MAAM,CAAA;AAAA,IAC3B;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAa,OAAO,SAAA,EAAU;AAC5D","file":"chunk-K575PT4S.js","sourcesContent":["/**\n * @demlik/tea/idempotency — dedupe-by-key + last-result cache as pure state + ops.\n *\n * Same shape as `@demlik/tea/work-queue`'s `ops.ts` and `@demlik/tea/rate-limit`:\n * a state type plus pure transition functions. Host-agnostic by construction —\n * no `Date.now()`, no timers, no I/O. The caller injects the clock (`nowMs`)\n * and wires the result into its own reducer / interpret. This keeps the store\n * inside TEA's invariant 2 (transitions are pure: same inputs → same outputs,\n * no `Date.now`) so a consumer can fold it through `update` and exercise it\n * with `replay` / the pbt fold without a fake clock.\n *\n * Every op returns a NEW store value and never mutates its input (invariant 1 —\n * state is a value, not a place). `seen` / `recall` are read-shaped predicates;\n * `remember` / `evictExpired` return a new store.\n *\n * The use case: a webhook receiver, a retried queue message, or a repeated user\n * action arrives carrying an idempotency key. `seen(store, key, now)` answers\n * \"did I already process this?\"; `recall(store, key, now)` returns the cached\n * result so the duplicate request gets the SAME response the original produced\n * rather than re-running the side effect. `remember(store, key, value, now)`\n * records the key + its result after the first run.\n *\n * Two eviction policies keep the store bounded, both injected at `initStore`:\n *\n * - **`ttlMs`** — an entry expires `ttlMs` after the `atMs` it was remembered\n * at. `seen` / `recall` treat an expired entry as absent; `evictExpired`\n * (and every `remember`) physically drops it. Without a TTL, entries live\n * until evicted by capacity. The cutoff is half-open: an entry exactly\n * `ttlMs` old is expired (`age >= ttlMs` drops it), matching `rate-limit`'s\n * window edge — a key remembered at `t` no longer counts at `t + ttlMs`.\n * - **`capacity`** — at most `capacity` entries are retained. When a\n * `remember` would push the count past `capacity` (after expiry eviction),\n * the OLDEST entries by insertion `order` are dropped until the count fits.\n * Without a capacity, the store grows unbounded (TTL alone bounds it).\n *\n * `order` is the insertion-order index that capacity eviction reads — the keys\n * oldest-first. It is the store's own LRU-by-insertion ledger, kept in lockstep\n * with `entries` so `entries` never needs to be sorted to find the oldest.\n *\n * **Overwrite semantics** (decided + pinned by tests): re-`remember`ing an\n * existing key is an UPDATE, not a no-op. It (1) overwrites `value`, (2)\n * refreshes `atMs` to the new `nowMs` — so the TTL clock restarts and a\n * frequently-seen key never expires — and (3) moves the key to the NEWEST\n * position in `order` — so capacity eviction treats a re-remembered key as\n * fresh, not as a stale candidate for eviction. This makes `remember` behave\n * like \"touch\": a key actively being re-seen is kept alive on both the TTL and\n * capacity axes.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/idempotency`\n * subpath, same one-shape-per-package rule as `work-queue`, `rate-limit`, and\n * `do`.\n */\n\n/**\n * A cached entry: the result `value` the original request produced and the\n * clock reading `atMs` it was remembered at. `atMs` is the TTL anchor — expiry\n * is measured from here, and a re-`remember` refreshes it. Both fields are\n * plain data so the whole store is trivially JSON-serializable into a\n * consumer's `Store<S>`.\n */\nexport interface IdempotencyEntry<V> {\n readonly value: V;\n readonly atMs: number;\n}\n\n/**\n * The dedupe store: a map of `key → entry`, an insertion-`order` ledger\n * (oldest-first) that capacity eviction reads, and the two optional bounds.\n *\n * `entries` and `order` are kept in lockstep — every key in `order` has an\n * entry and vice versa. `capacity` / `ttlMs` are carried on the store (not\n * passed per-op) so a single value fully describes the policy; a consumer\n * persists the store and rehydrates the same bounds without re-deriving them.\n */\nexport interface IdempotencyStore<V> {\n readonly entries: Readonly<Record<string, IdempotencyEntry<V>>>;\n readonly order: readonly string[];\n readonly capacity?: number;\n readonly ttlMs?: number;\n}\n\n/**\n * Create an empty store. `capacity` and `ttlMs` are both optional — omit both\n * for an unbounded cache, supply either or both to bound by count, age, or\n * both. Stored on the returned value so every later op reads the policy off\n * the store rather than from a re-passed argument.\n */\nexport function initStore<V>(opts?: {\n capacity?: number;\n ttlMs?: number;\n}): IdempotencyStore<V> {\n return {\n entries: {},\n order: [],\n capacity: opts?.capacity,\n ttlMs: opts?.ttlMs,\n };\n}\n\n/**\n * Whether `entry` (remembered at `entry.atMs`) is expired at `nowMs` under\n * `ttlMs`. Half-open: an entry exactly `ttlMs` old is expired (`age >= ttlMs`),\n * matching `rate-limit`'s window-edge cutoff. A `nowMs` before `atMs` (clock\n * skewed backwards) yields a negative age — never expired, the safe direction\n * (a duplicate is still deduped rather than re-run). When `ttlMs` is undefined\n * the entry never expires by age.\n */\nfunction isExpired<V>(\n entry: IdempotencyEntry<V>,\n nowMs: number,\n ttlMs: number | undefined,\n): boolean {\n if (ttlMs === undefined) return false;\n return nowMs - entry.atMs >= ttlMs;\n}\n\n/**\n * True iff `key` is present AND not expired at `nowMs`. This is the dedupe\n * predicate: a webhook / retried message / repeated action whose key returns\n * `true` has already been processed and should be short-circuited (paired with\n * `recall` to return the original result). PURE — reads only, never mutates.\n */\nexport function seen<V>(\n store: IdempotencyStore<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = store.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs, store.ttlMs);\n}\n\n/**\n * The cached `value` for `key` if present and unexpired at `nowMs`, else\n * `undefined`. Use after `seen` returns `true` to replay the original result to\n * a duplicate request. An expired entry reads as absent (returns `undefined`)\n * even though it is physically still in `entries` until the next `remember` /\n * `evictExpired` drops it. PURE — reads only, never mutates.\n */\nexport function recall<V>(\n store: IdempotencyStore<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = store.entries[key];\n if (entry === undefined || isExpired(entry, nowMs, store.ttlMs))\n return undefined;\n return entry.value;\n}\n\n/**\n * Drop every entry expired at `nowMs` (older than `ttlMs`). Returns a NEW store\n * with the survivors; `order` is rebuilt to drop the same keys, preserving the\n * relative insertion order of the survivors. A no-TTL store returns an\n * equivalent store (nothing ages out). PURE — the input store is never mutated.\n */\nexport function evictExpired<V>(\n store: IdempotencyStore<V>,\n nowMs: number,\n): IdempotencyStore<V> {\n if (store.ttlMs === undefined) return store;\n const nextEntries: Record<string, IdempotencyEntry<V>> = {};\n const nextOrder: string[] = [];\n // Walk `order` (not `Object.keys`) so the surviving order ledger keeps the\n // exact relative insertion order capacity eviction depends on.\n for (const key of store.order) {\n const entry = store.entries[key];\n if (entry === undefined) continue; // defensive: order/entries kept in lockstep\n if (isExpired(entry, nowMs, store.ttlMs)) continue;\n nextEntries[key] = entry;\n nextOrder.push(key);\n }\n return { ...store, entries: nextEntries, order: nextOrder };\n}\n\n/**\n * Record `key → value` at `nowMs`, then enforce both bounds. PURE — returns a\n * new store; the input is untouched.\n *\n * Order of operations (each step matters):\n * 1. **Expiry eviction** — drop everything older than `ttlMs` first, so an\n * expired key never occupies a capacity slot the new entry could take, and\n * a stale entry under the same `key` is cleared before the overwrite.\n * 2. **Insert / overwrite** — write the new entry. If `key` already exists,\n * this is an UPDATE: `value` is overwritten, `atMs` is refreshed to\n * `nowMs` (restarting the TTL clock), and the key is moved to the NEWEST\n * position in `order` (removed from wherever it sat, appended at the end).\n * See the module-level \"Overwrite semantics\" note.\n * 3. **Capacity eviction** — if the entry count now exceeds `capacity`, drop\n * the OLDEST keys by `order` until the count fits. Because step 2 moves a\n * re-remembered key to the newest position, an actively re-seen key is\n * never the eviction victim.\n */\nexport function remember<V>(\n store: IdempotencyStore<V>,\n key: string,\n value: V,\n nowMs: number,\n): IdempotencyStore<V> {\n // Step 1 — expire first so stale entries free capacity slots before insert.\n const pruned = evictExpired(store, nowMs);\n\n // Step 2 — insert/overwrite. Drop any prior position of `key` from order,\n // then append it as newest. `delete` on a fresh shallow copy keeps the input\n // store untouched (invariant 1).\n const nextEntries: Record<string, IdempotencyEntry<V>> = {\n ...pruned.entries,\n };\n nextEntries[key] = { value, atMs: nowMs };\n const nextOrder = pruned.order.filter((k) => k !== key);\n nextOrder.push(key);\n\n // Step 3 — capacity eviction. Drop oldest (front of `order`) until the count\n // fits. `capacity <= 0` evicts everything just inserted down to the bound;\n // `undefined` capacity skips eviction entirely.\n if (store.capacity !== undefined) {\n while (nextOrder.length > store.capacity) {\n const oldest = nextOrder.shift();\n if (oldest === undefined) break; // order emptied — nothing left to evict\n delete nextEntries[oldest];\n }\n }\n\n return { ...store, entries: nextEntries, order: nextOrder };\n}\n"]}
@@ -1,15 +0,0 @@
1
- import { patchItemOp, markDoneOp, claimNextOp, enqueueOp } from './chunk-FV3Z5MBE.js';
2
-
3
- // src/work-queue/adapter.ts
4
- function queueAdapter() {
5
- return {
6
- enqueue: (queue, input, now, id) => enqueueOp(queue, input, now, id),
7
- claim: (queue, now) => claimNextOp(queue, now),
8
- markDone: (queue, id) => markDoneOp(queue, id),
9
- patch: (queue, id, patch) => patchItemOp(queue, id, patch)
10
- };
11
- }
12
-
13
- export { queueAdapter };
14
- //# sourceMappingURL=chunk-MU6MCIAC.js.map
15
- //# sourceMappingURL=chunk-MU6MCIAC.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/work-queue/adapter.ts"],"names":[],"mappings":";;;AAqFO,SAAS,YAAA,GAAmC;AACjD,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAC,KAAA,EAAO,KAAA,EAAO,GAAA,EAAK,OAAO,SAAA,CAAa,KAAA,EAAO,KAAA,EAAO,GAAA,EAAK,EAAE,CAAA;AAAA,IACtE,OAAO,CAAC,KAAA,EAAO,GAAA,KAAQ,WAAA,CAAe,OAAO,GAAG,CAAA;AAAA,IAChD,UAAU,CAAC,KAAA,EAAO,EAAA,KAAO,UAAA,CAAc,OAAO,EAAE,CAAA;AAAA,IAChD,KAAA,EAAO,CAAC,KAAA,EAAO,EAAA,EAAI,UAAU,WAAA,CAAe,KAAA,EAAO,IAAI,KAAK;AAAA,GAC9D;AACF","file":"chunk-MU6MCIAC.js","sourcesContent":["/**\n * @demlik/tea/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 +0,0 @@
1
- {"version":3,"sources":["../src/recorder/index.ts"],"names":[],"mappings":"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","file":"chunk-MUUROGIH.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 \"@demlik/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 \"@demlik/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 `@demlik/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 `@demlik/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 `@demlik/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 `@demlik/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 \"@demlik/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 \"@demlik/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 \"@demlik/tea/recorder\";\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 \"@demlik/tea/recorder\";\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"]}
@@ -1,88 +0,0 @@
1
- import { createResilientCall, liftResilience } from './chunk-EUF7XOEE.js';
2
- import { MsgType } from './chunk-JELSZN25.js';
3
- import { describeError } from './chunk-G5E74IJB.js';
4
-
5
- // src/llm-call/index.ts
6
- function createLlmCall(config, rng = Math.random) {
7
- const resilientConfig = {
8
- ...config.retry === void 0 ? {} : { retry: config.retry }
9
- };
10
- const rc = createResilientCall(resilientConfig, rng);
11
- function init() {
12
- return rc.init();
13
- }
14
- function attempt(s, input, at, key = input.purpose) {
15
- return rc.attempt(s, key, input, at);
16
- }
17
- function succeed(s, key, msg) {
18
- return rc.succeed(s, key, msg);
19
- }
20
- function fail(s, key, msg) {
21
- return rc.fail(s, key, msg);
22
- }
23
- function onTimer(s, msg) {
24
- return rc.onTimer(s, msg);
25
- }
26
- function subs(s) {
27
- return rc.subs(s);
28
- }
29
- async function invokeOne(input) {
30
- const model = config.model(input.model);
31
- const messages = config.loadMessages ? await config.loadMessages(input) : [];
32
- const schema = config.schemas[input.purpose];
33
- const structured = model.withStructuredOutput(schema);
34
- const raw = await structured.invoke(messages);
35
- const output = schema.parse(raw);
36
- return { key: input.purpose, purpose: input.purpose, output };
37
- }
38
- const resilientRunHandler = rc.handlers({
39
- run: (input) => invokeOne(input)
40
- }).resilient_run;
41
- async function settleOf(cmd) {
42
- const settle = await resilientRunHandler(cmd, {});
43
- if (settle.type === MsgType.ResilientOk) {
44
- return { ...settle, result: { ...settle.result, key: cmd.key } };
45
- }
46
- const rawError = settle.error;
47
- const err = {
48
- key: cmd.key,
49
- purpose: cmd.input.purpose,
50
- reason: describeError(rawError),
51
- error: rawError
52
- };
53
- return {
54
- type: MsgType.ResilientErr,
55
- key: settle.key,
56
- error: err,
57
- at: settle.at
58
- };
59
- }
60
- function handlers(ports) {
61
- if (ports === void 0) {
62
- return { resilient_run: (cmd) => settleOf(cmd) };
63
- }
64
- return {
65
- resilient_run: (cmd, ctx) => {
66
- const fired = (async () => {
67
- const settle = await settleOf(cmd);
68
- if (settle.type === MsgType.ResilientOk) {
69
- const msg2 = ports.onOk(settle.result);
70
- if (msg2 !== void 0) await ctx.dispatch(msg2);
71
- return;
72
- }
73
- const msg = ports.onErr(settle.error);
74
- if (msg !== void 0) await ctx.dispatch(msg);
75
- })();
76
- ctx.waitUntil(fired);
77
- }
78
- };
79
- }
80
- return { init, attempt, succeed, fail, onTimer, subs, handlers, invokeOne };
81
- }
82
- function liftLlmCall(state, result) {
83
- return liftResilience(state, result);
84
- }
85
-
86
- export { createLlmCall, liftLlmCall };
87
- //# sourceMappingURL=chunk-ND3DH6KM.js.map
88
- //# sourceMappingURL=chunk-ND3DH6KM.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/llm-call/index.ts"],"names":["msg"],"mappings":"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","file":"chunk-ND3DH6KM.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/llm-call — `resilient-call` + structured-output parse + a typed\n * failure variant, around a purpose-discriminated LLM invocation.\n *\n * This is the ~60-line `call_llm` handler from the audit-agent seed\n * (`interpret.ts` / `effects.ts`) collapsed into the uniform L2 knob: a config\n * object, the resilient-call slice it inherits, and one interpret handler that\n * assembles messages, binds the structured-output schema for the call's\n * `purpose`, invokes the model (retry composed from `../resilient-call`, NOT\n * reinvented), parses the output, and RETURNS the enriched resilient settle Msg\n * (`resilient_ok` / `resilient_err`) so it re-enters the host reducer and drives\n * the inherited succeed/fail → backoff → onTimer loop.\n *\n * ## What it adds over `../resilient-call`\n *\n * - **Two DI ports** (the seed's two seams):\n * 1. `model: (modelId) => LLM` — the model factory. Tests pass a fake so\n * the handler runs end-to-end without touching a real provider; the\n * production worker wires `createChatModel(env, …)`.\n * 2. `loadMessages: Loader` — the SDK / message loader. The seed lazy-\n * imports `@langchain/core/messages` (a top-level `import type` blows\n * up the workers test runner); the loader keeps that import lazy and\n * stubbable. It produces the `BaseMessage[]` for a given call.\n * - **Structured-output parse** — each `purpose` maps to a `Schema` in\n * `config.schemas`. The handler binds `model.withStructuredOutput(schema)`\n * so the invoke resolves to a typed object; a `Schema.parse` that throws\n * becomes a `resilient_err` carrying an `LlmErr` (a parse failure is a\n * failure, not a stall — \"errors are data\"). `withStructuredOutput` is the\n * seed's brain-only path; the fake model in tests returns the typed object\n * directly.\n * - **A typed failure variant** — every failure (model throw, timeout-via-\n * retry-exhaustion, schema parse) is surfaced as the `LlmErr` carried on the\n * enriched `resilient_err` settle Msg, tagged with the `purpose` so the\n * consumer's reducer routes per-stage. The seed dispatched\n * `llm_failed{purpose}`; the consumer rebuilds that shape in its reducer arm\n * from `m.error: LlmErr`.\n *\n * ## Inheriting resilient-call by composition (not reinvention)\n *\n * The retry / backoff lives in `../resilient-call`: `createLlmCall` builds a\n * `createResilientCall` knob over the model-invoke port and DELEGATES `init`,\n * `attempt`, `succeed`, `fail`, `onTimer`, `subs` to it verbatim — the slice is\n * literally resilient-call's slice (the spec's \"retry lives here\"). There is no\n * second backoff implementation here. The handler runs the invoke through the\n * resilient-call handler so a transient model failure backs off exactly as a\n * resilient HTTP call would; the `purpose`-branch + structured parse wrap the\n * port the resilient handler drives.\n *\n * ## The two non-negotiables (canon, inherited)\n *\n * - **Durable** — the slice is resilient-call's plain-data slice (a Model\n * field). Input carried on the `resilient_run` Cmd is the plain\n * `LlmCall` request (purpose + modelId + payload) — no closures.\n * - **Replayable** — every transition is a resilient-call verb; the handler\n * is the only impurity, and the one clock read is `Date.now()` at the\n * effect boundary (inside the handler), exactly as the seed stamped its\n * `llm_responded` / `llm_failed` Msgs.\n *\n * ## Typical wiring\n *\n * const llm = createLlmCall<MyPurpose, MyOutputs>({\n * model: (id) => createChatModel(env, id),\n * schemas: { plan: planSchema, report: reportSchema },\n * retry: defaultRetryPolicy,\n * loadMessages: defaultMessagesLoader,\n * });\n *\n * // in the machine:\n * init: () => [{ resilience: llm.init() }, []],\n * update: {\n * call_llm: (s, m) => lift(s, llm.attempt(s.resilience, m.input, m.at)),\n * // `resilient_ok` / `resilient_err` RE-ENTER from `handlers` (the settle\n * // Msg the interpret handler returns). Run the inherited verb FIRST so the\n * // succeed/fail → backoff → onTimer loop advances, THEN fold the enriched\n * // payload (`m.result: LlmOk` / `m.error: LlmErr`) into the host's own\n * // state — the enrichment lives in the reducer arm, not the handler.\n * resilient_ok: (s, m) => {\n * const [slice, cmds] = llm.succeed(s.resilience, m.key, m);\n * return [{ ...s, resilience: slice, output: m.result.output }, cmds];\n * },\n * resilient_err: (s, m) => {\n * const [slice, cmds] = llm.fail(s.resilience, m.key, m);\n * return [{ ...s, resilience: slice, failure: m.error }, cmds];\n * },\n * deadline_exceeded: (s, m) => lift(s, llm.onTimer(s.resilience, m)),\n * },\n * subscriptions: (s) => llm.subs(s.resilience),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: llm.handlers(),\n */\n\nimport { describeError } from \"../describe-error\";\nimport type { Cmd } from \"../index\";\nimport { MsgType } from \"../protocol\";\nimport {\n createResilientCall,\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineSub,\n type FailMsg,\n liftResilience,\n type ResilientConfig,\n type ResilientState,\n type RunCmd,\n type SucceedMsg,\n subscribeDeadline,\n} from \"../resilient-call\";\nimport type { RetryPolicy } from \"../retry-backoff\";\n\n// ===========================================================================\n// The DI port surfaces — model factory + structured output + message loader.\n// ===========================================================================\n\n/**\n * The minimal structured-output schema contract: `parse(unknown) => T`, the\n * zod-style call the handler uses to validate the model's output before it\n * settles `resilient_ok`. A throwing `parse` (the zod contract on invalid\n * input) propagates out of `invokeOne` and is caught by the resilient handler,\n * which settles `resilient_err` (enriched to `LlmErr`) — a parse failure is a\n * failure, never a silent pass. Structural so a real `z.ZodType<T>` satisfies\n * it without an import.\n */\nexport interface Schema<T> {\n /** Validate + narrow `value` to `T`, or throw on mismatch (the zod contract). */\n parse(value: unknown): T;\n}\n\n/**\n * The minimal chat-model contract every model the handler talks to must\n * satisfy — the seed's `InjectableChatModel`, trimmed to the one operation\n * llm-call drives for brain-only stages:\n *\n * `withStructuredOutput(schema)` → a runnable whose `invoke(messages)`\n * resolves to a typed object matching `schema`.\n *\n * `BaseChatModel` from `@langchain/core` is the runtime type; this surface\n * names only what llm-call calls so a test fake can ignore the rest. Generic\n * over the message shape `Msg` so a consumer's loader and model agree on it\n * without llm-call inspecting messages.\n */\nexport interface Llm<Msg> {\n withStructuredOutput<T>(schema: Schema<T>): {\n invoke(messages: readonly Msg[]): Promise<T>;\n };\n}\n\n/**\n * Build the `Msg[]` the handler hands to the bound model for a given call. The\n * SDK / message-assembly seam — the seed's `buildBaseMessages` + the lazy\n * `loadMessages` loader rolled into one injected port. Async because the seed\n * lazy-imports the SDK (a top-level `import type` of the langchain messages\n * package explodes the workers test runner). Receives the full `LlmCall` so it\n * can branch on `purpose` exactly as the seed's `buildBaseMessages` did.\n */\nexport type MessageLoader<P extends string, Msg> = (\n call: LlmCall<P>,\n) => Promise<readonly Msg[]>;\n\n/**\n * The model factory — the first DI port. `(modelId) => Llm`. Tests pass a fake\n * builder; production wires `createChatModel(env, getModelConfig(id))`. `null`\n * means \"the host's default model\" (the seed's `string | null`).\n */\nexport type ModelFactory<Msg> = (modelId: string | null) => Llm<Msg>;\n\n// ===========================================================================\n// Config — the knob. `model` + `schemas` required (the two DI ports + the\n// per-purpose parse target); `retry` + `loadMessages` optional.\n// ===========================================================================\n\n/**\n * The llm-call knob. `model` and `schemas` are the load-bearing pair — the\n * model factory DI port and the per-purpose structured-output targets; the\n * rest is optional, exactly the resilient-call \"omit a brick → omit its gate\"\n * story for `retry`:\n *\n * - `model` — DI port 1: the model factory.\n * - `schemas` — one `Schema` per `Purpose`. The handler binds\n * `schemas[call.purpose]` as the structured-output target.\n * - `retry` — backoff policy, composed straight into `../resilient-\n * call`. Omit it and a model failure is terminal (no\n * backoff), exactly as resilient-call with no retry brick.\n * - `loadMessages` — DI port 2: the SDK / message loader. Omit it and the\n * handler invokes the bound model with `[]` (a degenerate\n * but valid call) — provide it to assemble real messages.\n *\n * `P` is the purpose union (e.g. `\"plan\" | \"report\"`); `O` maps each purpose to\n * its parsed output type; `Msg` is the model's message shape (threaded through\n * the loader + model so llm-call never inspects a message).\n */\nexport interface LlmCallConfig<\n P extends string,\n O extends Record<P, unknown>,\n Msg = unknown,\n> {\n /** DI port 1 — the model factory. `(modelId) => Llm`. */\n readonly model: ModelFactory<Msg>;\n /** One structured-output schema per purpose; the parse target the handler binds. */\n readonly schemas: { readonly [K in P]: Schema<O[K]> };\n /** Backoff policy, composed into `../resilient-call`. Omit → no backoff. */\n readonly retry?: RetryPolicy;\n /** DI port 2 — the SDK / message loader. Omit → the handler invokes with `[]`. */\n readonly loadMessages?: MessageLoader<P, Msg>;\n}\n\n// ===========================================================================\n// The request / response shapes the knob speaks.\n// ===========================================================================\n\n/**\n * One LLM call request — the resilient-call `input` for this knob, carried on\n * the `resilient_run` Cmd as plain data (no closures, invariant 3). Mirrors the\n * seed's purpose-discriminated `CallLlmCmd`: a `purpose` selecting the schema +\n * prompt assembly, the `model` id, and the opaque per-purpose `payload` the\n * loader consumes.\n *\n * `key` defaults to `purpose` when the consumer calls `attempt(s, purpose, …)`,\n * so one in-flight call per purpose is tracked under the resilient-call slice —\n * the common single-call-per-stage shape. A consumer that fans out many calls\n * of one purpose passes a distinct `key`.\n */\nexport interface LlmCall<P extends string> {\n /** The stage / schema selector — drives both `schemas[purpose]` and message assembly. */\n readonly purpose: P;\n /** The model id to invoke; `null` = the host's default model. */\n readonly model: string | null;\n /** The per-purpose prompt payload the `MessageLoader` consumes. Opaque to the knob. */\n readonly payload: unknown;\n}\n\n/** The parsed, typed success carried on the `resilient_ok` settle Msg, tagged with its purpose. */\nexport interface LlmOk<P extends string, O extends Record<P, unknown>> {\n readonly key: string;\n readonly purpose: P;\n readonly output: O[P];\n}\n\n/** The typed failure variant — every failure path surfaces this, tagged by purpose. */\nexport interface LlmErr<P extends string> {\n readonly key: string;\n readonly purpose: P;\n /** A human-readable cause (model throw, retry-exhaustion, or schema parse). */\n readonly reason: string;\n /** The original error, carried untouched for the consumer to inspect. */\n readonly error: unknown;\n}\n\n/**\n * The settle Msgs llm-call's handler RETURNS from `interpret` so the substrate\n * enqueues them as follow-up Msgs (re-entry) into the host reducer — exactly as\n * `../resilient-call` does. They are the resilient-call settle Msgs with the\n * payloads ENRICHED to llm-call's typed variants: `result` is the parsed,\n * purpose-tagged `LlmOk`; `error` is the typed `LlmErr` (purpose + reason +\n * raw). The host wires `resilient_ok` → `succeed` and `resilient_err` → `fail`\n * reducer arms (the doc-comment wiring), and folds the enriched payload into its\n * own state there — llm-call never forces a Msg vocabulary on the host, and the\n * settle Msg drives the inherited succeed/fail → backoff → onTimer loop instead\n * of bypassing it.\n */\n//\n// The success Msg is `../resilient-call`'s `SucceedMsg` specialized to the parsed\n// `LlmOk` result — NOT a re-declared shape — so the verb returns (`succeed`'s\n// argument, the handler's resolve) thread through with no cast. The failure Msg\n// is the resilient `FailMsg` with its `error: unknown` NARROWED to the typed\n// `LlmErr`: every value the handler enriches is an `LlmErr`, and `LlmErr` is\n// assignable to `unknown`, so `LlmFailMsg` flows into `rc.fail` (which takes the\n// wide `FailMsg`) directly while the host reducer reads the narrow `error` type.\nexport type LlmSucceedMsg<\n P extends string,\n O extends Record<P, unknown>,\n> = SucceedMsg<LlmOk<P, O>>;\nexport type LlmFailMsg<P extends string> = Omit<FailMsg, \"error\"> & {\n readonly error: LlmErr<P>;\n};\n\n/** The retry / deadline timer Msg — `DeadlineExceeded`, inherited from resilient-call. */\nexport type LlmTimerMsg = DeadlineExceeded;\n\n/**\n * The ports the LEGACY detached `handlers(ports)` form takes. Two outbound Msg\n * builders the handler hands the typed Ok / Err; the consumer returns its own\n * Msg (or `undefined`). KEPT only for `../agent`, which still inherits the\n * detached shape; that path does NOT drive the retry loop (it dispatches the\n * consumer's Msg directly and never re-enters the resilient settle Msg). New\n * consumers use the no-arg `handlers()` form, which RETURNS the settle Msg for\n * re-entry and drives the inherited succeed/fail → backoff → onTimer loop.\n * `../agent` carries the same retry-loop gap and is fixed separately.\n *\n * The builders return `M | undefined` — `undefined` is the \"dispatch nothing\"\n * sentinel the detached form checks (`!== undefined`), spelled as `undefined`\n * rather than `void` so the union is unambiguous (a `void` member of a union is\n * confusing and reads as \"may return anything\").\n */\nexport interface LlmCallPorts<\n P extends string,\n O extends Record<P, unknown>,\n M,\n> {\n /** Build the Msg dispatched on a parsed success. Return `undefined` to dispatch nothing. */\n readonly onOk: (ok: LlmOk<P, O>) => M | undefined;\n /** Build the Msg dispatched on any failure (throw / retry-exhausted / parse). */\n readonly onErr: (err: LlmErr<P>) => M | undefined;\n}\n\n/**\n * The effect Cmd the knob emits: run the LLM call for `key` with `input`. The\n * `input` is the plain `LlmCall` request — the handler reads `purpose` /\n * `model` / `payload` off it (invariant 3: Cmds are data).\n *\n * It is `../resilient-call`'s `RunCmd` specialized to the `LlmCall` input — NOT a\n * re-declared shape — so the resilient verbs' return tuples (`[State, RunCmd[]]`)\n * are this exact type and thread through `attempt` / `succeed` / `fail` /\n * `onTimer` with no cast.\n */\nexport type LlmRunCmd<P extends string> = RunCmd<LlmCall<P>>;\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build an llm-call knob from `config`. `rng` is injected for the inherited\n * retry jitter (pass a fixed `() => 0.5` in tests to pin backoff; defaults to\n * `Math.random`, read only at the resilient-call verb boundary).\n *\n * Returns the uniform L2 knob contract. The slice + verbs (`init`, `attempt`,\n * `succeed`, `fail`, `onTimer`, `subs`) are DELEGATED to a `../resilient-call`\n * knob built over the structured-output model-invoke port — no second backoff\n * here. `handlers` is the one new piece: the purpose-branching, schema-parsing\n * wrapper that returns the enriched resilient settle Msg for re-entry.\n *\n * `P` is the purpose union, `O` the purpose→output map, `Msg` the model's\n * message shape.\n */\nexport function createLlmCall<\n P extends string,\n O extends Record<P, unknown>,\n Msg = unknown,\n>(config: LlmCallConfig<P, O, Msg>, rng: () => number = Math.random) {\n // ---- The composed resilient-call knob ----------------------------------\n //\n // The resilient-call input is the full `LlmCall` request; its result is the\n // PARSED, typed `LlmOk`. Only the `retry` brick is forwarded — llm-call does\n // not expose circuit / rate-limit / cache / deadline knobs (a stage call is\n // a single brain invocation, not a keyed downstream target). Omitting those\n // bricks omits their gates, exactly as resilient-call documents.\n const resilientConfig: ResilientConfig = {\n ...(config.retry === undefined ? {} : { retry: config.retry }),\n };\n const rc = createResilientCall<LlmCall<P>, LlmOk<P, O>>(resilientConfig, rng);\n\n /** The slice this knob owns — resilient-call's slice verbatim. */\n type State = ResilientState<LlmCall<P>, LlmOk<P, O>>;\n\n /** The starting slice — resilient-call's. */\n function init(): State {\n return rc.init();\n }\n\n /**\n * Start (or restart) an LLM call. `keyOrPurpose` defaults the resilient-call\n * `key` to the call's `purpose` so one in-flight call per stage is tracked\n * under the slice (the common shape); pass a distinct key to fan out. PURE —\n * delegates straight to resilient-call's gate.\n */\n function attempt(\n s: State,\n input: LlmCall<P>,\n at: number,\n key: string = input.purpose,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.attempt(s, key, input, at);\n }\n\n /** Record a parsed success for `key`. PURE — resilient-call's `succeed`. */\n function succeed(\n s: State,\n key: string,\n msg: LlmSucceedMsg<P, O>,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.succeed(s, key, msg);\n }\n\n /**\n * Record a failure for `key`: back off via the inherited retry, or settle\n * `failed`. PURE — resilient-call's `fail`. The `error` is the typed\n * `LlmErr`, carried on the call's `failed` phase for the consumer to read.\n */\n function fail(\n s: State,\n key: string,\n msg: LlmFailMsg<P>,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.fail(s, key, msg);\n }\n\n /** A retry / deadline timer fired. PURE — resilient-call's `onTimer`. */\n function onTimer(\n s: State,\n msg: LlmTimerMsg,\n ): readonly [State, readonly LlmRunCmd<P>[]] {\n return rc.onTimer(s, msg);\n }\n\n /** Pre-wired subs — resilient-call's retry-timer subscriptions. */\n function subs(s: State): readonly DeadlineSub[] {\n return rc.subs(s);\n }\n\n // ---- The model-invoke port the resilient handler drives ----------------\n\n /**\n * Run ONE LLM call: assemble messages via the loader, bind the purpose's\n * structured-output schema, invoke, and parse. Returns the typed `LlmOk` on\n * success; throws on any failure (model throw OR schema parse) so the\n * resilient-call handler routes it to a failure Msg + backoff. This is the\n * single port resilient-call wraps — the retry / timeout machinery lives in\n * resilient-call, never here (\"do NOT reinvent backoff\").\n *\n * The seed's two paths collapse to one: every brain-only purpose goes through\n * `withStructuredOutput(schema)`. The `parse` re-validates the output (a fake\n * model might skip validation, and a real provider can drift) so a malformed\n * structured response is a failure, not a corrupt success.\n */\n async function invokeOne(input: LlmCall<P>): Promise<LlmOk<P, O>> {\n const model = config.model(input.model);\n const messages = config.loadMessages\n ? await config.loadMessages(input)\n : [];\n const schema = config.schemas[input.purpose];\n const structured = model.withStructuredOutput(schema);\n const raw = await structured.invoke(messages);\n // Re-validate at the boundary — a parse throw becomes a `resilient_err`\n // (enriched to `LlmErr`) settle Msg upstream, never a corrupt success.\n const output = schema.parse(raw);\n return { key: input.purpose, purpose: input.purpose, output };\n }\n\n // ---- Handlers — the one new piece, composed onto resilient-call --------\n\n /**\n * Pre-wired interpret handler for `resilient_run`. Composes\n * `../resilient-call`'s `resilient_run` handler over the `invokeOne` port and\n * RETURNS the resilient settle Msg — it does NOT dispatch the consumer's Msg\n * itself. Returning the settle Msg is the whole point: the substrate enqueues\n * an interpret handler's returned Msg as a FOLLOW-UP (re-entry) onto the\n * dispatch tail, so the host reducer's `resilient_ok` / `resilient_err` arms\n * run `succeed` / `fail`, which advances the inherited retry loop\n * (succeed/fail → backoff → onTimer → re-issue). A handler that instead\n * `dispatch`ed an enriched Msg directly would settle ONE invoke and bypass\n * that loop entirely — the slice would never leave `running`, the breaker\n * would never trip/close, and the retry counter would never reset.\n *\n * Resilience is composed, not reimplemented: the body delegates to the\n * resilient-call handler's Railway-routed invoke (Ok → `resilient_ok`, Err →\n * `resilient_err`), so a transient model failure backs off via the inherited\n * retry exactly as resilient-call decides. The ONE enrichment llm-call adds is\n * mapping the settle PAYLOADS into its typed variants:\n *\n * - success → `result` is already the parsed `LlmOk` (`invokeOne` returns\n * it), so the resilient `resilient_ok` Msg is the llm-call `LlmSucceedMsg`\n * as-is.\n * - failure → the resilient `resilient_err` carries the RAW throw (a model\n * 503 or a `schema.parse` throw). We rebuild it into the typed `LlmErr`\n * (purpose + reason + raw) so the host reducer routes per-stage.\n *\n * The host folds the enriched payload into its own state in the reducer arm\n * (the doc-comment wiring), NOT here — the handler stays the one impurity\n * (the `invokeOne` await + the `Date.now()` stamp the resilient handler makes)\n * and the reducer stays pure.\n */\n // The resilient-call `resilient_run` handler over `invokeOne` — the shared\n // Railway-routed invoke (Ok → resilient_ok Msg, Err → resilient_err Msg). Both\n // handler forms below build on it; the settle shape is composed, not reinvented.\n const resilientRunHandler = rc.handlers({\n run: (input) => invokeOne(input),\n }).resilient_run;\n\n /**\n * Await one composed invoke and resolve to the ENRICHED resilient settle Msg\n * (`resilient_ok` carrying the parsed `LlmOk`, or `resilient_err` carrying the\n * typed `LlmErr`). The shared core both handler forms reuse.\n */\n async function settleOf(\n cmd: LlmRunCmd<P>,\n ): Promise<LlmSucceedMsg<P, O> | LlmFailMsg<P>> {\n // `resilientRunHandler` never rejects (tryInterpret contract) — it resolves\n // to a `resilient_ok` / `resilient_err` settle Msg stamped with `Date.now()`.\n // Its `ctx` slot is `NoCtx` (the resilient-call work fn reads no ctx — a\n // DELIBERATE context-free seam, not accidental `unknown`), so the empty\n // record satisfies it without a cast.\n const settle = await resilientRunHandler(cmd, {});\n if (settle.type === MsgType.ResilientOk) {\n // `result` is the parsed `LlmOk` from `invokeOne`, which hardcoded its\n // `key` to `purpose` (the port receives no key). Re-key it to `cmd.key` —\n // the fan-out key `attempt` threaded in — so a fanned-out success carries\n // the SAME key its failure would (`settleOf`'s err path below uses\n // `cmd.key`), never the collapsed purpose. `settle.key` is already\n // `cmd.key`; this aligns the enriched `LlmOk.key` inside it.\n return { ...settle, result: { ...settle.result, key: cmd.key } };\n }\n // resilient_err — enrich the raw throw into the typed `LlmErr`, keeping the\n // resilient Msg's `key` / `at` so the host reducer's `fail` verb re-issues\n // from the remembered input and backs off on schedule.\n const rawError: unknown = settle.error;\n const err: LlmErr<P> = {\n key: cmd.key,\n purpose: cmd.input.purpose,\n reason: describeError(rawError),\n error: rawError,\n };\n return {\n type: MsgType.ResilientErr,\n key: settle.key,\n error: err,\n at: settle.at,\n };\n }\n\n /**\n * Pre-wired interpret handler for `resilient_run`. TWO forms:\n *\n * - `handlers()` (no args, the FIXED primary) — RETURNS the enriched\n * resilient settle Msg. The substrate enqueues an interpret handler's\n * returned Msg as a FOLLOW-UP (re-entry) onto the dispatch tail, so the\n * host reducer's `resilient_ok` / `resilient_err` arms run `succeed` /\n * `fail`, advancing the inherited retry loop (succeed/fail → backoff →\n * onTimer → re-issue). This is the correct wiring; the module doc shows it.\n *\n * - `handlers(ports)` (LEGACY, detached) — runs the invoke inside\n * `ctx.waitUntil` and dispatches the consumer's `onOk` / `onErr` Msg\n * directly. KEPT only for `../agent`, which still inherits this shape. It\n * does NOT drive the retry loop (it never re-enters the settle Msg, so\n * `succeed` / `fail` never run) — `../agent` carries the same gap and is\n * fixed in its own pass. New consumers MUST use the no-arg form.\n *\n * Resilience is composed, not reimplemented: both forms delegate to the\n * resilient-call handler's Railway-routed invoke and add only the typed\n * `LlmOk` / `LlmErr` enrichment. The ONE impurity is the `invokeOne` await +\n * the `Date.now()` stamp the resilient handler makes; the reducer stays pure.\n */\n function handlers(): {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ) => Promise<LlmSucceedMsg<P, O> | LlmFailMsg<P>>;\n };\n function handlers<M>(ports: LlmCallPorts<P, O, M>): {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ctx: { waitUntil(p: Promise<unknown>): void; dispatch(msg: M): unknown },\n ) => void;\n };\n function handlers<M>(ports?: LlmCallPorts<P, O, M>) {\n if (ports === undefined) {\n // FIXED primary: return the settle Msg for re-entry — drives the loop.\n return { resilient_run: (cmd: LlmRunCmd<P>) => settleOf(cmd) };\n }\n // LEGACY detached form — kept for `../agent` (see doc above). Dispatches the\n // consumer's Msg directly; does not re-enter the settle Msg.\n return {\n resilient_run: (\n cmd: LlmRunCmd<P>,\n ctx: {\n waitUntil(p: Promise<unknown>): void;\n dispatch(msg: M): unknown;\n },\n ): void => {\n const fired = (async () => {\n const settle = await settleOf(cmd);\n if (settle.type === MsgType.ResilientOk) {\n const msg = ports.onOk(settle.result);\n if (msg !== undefined) await ctx.dispatch(msg);\n return;\n }\n const msg = ports.onErr(settle.error);\n if (msg !== undefined) await ctx.dispatch(msg);\n })();\n ctx.waitUntil(fired);\n },\n };\n }\n\n return { init, attempt, succeed, fail, onTimer, subs, handlers, invokeOne };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the\n * slice lives at `state.resilience` — the same convenience `../resilient-call`\n * ships, re-typed for the llm-call slice so consumers wire one import. Pure.\n */\nexport function liftLlmCall<\n S extends { resilience: ResilientState<LlmCall<P>, LlmOk<P, O>> },\n P extends string,\n O extends Record<P, unknown>,\n C extends Cmd,\n>(\n state: S,\n result: readonly [ResilientState<LlmCall<P>, LlmOk<P, O>>, readonly C[]],\n): readonly [S, readonly C[]] {\n return liftResilience(state, result);\n}\n\n/**\n * Re-export the deadline Sub primitives (inherited from resilient-call) so\n * consumers wire one import: `subscribeDeadline` is the `subscribe` cell,\n * `deadlineSub` builds the Sub literal `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub, DeadlineExceeded, ResilientState };\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/retry-backoff/index.ts"],"names":[],"mappings":";AA0HO,SAAS,KAAA,CAAM,EAAA,EAAkB,KAAA,GAAQ,KAAA,EAAY;AAC1D,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,MAAM,SAAS,EAAA,EAAG;AAClB,IAAA,IAAI,EAAE,MAAA,IAAU,CAAA,IAAK,MAAA,GAAS,CAAA,CAAA,EAAI;AAChC,MAAA,MAAM,IAAI,UAAA;AAAA,QACR,6BAA6B,MAAM,CAAA,6BAAA;AAAA,OACrC;AAAA,IACF;AAAA,EACF;AACA,EAAA,OAAO,EAAA;AACT;AAOO,IAAM,aAAkB,IAAA,CAAK;AAsH7B,IAAM,kBAAA,GAAkC;AAAA,EAC7C,MAAA,EAAQ,GAAA;AAAA,EACR,MAAA,EAAQ,CAAA;AAAA,EACR,KAAA,EAAO,GAAA;AAAA,EACP,WAAA,EAAa,CAAA;AAAA,EACb,MAAA,EAAQ;AACV;AAuBO,SAAS,YAAA,CACd,OAAA,EACA,MAAA,EACA,GAAA,GAAW,UAAA,EACH;AAIR,EAAA,MAAM,WAAA,GAAc,OAAA,GAAU,CAAA,GAAI,CAAA,GAAI,OAAA;AACtC,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,WAAA;AAMrD,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,KAAA,CAAM,WAAW,CAAA,GAAI,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,KAAA,EAAO,WAAW,CAAA;AAE5E,EAAA,QAAQ,OAAO,MAAA;AAAQ,IACrB,KAAK,MAAA;AACH,MAAA,OAAO,CAAA;AAAA,IACT,KAAK,MAAA;AAKH,MAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAA,KAAQ,CAAC,CAAA;AAAA,IAC9B,KAAK,OAAA;AAGH,MAAA,OAAO,IAAA,CAAK,IAAI,CAAA,EAAG,CAAA,GAAI,IAAI,GAAA,EAAI,IAAK,IAAI,CAAA,CAAE,CAAA;AAAA;AAEhD;AAiCO,SAAS,SAAA,GAAwB;AACtC,EAAA,OAAO,EAAE,SAAS,CAAA,EAAE;AACtB;AAwBO,SAAS,aAAA,CACd,KAAA,EACA,KAAA,EACA,IAAA,EAC8B;AAC9B,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,GAAU,CAAA;AAEhC,EAAA,MAAM,gBAAA,GAAmB,MAAM,gBAAA,IAAoB,IAAA;AACnD,EAAA,OAAO,gBAAA,KAAqB,MAAA,GACxB,EAAE,OAAA,EAAS,SAAA,EAAW,KAAA,EAAM,GAC5B,EAAE,OAAA,EAAS,SAAA,EAAW,KAAA,EAAO,gBAAA,EAAiB;AACpD;AAUO,SAAS,cAAA,CAAe,OAAwB,KAAA,EAAuB;AAC5E,EAAA,MAAM,OAAA,GAAU,QAAQ,KAAA,CAAM,gBAAA;AAC9B,EAAA,OAAO,OAAA,GAAU,IAAI,OAAA,GAAU,CAAA;AACjC;AAmCO,SAAS,WAAA,CACd,KAAA,EACA,MAAA,EACA,KAAA,EACS;AACT,EAAA,IAAI,OAAO,WAAA,KAAgB,MAAA,IAAa,KAAA,CAAM,OAAA,IAAW,OAAO,WAAA,EAAa;AAC3E,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,MAAA,CAAO,YAAA,KAAiB,MAAA,EAAW,OAAO,IAAA;AAK9C,EAAA,IAAI,KAAA,CAAM,gBAAA,KAAqB,MAAA,IAAa,KAAA,KAAU,QAAW,OAAO,KAAA;AACxE,EAAA,OAAO,KAAA,GAAQ,KAAA,CAAM,gBAAA,GAAmB,MAAA,CAAO,YAAA;AACjD;AASO,SAAS,WAAA,CACd,KAAA,EACA,MAAA,EACA,GAAA,GAAW,UAAA,EACH;AACR,EAAA,OAAO,YAAA,CAAa,KAAA,CAAM,OAAA,EAAS,MAAA,EAAQ,GAAG,CAAA;AAChD","file":"chunk-Q2UM7BXU.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 * `@demlik/tea/work-queue`: a state type plus pure transition functions,\n * storage- and host-agnostic, that a consumer wires into their own reducer /\n * interpret. Nothing here reads the clock or the RNG on its own behalf —\n * randomness is *injected* (`rng`) and time is *computed by the caller* from\n * the millisecond delays this module returns. That keeps every function\n * deterministic and unit-testable, and keeps the substrate's purity invariants\n * intact when the result is threaded through `update` (invariant 2) and the\n * delay is realized as a `Cmd` the runtime performs (invariant 3).\n *\n * Typical wiring:\n *\n * // in `update`, on a failure Msg:\n * const retry = recordFailure(state.retry, msg.error);\n * if (!shouldRetry(retry, policy)) return [toGaveUp(state), []];\n * const delayMs = nextDelayMs(retry, policy); // pure: rng injected at call site\n * return [{ ...state, retry }, [{ type: \"ScheduleRetry\", delayMs, into: TimerFiredRetry }]];\n *\n * 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 knobs 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,47 +0,0 @@
1
- // src/rate-limit/index.ts
2
- function initBucket(capacity, refillPerSec, nowMs) {
3
- return {
4
- tokens: capacity,
5
- capacity,
6
- refillPerSec,
7
- lastRefillMs: nowMs
8
- };
9
- }
10
- function refill(bucket, nowMs) {
11
- const elapsedSec = Math.max(0, nowMs - bucket.lastRefillMs) / 1e3;
12
- const replenished = bucket.tokens + elapsedSec * bucket.refillPerSec;
13
- return {
14
- ...bucket,
15
- tokens: Math.min(bucket.capacity, replenished),
16
- lastRefillMs: nowMs
17
- };
18
- }
19
- function tryConsume(bucket, nowMs, n = 1) {
20
- const refilled = refill(bucket, nowMs);
21
- if (refilled.tokens >= n) {
22
- return [{ ...refilled, tokens: refilled.tokens - n }, true];
23
- }
24
- return [refilled, false];
25
- }
26
- function initWindow(windowMs, limit) {
27
- return { hits: [], windowMs, limit };
28
- }
29
- function prune(window, nowMs) {
30
- const cutoff = nowMs - window.windowMs;
31
- return window.hits.filter((t) => t > cutoff);
32
- }
33
- function record(window, nowMs) {
34
- const pruned = prune(window, nowMs);
35
- if (pruned.length < window.limit) {
36
- return [{ ...window, hits: [...pruned, nowMs] }, true];
37
- }
38
- return [{ ...window, hits: pruned }, false];
39
- }
40
- function remaining(window, nowMs) {
41
- const live = prune(window, nowMs).length;
42
- return Math.max(0, window.limit - live);
43
- }
44
-
45
- export { initBucket, initWindow, record, refill, remaining, tryConsume };
46
- //# sourceMappingURL=chunk-S2EGRPCM.js.map
47
- //# sourceMappingURL=chunk-S2EGRPCM.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/rate-limit/index.ts"],"names":[],"mappings":";AAsDO,SAAS,UAAA,CACd,QAAA,EACA,YAAA,EACA,KAAA,EACa;AACb,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,QAAA;AAAA,IACR,QAAA;AAAA,IACA,YAAA;AAAA,IACA,YAAA,EAAc;AAAA,GAChB;AACF;AAcO,SAAS,MAAA,CAAO,QAAqB,KAAA,EAA4B;AACtE,EAAA,MAAM,aAAa,IAAA,CAAK,GAAA,CAAI,GAAG,KAAA,GAAQ,MAAA,CAAO,YAAY,CAAA,GAAI,GAAA;AAC9D,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,MAAA,GAAS,UAAA,GAAa,MAAA,CAAO,YAAA;AACxD,EAAA,OAAO;AAAA,IACL,GAAG,MAAA;AAAA,IACH,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,UAAU,WAAW,CAAA;AAAA,IAC7C,YAAA,EAAc;AAAA,GAChB;AACF;AAeO,SAAS,UAAA,CACd,MAAA,EACA,KAAA,EACA,CAAA,GAAI,CAAA,EAC6B;AACjC,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,MAAA,EAAQ,KAAK,CAAA;AACrC,EAAA,IAAI,QAAA,CAAS,UAAU,CAAA,EAAG;AACxB,IAAA,OAAO,CAAC,EAAE,GAAG,QAAA,EAAU,QAAQ,QAAA,CAAS,MAAA,GAAS,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,EAC5D;AACA,EAAA,OAAO,CAAC,UAAU,KAAK,CAAA;AACzB;AAqBO,SAAS,UAAA,CAAW,UAAkB,KAAA,EAA8B;AACzE,EAAA,OAAO,EAAE,IAAA,EAAM,EAAC,EAAG,UAAU,KAAA,EAAM;AACrC;AAUA,SAAS,KAAA,CAAM,QAAuB,KAAA,EAAkC;AACtE,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,CAAO,QAAA;AAC9B,EAAA,OAAO,OAAO,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,KAAM,IAAI,MAAM,CAAA;AAC7C;AAgBO,SAAS,MAAA,CACd,QACA,KAAA,EACmC;AACnC,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA;AAClC,EAAA,IAAI,MAAA,CAAO,MAAA,GAAS,MAAA,CAAO,KAAA,EAAO;AAChC,IAAA,OAAO,CAAC,EAAE,GAAG,MAAA,EAAQ,IAAA,EAAM,CAAC,GAAG,MAAA,EAAQ,KAAK,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,EACvD;AACA,EAAA,OAAO,CAAC,EAAE,GAAG,QAAQ,IAAA,EAAM,MAAA,IAAU,KAAK,CAAA;AAC5C;AAQO,SAAS,SAAA,CAAU,QAAuB,KAAA,EAAuB;AACtE,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA,CAAE,MAAA;AAClC,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,IAAI,CAAA;AACxC","file":"chunk-S2EGRPCM.js","sourcesContent":["/**\n * @demlik/tea/rate-limit — two rate limiters as pure state + ops.\n *\n * Same shape as `@demlik/tea/work-queue`'s `ops.ts`: a state type plus pure\n * transition functions. Host-agnostic by construction — no `Date.now()`, no\n * timers, no I/O. The caller injects the clock (`nowMs`) and wires the result\n * into its own reducer / interpret. This keeps the limiters inside TEA's\n * invariant 2 (transitions are pure: same inputs → same outputs, no `Date.now`)\n * so a consumer can fold them through `update` and exercise them with `replay`\n * / the pbt fold without a fake clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). The `tryConsume` / `record` ops return a\n * `[next, ok]` tuple so the caller branches on the decision while threading the\n * advanced state forward; `refill` / `remaining` are read-shaped helpers.\n *\n * Two complementary algorithms, picked by the cost the caller wants to pay:\n *\n * - **Token bucket** — O(1) state (four numbers), smooths bursts against a\n * steady refill rate. Allows a burst up to `capacity`, then throttles to\n * `refillPerSec`. Cheapest; the default choice.\n * - **Sliding window log** — O(limit) state (one timestamp per in-window\n * hit), exact \"no more than `limit` events per `windowMs`\". Pay the memory\n * for precise window edges when a token bucket's continuous refill would\n * let through too many at a boundary.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/rate-limit`\n * subpath, same one-shape-per-package rule as `work-queue` and `do`.\n */\n\n// ===========================================================================\n// Token bucket\n// ===========================================================================\n\n/**\n * A token bucket: `tokens` of `capacity` available now, replenished at\n * `refillPerSec` tokens per second. `lastRefillMs` is the clock reading at\n * which `tokens` was last computed — `refill` advances it. All four fields are\n * plain numbers so the whole bucket is trivially JSON-serializable into a\n * consumer's `Store<S>`.\n */\nexport interface TokenBucket {\n readonly tokens: number;\n readonly capacity: number;\n readonly refillPerSec: number;\n readonly lastRefillMs: number;\n}\n\n/**\n * Create a full bucket. Starts at `capacity` tokens (the conventional \"a fresh\n * limiter lets the first burst through\" behavior) and stamps `lastRefillMs`\n * with the caller's clock so the first `refill` measures elapsed time from\n * boot, not from epoch 0.\n */\nexport function initBucket(\n capacity: number,\n refillPerSec: number,\n nowMs: number,\n): TokenBucket {\n return {\n tokens: capacity,\n capacity,\n refillPerSec,\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Add the tokens accrued since `lastRefillMs`, clamped to `capacity`, and\n * advance `lastRefillMs` to `nowMs`. PURE — returns a new bucket; the input is\n * untouched.\n *\n * A `nowMs` at or before `lastRefillMs` (clock not advanced, or skewed\n * backwards) adds a non-positive amount; `Math.max(0, …)` floors the elapsed\n * term so a backwards clock can never *remove* tokens. `lastRefillMs` still\n * advances to `nowMs` so the bucket tracks the latest reading the caller\n * presented — the next forward step measures from there, not from a stale\n * future timestamp.\n */\nexport function refill(bucket: TokenBucket, nowMs: number): TokenBucket {\n const elapsedSec = Math.max(0, nowMs - bucket.lastRefillMs) / 1000;\n const replenished = bucket.tokens + elapsedSec * bucket.refillPerSec;\n return {\n ...bucket,\n tokens: Math.min(bucket.capacity, replenished),\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Refill against `nowMs`, then attempt to take `n` tokens (default 1).\n *\n * On success returns `[next, true]` with `n` subtracted; on failure returns\n * `[refilled, false]` — the refilled bucket (so the caller keeps the advanced\n * clock + accrued tokens) with no tokens spent. Branch on the boolean, thread\n * the bucket forward regardless:\n *\n * const [b1, ok] = tryConsume(bucket, nowMs);\n * return ok ? [{ ...state, bucket: b1 }, [doWork]] : [{ ...state, bucket: b1 }, []];\n *\n * PURE — input bucket is never mutated.\n */\nexport function tryConsume(\n bucket: TokenBucket,\n nowMs: number,\n n = 1,\n): readonly [TokenBucket, boolean] {\n const refilled = refill(bucket, nowMs);\n if (refilled.tokens >= n) {\n return [{ ...refilled, tokens: refilled.tokens - n }, true];\n }\n return [refilled, false];\n}\n\n// ===========================================================================\n// Sliding window log\n// ===========================================================================\n\n/**\n * A sliding-window log: the timestamps of every hit still inside the trailing\n * `windowMs`, capped at `limit` events per window. `hits` is `readonly` and\n * kept oldest-first (pruning slices the stale prefix off the front, recording\n * appends to the back), so the array is its own sorted index. Exact at the\n * window edge — unlike a token bucket's continuous refill, a hit leaves the\n * count the instant it ages past `windowMs`.\n */\nexport interface SlidingWindow {\n readonly hits: readonly number[];\n readonly windowMs: number;\n readonly limit: number;\n}\n\n/** Create an empty window. No hits recorded yet. */\nexport function initWindow(windowMs: number, limit: number): SlidingWindow {\n return { hits: [], windowMs, limit };\n}\n\n/**\n * The window-edge rule, in one place: keep only the hits still live at `nowMs`.\n * The cutoff is half-open — a hit exactly `windowMs` old is dropped (`> cutoff`,\n * not `>=`), so a hit recorded at `t` no longer counts at `t + windowMs` and the\n * window's capacity fully recovers `windowMs` after the last hit. `record` (the\n * writer) and `remaining` (the read helper) both prune through here so a change\n * to the edge semantics can't leave the reader disagreeing with the writer.\n */\nfunction prune(window: SlidingWindow, nowMs: number): readonly number[] {\n const cutoff = nowMs - window.windowMs;\n return window.hits.filter((t) => t > cutoff);\n}\n\n/**\n * Prune hits older than `nowMs - windowMs`, then record `nowMs` if there is\n * room. Returns `[next, true]` with the hit appended when `hits.length < limit`\n * after pruning, else `[pruned, false]` — the pruned window (capacity recovers\n * as old hits age out) with nothing appended.\n *\n * The cutoff is half-open (see {@link prune}): a hit exactly `windowMs` old is\n * pruned, so a hit recorded at `t` no longer counts at `t + windowMs`. That\n * makes the window's capacity fully recover `windowMs` after the last hit, the\n * behavior tests pin.\n *\n * PURE — input window is never mutated; `hits` is rebuilt by `filter` +\n * spread.\n */\nexport function record(\n window: SlidingWindow,\n nowMs: number,\n): readonly [SlidingWindow, boolean] {\n const pruned = prune(window, nowMs);\n if (pruned.length < window.limit) {\n return [{ ...window, hits: [...pruned, nowMs] }, true];\n }\n return [{ ...window, hits: pruned }, false];\n}\n\n/**\n * How many more hits `record` would accept at `nowMs` without blocking —\n * `limit` minus the live (post-prune) hit count, floored at 0. Read-only: it\n * prunes a local copy to answer but returns no new window. Use it to render a\n * \"N requests remaining\" affordance without committing a hit.\n */\nexport function remaining(window: SlidingWindow, nowMs: number): number {\n const live = prune(window, nowMs).length;\n return Math.max(0, window.limit - live);\n}\n"]}
@@ -1,55 +0,0 @@
1
- // src/paginator/index.ts
2
- var defaultPaginatorPolicy = {
3
- firstCursor: 0,
4
- highWaterMark: 1e3
5
- };
6
- function initPaginator() {
7
- return { phase: "idle", seen: 0, pages: 0 };
8
- }
9
- function start(state, policy) {
10
- if (state.phase !== "idle") return state;
11
- return {
12
- phase: "fetching",
13
- cursor: policy.firstCursor,
14
- seen: state.seen,
15
- pages: state.pages
16
- };
17
- }
18
- function recordPage(state, count, next, policy) {
19
- if (state.phase !== "fetching") return state;
20
- const safeCount = Number.isFinite(count) ? Math.max(0, count) : 0;
21
- const seen = state.seen + safeCount;
22
- const pages = state.pages + 1;
23
- if (next == null) {
24
- return { phase: "done", seen, pages };
25
- }
26
- if (policy.highWaterMark > 0 && seen >= policy.highWaterMark) {
27
- return { phase: "paused", cursor: next, seen, pages };
28
- }
29
- return { phase: "fetching", cursor: next, seen, pages };
30
- }
31
- function drain(state, n) {
32
- if (n <= 0) return state;
33
- const seen = Math.max(0, state.seen - n);
34
- if (seen === state.seen) return state;
35
- return { ...state, seen };
36
- }
37
- function resume(state, policy) {
38
- if (state.phase !== "paused") return state;
39
- if (state.seen < policy.highWaterMark) {
40
- return {
41
- phase: "fetching",
42
- cursor: state.cursor,
43
- seen: state.seen,
44
- pages: state.pages
45
- };
46
- }
47
- return state;
48
- }
49
- function isDone(state) {
50
- return state.phase === "done";
51
- }
52
-
53
- export { defaultPaginatorPolicy, drain, initPaginator, isDone, recordPage, resume, start };
54
- //# sourceMappingURL=chunk-SINXE47Y.js.map
55
- //# sourceMappingURL=chunk-SINXE47Y.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/paginator/index.ts"],"names":[],"mappings":";AAmJO,IAAM,sBAAA,GAAkD;AAAA,EAC7D,WAAA,EAAa,CAAA;AAAA,EACb,aAAA,EAAe;AACjB;AAqCO,SAAS,aAAA,GAAgD;AAC9D,EAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,CAAA,EAAG,OAAO,CAAA,EAAE;AAC5C;AAWO,SAAS,KAAA,CACd,OACA,MAAA,EACwB;AACxB,EAAA,IAAI,KAAA,CAAM,KAAA,KAAU,MAAA,EAAQ,OAAO,KAAA;AACnC,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,UAAA;AAAA,IACP,QAAQ,MAAA,CAAO,WAAA;AAAA,IACf,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,OAAO,KAAA,CAAM;AAAA,GACf;AACF;AAgCO,SAAS,UAAA,CACd,KAAA,EACA,KAAA,EACA,IAAA,EACA,MAAA,EACwB;AACxB,EAAA,IAAI,KAAA,CAAM,KAAA,KAAU,UAAA,EAAY,OAAO,KAAA;AAcvC,EAAA,MAAM,SAAA,GAAY,OAAO,QAAA,CAAS,KAAK,IAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAK,CAAA,GAAI,CAAA;AAChE,EAAA,MAAM,IAAA,GAAO,MAAM,IAAA,GAAO,SAAA;AAC1B,EAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,GAAQ,CAAA;AAM5B,EAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,KAAA,EAAM;AAAA,EACtC;AAKA,EAAA,IAAI,MAAA,CAAO,aAAA,GAAgB,CAAA,IAAK,IAAA,IAAQ,OAAO,aAAA,EAAe;AAC5D,IAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,MAAA,EAAQ,IAAA,EAAM,MAAM,KAAA,EAAM;AAAA,EACtD;AACA,EAAA,OAAO,EAAE,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,IAAA,EAAM,MAAM,KAAA,EAAM;AACxD;AAkBO,SAAS,KAAA,CACd,OACA,CAAA,EACwB;AACxB,EAAA,IAAI,CAAA,IAAK,GAAG,OAAO,KAAA;AACnB,EAAA,MAAM,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,OAAO,CAAC,CAAA;AACvC,EAAA,IAAI,IAAA,KAAS,KAAA,CAAM,IAAA,EAAM,OAAO,KAAA;AAChC,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,IAAA,EAAK;AAC1B;AAoBO,SAAS,MAAA,CACd,OACA,MAAA,EACwB;AACxB,EAAA,IAAI,KAAA,CAAM,KAAA,KAAU,QAAA,EAAU,OAAO,KAAA;AACrC,EAAA,IAAI,KAAA,CAAM,IAAA,GAAO,MAAA,CAAO,aAAA,EAAe;AACrC,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,UAAA;AAAA,MACP,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,MAAM,KAAA,CAAM,IAAA;AAAA,MACZ,OAAO,KAAA,CAAM;AAAA,KACf;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAQO,SAAS,OAAe,KAAA,EAAwC;AACrE,EAAA,OAAO,MAAM,KAAA,KAAU,MAAA;AACzB","file":"chunk-SINXE47Y.js","sourcesContent":["/**\n * @demlik/tea/paginator — the cursor/offset/page-token walk loop as pure\n * state + ops.\n *\n * Same shape as `@demlik/tea/retry-backoff`, `@demlik/tea/circuit-breaker`,\n * and `@demlik/tea/rate-limit`: a state type plus pure transition functions,\n * host-agnostic by construction. Nothing here reads the clock, the RNG, or any\n * I/O on its own behalf — the consumer fetches a page out-of-band and folds the\n * *result* back in through `recordPage`. That keeps every function inside TEA's\n * invariant 2 (transitions are pure: same inputs → same outputs) so a consumer\n * can thread the walk through `update` and exercise it with `replay` / the pbt\n * fold without a fake network.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). All fields are `readonly` and plain data, so\n * the whole walk is trivially JSON-serializable into a consumer's `Store<S>`\n * and survives a Durable-Object eviction / reload mid-walk: the cursor lives in\n * the Model, not a closure (the durability + replay non-negotiables every TEA\n * knob preserves).\n *\n * ## The cursor is opaque, and the consumer advances it\n *\n * A paginated API hands back a *next cursor* however it likes: an offset\n * (`?offset=40`), a page token (`?pageToken=ey…`), a \"next\" URL, or `null` at\n * the end. This module is generic in `Cursor` and never inspects it — it only\n * carries it forward. The consumer (who alone knows the API's shape) reads the\n * next cursor off the just-fetched page and passes it to `recordPage`:\n *\n * const next = page.nextPageToken ?? null; // consumer-specific extraction\n * const walk = recordPage(state.walk, page.items.length, next);\n *\n * Passing `next: null` (or a missing `undefined` cursor — the op compares with\n * loose `== null`) means \"the API said there is no next page\" → the walk\n * finishes (`done`). Passing a `Cursor` means \"fetch this one next\" → the walk\n * either keeps `fetching` or `pauses` for backpressure (below). Decoupling the\n * extraction from the op is the same discipline `circuit-breaker` uses for the\n * clock: the impure / domain-specific bit (`nextCursor(page)`) lives at the\n * call site; the op stays a pure fold over the extracted value.\n *\n * ## The phase machine\n *\n * Four phases, each a discriminated-union arm carrying ONLY the data that phase\n * needs so impossible states are unrepresentable (pattern 11):\n *\n * - **idle** — created, not started. No cursor is armed yet; `start` arms the\n * first one. Carries the running `seen` / `pages` counters (both 0 here, but\n * present on every phase so a consumer reads them uniformly).\n *\n * - **fetching** — a page request for `cursor` is outstanding. The walk sits\n * here while the consumer's fetch is in flight; `recordPage` resolves it.\n * Carries the `cursor` being fetched so a post-eviction resume re-issues the\n * SAME request (idempotent retry) rather than skipping a page.\n *\n * - **paused** — backpressure tripped. The API still has more pages (a non-null\n * next cursor), but `seen` has reached `highWaterMark` and the consumer has\n * not drained yet. Carries the `cursor` to resume from. No fetch is\n * outstanding — the loop is deliberately idle until `resume`. This is the\n * valve that stops a fast producer (the API) from outrunning a slow consumer\n * (whatever processes each page) and blowing memory.\n *\n * - **done** — the API returned a null next cursor; the walk is exhausted. A\n * terminal phase: `start` / `recordPage` / `resume` are all no-ops on it\n * (idempotent completion — a late duplicate result can't un-finish a walk).\n *\n * ### Transition table\n *\n * Rows are the current phase; columns are the op. `→` denotes the next phase;\n * `self` means unchanged (a no-op for that (phase × op) pair). `next` is the\n * cursor the consumer extracted from the page; `hwm` is `highWaterMark`.\n *\n * | from \\ op | start | recordPage(count, next) | resume |\n * | ----------- | -------------- | -------------------------------------------------------------------- | ------------------------------ |\n * | idle | → fetching(c0) | self *(¹)* | self |\n * | fetching | self | seen=max(0,seen+count), pages+=1; next==null → done; seen≥hwm → paused(next); else → fetching(next) | self |\n * | paused | self | self *(¹)* | seen<hwm → fetching(cursor) *(²)* |\n * | done | self | self | self |\n *\n * *(¹)* `recordPage` only makes sense while `fetching` (a page was outstanding).\n * On any other phase it is a no-op: a result that arrives when no fetch was in\n * flight (a late duplicate, a double-dispatch) must not advance the cursor or\n * double-count `seen`. The walk is a coordination aid, not a correctness gate —\n * it absorbs the stray rather than corrupting the count.\n *\n * *(²)* `resume` re-arms the paused cursor ONLY once `seen` has dropped below\n * `highWaterMark` (the consumer drained, lowering `seen` via `drain`). Calling\n * `resume` while still over the mark is a no-op — the valve stays shut until\n * there is real headroom, so a consumer that resumes optimistically can't\n * defeat the backpressure. Resuming a walk that isn't `paused` is likewise a\n * no-op.\n *\n * ## Backpressure\n *\n * `seen` is the count of items the API has produced that the consumer has not\n * yet acknowledged as processed. `recordPage` adds each page's item count to it;\n * `drain(n)` subtracts what the consumer finished. When `seen` reaches\n * `highWaterMark`, `recordPage` parks the walk in `paused` instead of issuing\n * the next fetch — the producer stops until `drain` + `resume` re-open the\n * valve. A `highWaterMark` of `0` (or any non-positive value) disables\n * backpressure: the comparison `seen >= hwm` is satisfied vacuously only when\n * `hwm <= 0` would pause immediately, so we treat `hwm <= 0` as \"unbounded\" and\n * never pause. The walk then runs flat-out, `fetching → fetching` until `done`.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/paginator`\n * subpath, same one-shape-per-package rule as `work-queue`, `retry-backoff`,\n * `circuit-breaker`, and `rate-limit`. It is the L1 escape hatch the\n * `paginated-walk` composition builds its walk loop on.\n */\n\n/**\n * Paginator policy — pure configuration, no mutable state. One policy is shared\n * across the whole walk (often a module-scope const). `PaginatorState` carries\n * the cursor + counters; this carries the knobs they're compared against.\n *\n * Generic in `Cursor` so `firstCursor` matches whatever the API's cursor shape\n * is — an offset `number`, a page-token `string`, a `{ after: string }` record,\n * etc. The module never inspects the cursor; the type just threads through.\n */\nexport interface PaginatorPolicy<Cursor> {\n /**\n * The cursor the walk fetches first, armed by `start`. For an offset API this\n * is typically `0`; for a token API, `null`-or-empty-meaning-\"first page\" is\n * the API's call, so pass whatever sentinel the API treats as page one.\n */\n readonly firstCursor: Cursor;\n /**\n * Backpressure threshold: the number of un-drained items at which the walk\n * pauses instead of fetching the next page. `recordPage` parks in `paused`\n * once `seen >= highWaterMark`; `drain` + `resume` re-open the valve.\n *\n * A non-positive value (`<= 0`) disables backpressure — the walk never pauses\n * and runs `fetching → fetching` until the API exhausts. Use that when the\n * consumer processes each page synchronously inside `recordPage` and there is\n * nothing to back up.\n */\n readonly highWaterMark: number;\n}\n\n/**\n * Sensible defaults: start from a numeric offset of `0`, pause after 1000\n * un-drained items. Tuned for an offset-paginated REST API whose consumer\n * processes pages asynchronously; override per call site (especially\n * `firstCursor`, which is API-specific) rather than mutating this shared const.\n *\n * Typed as `PaginatorPolicy<number>` because the only honest default cursor is\n * the numeric offset `0`; a token-based API supplies its own `firstCursor` and\n * its own `PaginatorPolicy<string>`.\n */\nexport const defaultPaginatorPolicy: PaginatorPolicy<number> = {\n firstCursor: 0,\n highWaterMark: 1000,\n};\n\n/**\n * The walk's phase. A discriminated union on `phase` so each phase carries ONLY\n * its own data and impossible combinations are unrepresentable (pattern 11):\n * there is no `done` state that still holds a pending cursor, no `idle` state\n * mid-fetch. Every field is `readonly` so the whole value is trivially\n * JSON-serializable into a consumer's `Store<S>` and safe to thread through a\n * pure reducer / a Durable-Object reload.\n *\n * Two counters ride on every phase so a consumer reads them uniformly without\n * narrowing first:\n * - `seen` — items produced by the API but not yet drained (backpressure gauge).\n * - `pages` — total pages recorded so far (progress / observability).\n *\n * - `idle` — not started; `start` arms `firstCursor`.\n * - `fetching` — a page request for `cursor` is outstanding.\n * - `paused` — backpressure tripped; `cursor` is the page to resume from.\n * - `done` — API exhausted (null next cursor); terminal.\n */\nexport type PaginatorState<Cursor> =\n | { readonly phase: \"idle\"; readonly seen: number; readonly pages: number }\n | {\n readonly phase: \"fetching\";\n readonly cursor: Cursor;\n readonly seen: number;\n readonly pages: number;\n }\n | {\n readonly phase: \"paused\";\n readonly cursor: Cursor;\n readonly seen: number;\n readonly pages: number;\n }\n | { readonly phase: \"done\"; readonly seen: number; readonly pages: number };\n\n/** The starting state: idle, nothing seen, no pages recorded. */\nexport function initPaginator<Cursor>(): PaginatorState<Cursor> {\n return { phase: \"idle\", seen: 0, pages: 0 };\n}\n\n/**\n * Arm the first fetch. PURE — returns a new state; the input is never mutated.\n *\n * - `idle` → `fetching(firstCursor)`. This is the only phase `start` acts on.\n * - any other phase → unchanged. Re-arming a walk that has already begun (or\n * finished) would skip back to page one and double-count; a stray `start`\n * after launch is absorbed, not honored. Use `initPaginator()` to begin a\n * genuinely fresh walk.\n */\nexport function start<Cursor>(\n state: PaginatorState<Cursor>,\n policy: PaginatorPolicy<Cursor>,\n): PaginatorState<Cursor> {\n if (state.phase !== \"idle\") return state;\n return {\n phase: \"fetching\",\n cursor: policy.firstCursor,\n seen: state.seen,\n pages: state.pages,\n };\n}\n\n/**\n * Record the result of the outstanding fetch and decide what comes next. PURE —\n * returns a new state; the input is never mutated.\n *\n * `count` is how many items the just-fetched page yielded (added to `seen` for\n * backpressure accounting). `next` is the cursor the consumer extracted from the\n * page — a `Cursor` to fetch next, or `null`/`undefined` (a missing optional\n * cursor field) if the API signalled the end.\n *\n * - `fetching` → `seen = max(0, seen + count)`, `pages += 1`, then branch on\n * `next`:\n * - `next == null` → `done` (API exhausted; loose-null so a\n * missing/`undefined` cursor also finishes).\n * - `seen >= highWaterMark` → `paused(next)` (backpressure; `hwm <= 0`\n * disables this branch — see policy docs).\n * - otherwise → `fetching(next)` (keep walking).\n * - any other phase → unchanged. A result with no fetch outstanding (a late\n * duplicate, a double-dispatch) must not advance the cursor or double-count;\n * it is absorbed. See the module header's note ¹.\n *\n * `count` is floored into the gauge: a non-finite `count` (`NaN`/`Infinity`) is\n * treated as `0`, and the additive sum is clamped at `0`. A record can only hold\n * or raise `seen`; only `drain` lowers it. This keeps a stray negative or\n * non-finite count from poisoning the `seen >= highWaterMark` comparison and\n * silently defeating backpressure.\n *\n * The new `seen` total — not the pre-page value — is what the backpressure check\n * reads, so a single page large enough to cross the mark pauses immediately\n * rather than fetching one more first.\n */\nexport function recordPage<Cursor>(\n state: PaginatorState<Cursor>,\n count: number,\n next: Cursor | null | undefined,\n policy: PaginatorPolicy<Cursor>,\n): PaginatorState<Cursor> {\n if (state.phase !== \"fetching\") return state;\n // A page's item count must be a finite, non-negative addition to `seen`. A\n // negative count would drive `seen` down (a record can never lower the gauge —\n // only `drain` does) and a non-finite count (`NaN` / `Infinity`) would poison\n // every later `seen >= highWaterMark` comparison, permanently defeating\n // backpressure (NaN compares false, so the valve never trips). Floor the\n // CONTRIBUTION (not just the sum) at 0: clamp a non-finite count to 0\n // (mirroring retry-backoff's `Number.isNaN` discipline at backoffDelay ~:119)\n // and a negative count to 0. Flooring the contribution — rather than the sum —\n // is what makes a record strictly monotonic: `Math.max(0, seen + count)` only\n // catches a count that drags the *sum* below zero, so a small negative count\n // (e.g. `seen=10, count=-3 → 7`) would still silently lower the gauge and\n // break \"only `drain` lowers `seen`\". Clamping `count` first guarantees the\n // additive step can only hold or raise the gauge.\n const safeCount = Number.isFinite(count) ? Math.max(0, count) : 0;\n const seen = state.seen + safeCount;\n const pages = state.pages + 1;\n // Loose `== null` so a *missing* next cursor (`undefined`, common for an\n // optional API field the consumer extracted as `page.nextPageToken`) finishes\n // the walk exactly like an explicit `null`. Strict `=== null` would route an\n // `undefined` cursor into the fetching/paused branch and carry a cursor-less\n // (malformed) state forward.\n if (next == null) {\n return { phase: \"done\", seen, pages };\n }\n // `hwm <= 0` disables backpressure entirely (unbounded walk); only a positive\n // mark can park the loop. `seen >= hwm` is the half-open edge — reaching the\n // mark exactly pauses, matching the `>=` window edges `rate-limit` /\n // `circuit-breaker` use.\n if (policy.highWaterMark > 0 && seen >= policy.highWaterMark) {\n return { phase: \"paused\", cursor: next, seen, pages };\n }\n return { phase: \"fetching\", cursor: next, seen, pages };\n}\n\n/**\n * Acknowledge that the consumer finished processing `n` items, lowering the\n * backpressure gauge. PURE — returns a new state; the input is never mutated.\n *\n * Subtracts `n` from `seen`, floored at 0 (a consumer that over-acks, or acks a\n * page recorded before a reload, can never drive `seen` negative). `n <= 0` is a\n * no-op on the count. `drain` does NOT change the phase — it only frees\n * headroom; `resume` is the separate, explicit decision to re-issue a fetch once\n * the headroom exists. Splitting \"I drained\" from \"I want to continue\" lets a\n * consumer drain in several steps and resume once.\n *\n * Works on every phase: a consumer may drain while still `fetching` (the gauge\n * just drops, no pause was reached) or while `paused` (freeing room for a later\n * `resume`). On `done` the gauge can still be drained for accurate final\n * accounting, though it no longer gates anything.\n */\nexport function drain<Cursor>(\n state: PaginatorState<Cursor>,\n n: number,\n): PaginatorState<Cursor> {\n if (n <= 0) return state;\n const seen = Math.max(0, state.seen - n);\n if (seen === state.seen) return state;\n return { ...state, seen };\n}\n\n/**\n * Re-open the backpressure valve after a `drain`. PURE — returns a new state;\n * the input is never mutated.\n *\n * - `paused` AND `seen < highWaterMark` → `fetching(cursor)`: the consumer has\n * drained below the mark, so re-issue the parked fetch.\n * - `paused` but `seen >= highWaterMark` → unchanged. There is still no\n * headroom; the valve stays shut until a `drain` lowers `seen`. A consumer\n * that resumes optimistically can't defeat the backpressure. (When `hwm <=\n * 0` backpressure is disabled, so the walk never reaches `paused` and this\n * branch is unreachable — but the guard reads `< hwm` consistently, and a\n * non-positive `hwm` makes `seen < hwm` false for any non-negative `seen`,\n * correctly refusing to resume a state that should never have paused.)\n * - any other phase → unchanged. Resuming a walk that isn't paused (idle,\n * fetching, done) is a no-op.\n *\n * See the module header's note ².\n */\nexport function resume<Cursor>(\n state: PaginatorState<Cursor>,\n policy: PaginatorPolicy<Cursor>,\n): PaginatorState<Cursor> {\n if (state.phase !== \"paused\") return state;\n if (state.seen < policy.highWaterMark) {\n return {\n phase: \"fetching\",\n cursor: state.cursor,\n seen: state.seen,\n pages: state.pages,\n };\n }\n return state;\n}\n\n/**\n * Whether the walk has finished (the API returned a null next cursor). PURE,\n * read-only. A derived predicate so a consumer's reducer can branch\n * (`if (isDone(walk)) return [doneState, [joinCmd]]`) without re-checking the\n * `phase` discriminant by hand at every call site.\n */\nexport function isDone<Cursor>(state: PaginatorState<Cursor>): boolean {\n return state.phase === \"done\";\n}\n"]}