@demlik/tea 0.1.2 → 0.3.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 (212) hide show
  1. package/README.md +49 -0
  2. package/dist/agent/index.d.ts +16 -770
  3. package/dist/agent/index.js +18 -16
  4. package/dist/authed-call/index.d.ts +3 -3
  5. package/dist/authed-call/index.js +22 -36
  6. package/dist/authed-call/index.js.map +1 -1
  7. package/dist/await-terminal/index.d.ts +2 -2
  8. package/dist/await-terminal/index.js +2 -2
  9. package/dist/batch-window/index.d.ts +3 -3
  10. package/dist/batch-window/index.js +3 -3
  11. package/dist/cache/index.d.ts +2 -2
  12. package/dist/cache/index.js +5 -2
  13. package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
  14. package/dist/chunk-25LBDCBC.js.map +1 -0
  15. package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
  16. package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
  17. package/dist/{chunk-MMH7CW5K.js → chunk-BP2R7TPP.js} +216 -157
  18. package/dist/chunk-BP2R7TPP.js.map +1 -0
  19. package/dist/chunk-C6QC273X.js +71 -0
  20. package/dist/chunk-C6QC273X.js.map +1 -0
  21. package/dist/{chunk-7W4LMPUW.js → chunk-CAXMQJJ3.js} +17 -24
  22. package/dist/chunk-CAXMQJJ3.js.map +1 -0
  23. package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
  24. package/dist/chunk-D736QUGF.js.map +1 -0
  25. package/dist/chunk-EWJP3YYP.js +21 -0
  26. package/dist/chunk-EWJP3YYP.js.map +1 -0
  27. package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
  28. package/dist/chunk-FV3Z5MBE.js.map +1 -0
  29. package/dist/chunk-G5E74IJB.js +8 -0
  30. package/dist/chunk-G5E74IJB.js.map +1 -0
  31. package/dist/{chunk-27N2SPTN.js → chunk-IXBKFQH5.js} +12 -11
  32. package/dist/chunk-IXBKFQH5.js.map +1 -0
  33. package/dist/{chunk-SQTCS44W.js → chunk-JFFVBEDI.js} +3 -3
  34. package/dist/{chunk-SQTCS44W.js.map → chunk-JFFVBEDI.js.map} +1 -1
  35. package/dist/chunk-LYLDCIPH.js +365 -0
  36. package/dist/chunk-LYLDCIPH.js.map +1 -0
  37. package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
  38. package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
  39. package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
  40. package/dist/chunk-MUUROGIH.js.map +1 -0
  41. package/dist/{chunk-EHIX7NYZ.js → chunk-MVY5DAMF.js} +4 -4
  42. package/dist/{chunk-EHIX7NYZ.js.map → chunk-MVY5DAMF.js.map} +1 -1
  43. package/dist/{chunk-LP7VQBRG.js → chunk-NPERUUUE.js} +76 -95
  44. package/dist/chunk-NPERUUUE.js.map +1 -0
  45. package/dist/{chunk-QLA76GIT.js → chunk-PBDYW2TU.js} +68 -9
  46. package/dist/chunk-PBDYW2TU.js.map +1 -0
  47. package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
  48. package/dist/chunk-Q2UM7BXU.js.map +1 -0
  49. package/dist/chunk-RMX4EOQK.js +26 -0
  50. package/dist/chunk-RMX4EOQK.js.map +1 -0
  51. package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
  52. package/dist/chunk-S2EGRPCM.js.map +1 -0
  53. package/dist/{chunk-ON24RKSE.js → chunk-S4O26D7B.js} +7 -14
  54. package/dist/chunk-S4O26D7B.js.map +1 -0
  55. package/dist/{chunk-3XJWC7VW.js → chunk-SK5JGUY7.js} +3 -3
  56. package/dist/{chunk-3XJWC7VW.js.map → chunk-SK5JGUY7.js.map} +1 -1
  57. package/dist/{chunk-5DEQ5I3U.js → chunk-TPGASTFF.js} +8 -10
  58. package/dist/chunk-TPGASTFF.js.map +1 -0
  59. package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
  60. package/dist/chunk-UF4MZSLT.js.map +1 -0
  61. package/dist/{chunk-PS23H6KQ.js → chunk-VBUCHVCV.js} +6 -5
  62. package/dist/chunk-VBUCHVCV.js.map +1 -0
  63. package/dist/{chunk-S3FQN4SL.js → chunk-WGGAMW7Y.js} +191 -178
  64. package/dist/chunk-WGGAMW7Y.js.map +1 -0
  65. package/dist/chunk-ZJ5AOYBH.js +8 -0
  66. package/dist/chunk-ZJ5AOYBH.js.map +1 -0
  67. package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
  68. package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
  69. package/dist/circuit-breaker/index.d.ts +1 -0
  70. package/dist/circuit-breaker/index.js +1 -1
  71. package/dist/{core-BoB6t7dB.d.ts → core-42kapW0n.d.ts} +105 -6
  72. package/dist/deadline/index.d.ts +77 -11
  73. package/dist/deadline/index.js +3 -3
  74. package/dist/devtools/index.js +8 -28
  75. package/dist/devtools/index.js.map +1 -1
  76. package/dist/do/index.d.ts +483 -339
  77. package/dist/do/index.js +244 -214
  78. package/dist/do/index.js.map +1 -1
  79. package/dist/{durable-effects-CO9Uh971.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
  80. package/dist/extension/index.d.ts +25 -7
  81. package/dist/extension/index.js +4 -3
  82. package/dist/extension/index.js.map +1 -1
  83. package/dist/extension/react.js +4 -3
  84. package/dist/extension/react.js.map +1 -1
  85. package/dist/extension/subs/index.d.ts +2 -2
  86. package/dist/extension/subs/index.js +7 -10
  87. package/dist/extension/subs/index.js.map +1 -1
  88. package/dist/fan-out/index.d.ts +10 -10
  89. package/dist/fan-out/index.js +3 -3
  90. package/dist/idempotent-intake/index.d.ts +2 -2
  91. package/dist/idempotent-intake/index.js +3 -3
  92. package/dist/index.d.ts +50 -472
  93. package/dist/index.js +2 -2
  94. package/dist/llm-call/index.d.ts +3 -2
  95. package/dist/llm-call/index.js +12 -10
  96. package/dist/machine-B_BF4jij.d.ts +804 -0
  97. package/dist/machine-viz/index.d.ts +1 -1
  98. package/dist/machine-viz/index.js +2 -2
  99. package/dist/mem/index.d.ts +3 -2
  100. package/dist/mem/index.js.map +1 -1
  101. package/dist/monitored-run/index.d.ts +54 -30
  102. package/dist/monitored-run/index.js +7 -7
  103. package/dist/node/index.d.ts +3 -2
  104. package/dist/node/index.js +2 -3
  105. package/dist/node/index.js.map +1 -1
  106. package/dist/paginated-walk/index.d.ts +2 -2
  107. package/dist/paginated-walk/index.js +11 -10
  108. package/dist/parity/index.d.ts +6 -5
  109. package/dist/parity/index.js +5 -4
  110. package/dist/parity/index.js.map +1 -1
  111. package/dist/pbt/arbitraries/index.d.ts +1 -1
  112. package/dist/pbt/index.d.ts +1 -1
  113. package/dist/pbt/index.js +3 -3
  114. package/dist/pbt/index.js.map +1 -1
  115. package/dist/pbt/runners/index.d.ts +1 -1
  116. package/dist/pbt/runners/index.js +3 -3
  117. package/dist/poller/index.d.ts +59 -17
  118. package/dist/poller/index.js +56 -20
  119. package/dist/poller/index.js.map +1 -1
  120. package/dist/prediction/index.d.ts +1 -1
  121. package/dist/prediction/index.js +2 -2
  122. package/dist/pure/index.d.ts +1 -1
  123. package/dist/pure/index.js +2 -2
  124. package/dist/rate-limit/index.d.ts +2 -2
  125. package/dist/rate-limit/index.js +1 -1
  126. package/dist/react/index.d.ts +3 -2
  127. package/dist/react/index.js +2 -2
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/reconciler/index.d.ts +2 -2
  130. package/dist/reconciler/index.js +13 -15
  131. package/dist/reconciler/index.js.map +1 -1
  132. package/dist/recorder/index.d.ts +20 -20
  133. package/dist/recorder/index.js +1 -1
  134. package/dist/resilient-call/index.d.ts +31 -6
  135. package/dist/resilient-call/index.js +10 -9
  136. package/dist/retry-backoff/index.d.ts +191 -14
  137. package/dist/retry-backoff/index.js +1 -1
  138. package/dist/retry-to-success/index.js +1 -1
  139. package/dist/runtime-types-gsog27b6.d.ts +337 -0
  140. package/dist/saga/index.d.ts +3 -2
  141. package/dist/saga/index.js +2 -2
  142. package/dist/saga/index.js.map +1 -1
  143. package/dist/snapshot/index.d.ts +1 -1
  144. package/dist/snapshot/index.js +3 -3
  145. package/dist/subs/index.d.ts +372 -4
  146. package/dist/subs/index.js +4 -1
  147. package/dist/testing/index.d.ts +2 -2
  148. package/dist/testing/index.js +2 -2
  149. package/dist/throttled-input/index.d.ts +2 -2
  150. package/dist/throttled-input/index.js +14 -13
  151. package/dist/throttled-input/index.js.map +1 -1
  152. package/dist/token-refresh/index.d.ts +1 -1
  153. package/dist/token-refresh/index.js +3 -3
  154. package/dist/trace-replay/index.d.ts +12 -7
  155. package/dist/trace-replay/index.js +4 -3
  156. package/dist/{types-B-8bcAp8.d.ts → types-KbL3W1mj.d.ts} +1 -1
  157. package/dist/with-deadline/index.d.ts +1 -1
  158. package/dist/with-deadline/index.js +2 -2
  159. package/dist/with-resilience/index.d.ts +21 -2
  160. package/dist/with-resilience/index.js +12 -23
  161. package/dist/with-resilience/index.js.map +1 -1
  162. package/dist/with-telemetry/index.d.ts +1 -1
  163. package/dist/with-telemetry/index.js +2 -2
  164. package/dist/work-queue/adapter.d.ts +2 -2
  165. package/dist/work-queue/adapter.js +2 -2
  166. package/dist/work-queue/index.d.ts +3 -2
  167. package/dist/work-queue/index.js +1 -1
  168. package/dist/work-queue/index.js.map +1 -1
  169. package/dist/work-queue/ops.d.ts +2 -2
  170. package/dist/work-queue/ops.js +1 -1
  171. package/dist/workflow/index.d.ts +51 -19
  172. package/dist/workflow/index.js +71 -52
  173. package/dist/workflow/index.js.map +1 -1
  174. package/package.json +4 -19
  175. package/dist/chunk-27N2SPTN.js.map +0 -1
  176. package/dist/chunk-2YOE5PIS.js.map +0 -1
  177. package/dist/chunk-3545BOYL.js.map +0 -1
  178. package/dist/chunk-5DEQ5I3U.js.map +0 -1
  179. package/dist/chunk-5VNWZADP.js.map +0 -1
  180. package/dist/chunk-6RGBKODV.js +0 -268
  181. package/dist/chunk-6RGBKODV.js.map +0 -1
  182. package/dist/chunk-7W4LMPUW.js.map +0 -1
  183. package/dist/chunk-DILMKOZ7.js +0 -91
  184. package/dist/chunk-DILMKOZ7.js.map +0 -1
  185. package/dist/chunk-K7Q23EQ4.js +0 -20
  186. package/dist/chunk-K7Q23EQ4.js.map +0 -1
  187. package/dist/chunk-LP7VQBRG.js.map +0 -1
  188. package/dist/chunk-MMH7CW5K.js.map +0 -1
  189. package/dist/chunk-NSDFA6M6.js +0 -198
  190. package/dist/chunk-NSDFA6M6.js.map +0 -1
  191. package/dist/chunk-ON24RKSE.js.map +0 -1
  192. package/dist/chunk-PS23H6KQ.js.map +0 -1
  193. package/dist/chunk-QLA76GIT.js.map +0 -1
  194. package/dist/chunk-S3FQN4SL.js.map +0 -1
  195. package/dist/chunk-SC3FAFZN.js.map +0 -1
  196. package/dist/chunk-TGG26XMK.js.map +0 -1
  197. package/dist/chunk-UV6UWGEO.js.map +0 -1
  198. package/dist/chunk-UYWUVEP6.js.map +0 -1
  199. package/dist/chunk-VLGARN6H.js +0 -202
  200. package/dist/chunk-VLGARN6H.js.map +0 -1
  201. package/dist/raft/demo.d.ts +0 -105
  202. package/dist/raft/demo.js +0 -172
  203. package/dist/raft/demo.js.map +0 -1
  204. package/dist/raft/index.d.ts +0 -548
  205. package/dist/raft/index.js +0 -7
  206. package/dist/raft/index.js.map +0 -1
  207. package/dist/raft/sim.d.ts +0 -222
  208. package/dist/raft/sim.js +0 -8
  209. package/dist/raft/sim.js.map +0 -1
  210. package/dist/raft/timeline.d.ts +0 -46
  211. package/dist/raft/timeline.js +0 -62
  212. package/dist/raft/timeline.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recorder/index.ts"],"names":[],"mappings":";AAqMO,SAAS,QAAA,CACd,SACA,IAAA,EACgB;AAIhB,EAAA,MAAM,UAAU,IAAA,EAAM,UAAA;AACtB,EAAA,MAAM,UAAA,GACJ,OAAA,KAAY,MAAA,IAAa,MAAA,CAAO,KAAA,CAAM,OAAO,CAAA,IAAK,OAAA,IAAW,CAAA,GACzD,CAAA,GACA,IAAA,CAAK,GAAA,CAAI,SAAS,CAAC,CAAA;AACzB,EAAA,MAAM,YAAA,GAAe,MAAM,YAAA,IAAgB,KAAA;AAK3C,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI,OAAA,GAAU,KAAA;AAGd,EAAA,MAAM,OAAY,EAAC;AAEnB,EAAA,MAAM,QAAgC,EAAC;AAKvC,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI,OAAA,GAAU,KAAA;AAMd,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,CAAO,CAAC,KAAA,KAAU;AACvC,IAAA,UAAA,GAAa,KAAA;AACb,IAAA,MAAA,GAAS,KAAA;AACT,IAAA,OAAA,GAAU,IAAA;AAAA,EACZ,CAAC,CAAA;AAED,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAK,KAAA,KAAU;AAGhD,IAAA,UAAA,GAAa,KAAA;AAKb,IAAA,IAAI,UAAA,GAAa,CAAA,IAAK,IAAA,CAAK,MAAA,MAAY,UAAA,EAAY;AAEnD,IAAA,IAAA,CAAK,KAAK,GAAG,CAAA;AACb,IAAA,IAAI,cAAc,KAAA,CAAM,IAAA,CAAK,EAAE,GAAA,EAAK,OAAO,CAAA;AAAA,EAC7C,CAAC,CAAA;AAED,EAAA,OAAO;AAAA,IACL,IAAA,GAAoB;AAClB,MAAA,IAAI,eAAe,MAAA,EAAW;AAC5B,QAAA,MAAM,IAAI,KAAA;AAAA,UACR;AAAA,SAEF;AAAA,MACF;AACA,MAAA,OAAO;AAAA;AAAA;AAAA,QAGL,MAAA,EAAQ,UAAW,MAAA,GAAe,IAAA;AAAA,QAClC,IAAA,EAAM,KAAK,KAAA,EAAM;AAAA,QACjB,UAAA;AAAA;AAAA;AAAA,QAGA,GAAI,eAAe,EAAE,KAAA,EAAO,MAAM,KAAA,EAAM,KAAM;AAAC,OACjD;AAAA,IACF,CAAA;AAAA,IACA,OAAA,GAAkB;AAChB,MAAA,IAAI,eAAe,MAAA,EAAW;AAC5B,QAAA,MAAM,IAAI,KAAA;AAAA,UACR;AAAA,SAEF;AAAA,MACF;AAQA,MAAA,OAAO,YAAA,CAAa,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,IACjC,CAAA;AAAA,IACA,IAAA,GAAa;AACX,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,SAAA,EAAU;AACV,MAAA,MAAA,EAAO;AAAA,IACT;AAAA,GACF;AACF;AA4BO,SAAS,WAAiB,KAAA,EAA4B;AAC3D,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AACjC,EAAA,IAAI,MAAA,GAAmB,IAAA;AACvB,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,MAAM,OAAY,EAAC;AACnB,EAAA,MAAM,QAAgC,EAAC;AACvC,EAAA,IAAI,UAAA,GAAuB,IAAA;AAC3B,EAAA,IAAI,UAAA,GAAa,KAAA;AAEjB,EAAA,KAAA,MAAW,CAAC,CAAA,EAAG,OAAO,CAAA,IAAK,QAAA,CAAS,SAAQ,EAAG;AAC7C,IAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAC1B,IAAA,IAAI,SAAS,EAAA,EAAI;AAEjB,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,IAC1B,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,mDAAmD,CAAA,GAAI,CAAC,CAAA,kBAAA,EAAqB,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,OAC1F;AAAA,IACF;AAEA,IAAA,IAAI,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC1B,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,kEAAA,EAAqE,IAAI,CAAC,CAAA,oCAAA;AAAA,SAE5E;AAAA,MACF;AACA,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,MAAA,GAAS,MAAA,CAAO,KAAA;AAGhB,MAAA,UAAA,GAAa,MAAA,CAAO,KAAA;AACpB,MAAA,UAAA,GAAa,OAAO,KAAA,KAAU,IAAA;AAAA,IAChC,CAAA,MAAA,IAAW,MAAA,CAAO,IAAA,KAAS,MAAA,EAAQ;AACjC,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,gFAAA,EAAmF,IAAI,CAAC,CAAA,CAAA;AAAA,SAC1F;AAAA,MACF;AACA,MAAA,IAAA,CAAK,IAAA,CAAK,OAAO,GAAG,CAAA;AACpB,MAAA,IAAI,MAAA,CAAO,UAAU,IAAA,EAAM;AACzB,QAAA,KAAA,CAAM,IAAA,CAAK,EAAE,GAAA,EAAK,MAAA,CAAO,KAAK,KAAA,EAAO,MAAA,CAAO,OAAO,CAAA;AACnD,QAAA,UAAA,GAAa,MAAA,CAAO,KAAA;AACpB,QAAA,UAAA,GAAa,IAAA;AAAA,MACf;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,qEAAqE,CAAA,GAAI,CAAC,OACxE,IAAA,CAAK,SAAA,CAAW,OAA8B,IAAI;AAAA,OACtD;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,IAAI,CAAC,UAAA,IAAc,UAAA,KAAe,IAAA,EAAM;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,UAAA,EAAY,KAAA,EAAM;AAC3C;AAyDO,SAAS,gBACd,KAAA,EACiB;AACjB,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,iBAAA;AAAA,IACV,IAAA,EAAM,aAAa,KAAK,CAAA;AAAA,IACxB,WAAA,EAAa;AAAA,GACf;AACF;AA2DO,SAAS,oBAAA,CACd,OACA,IAAA,EACmB;AACnB,EAAA,MAAM,WAAW,IAAA,EAAM,KAAA;AAIvB,EAAA,MAAM,KAAA,GACJ,QAAA,KAAa,MAAA,IAAa,MAAA,CAAO,MAAM,QAAQ,CAAA,GAC3C,GAAA,GACA,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAC,CAAA;AACtC,EAAA,MAAM,YAAY,IAAA,EAAM,SAAA;AAMxB,EAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,GAAG,KAAA,CAAM,IAAA,CAAK,SAAS,KAAK,CAAA;AACnD,EAAA,MAAM,IAAA,GAAO,UAAU,CAAA,GAAI,KAAA,CAAM,OAAO,KAAA,CAAM,IAAA,CAAK,MAAM,KAAK,CAAA;AAE9D,EAAA,MAAM,SAA4B,EAAC;AACnC,EAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,QAAA,EAAU,KAAA;AAAA,MACV,IAAA,EAAM,SAAA;AAAA,MACN,KAAA,EAAO,MAAA;AAAA,MACP,OAAA,EAAS,SAAA,GAAY,SAAA,CAAU,GAAG,IAAI,GAAA,CAAI,IAAA;AAAA;AAAA;AAAA;AAAA,MAI1C,IAAA,EAAM,EAAE,GAAA;AAA+C,KACxD,CAAA;AAAA,EACH;AACA,EAAA,OAAO,MAAA;AACT;AAgBA,SAAS,aAAmB,KAAA,EAA4B;AACtD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,QAAQ,KAAA,EAAO,KAAA,CAAM,MAAA,EAAQ,CAAC,CAAA;AAEhE,EAAA,MAAM,QAAQ,KAAA,CAAM,KAAA;AACpB,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AAC1C,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA;AACxB,IAAA,MAAM,SAAA,GAAY,KAAA,GACd,KAAA,CAAM,CAAC,CAAA,EAAG,KAAA,GACV,CAAA,KAAM,KAAA,CAAM,IAAA,CAAK,MAAA,GAAS,CAAA,GACxB,KAAA,CAAM,UAAA,GACN,IAAA;AACN,IAAA,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,QAAQ,GAAA,EAAK,KAAA,EAAO,SAAA,EAAW,CAAC,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB","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,5 +1,5 @@
1
- import { tryInterpret } from './chunk-MMH7CW5K.js';
2
- import { Cmd } from './chunk-QLA76GIT.js';
1
+ import { tryInterpret } from './chunk-BP2R7TPP.js';
2
+ import { Cmd } from './chunk-PBDYW2TU.js';
3
3
 
4
4
  // src/snapshot/index.ts
5
5
  var DEFAULT_SNAPSHOT_KEY = "@@snapshot";
@@ -89,5 +89,5 @@ function createSnapshot(config) {
89
89
  }
90
90
 
91
91
  export { createSnapshot, snapshotFailed, snapshotLoadFailed, snapshotLoaded, snapshotSaved };
92
- //# sourceMappingURL=chunk-EHIX7NYZ.js.map
93
- //# sourceMappingURL=chunk-EHIX7NYZ.js.map
92
+ //# sourceMappingURL=chunk-MVY5DAMF.js.map
93
+ //# sourceMappingURL=chunk-MVY5DAMF.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-EHIX7NYZ.js","sourcesContent":["/**\n * @demlik/tea/snapshot — periodic state checkpoint to a host store (R2/KV-shaped).\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: this is the same knob contract every L2 composition follows —\n * `createSnapshot(config)` hands back a `{ init, <verbs>, handlers }` bundle you\n * splice into your machine. The state it owns is a Model slice (durable +\n * replayable, the two non-negotiables); the verbs are PURE decisions; the I/O\n * (the actual `put` to the store) lives only in `handlers`.\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd` / `Interpret`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The Store port is deliberately ABSTRACT — `{ get, put }`, the lowest common\n * shape of R2 and KV — so this module never imports `@cloudflare/workers-types`\n * and can be checkpointed to anything (R2, KV, a DO sub-key, an in-memory map in\n * tests). The host binds the concrete adapter at `handlers({ store })`.\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_saved: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_loaded: (s, msg) => {\n * if (msg.payload === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.payload, snapshot }, cmds];\n * },\n * snapshot_load_failed: (s) => [s, []], // errors are data — decide here\n *\n * // splice the I/O — both effect cells (write + load):\n * interpret: { ...snap.handlers({ store: r2Adapter }) },\n */\n\nimport { Cmd, type Interpret, tryInterpret } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Store port — abstract R2/KV-shaped checkpoint sink.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The abstract checkpoint sink — the intersection of R2's and KV's surface,\n * narrowed to exactly the two methods a rolling checkpoint needs. Deliberately\n * NOT the substrate's `Store<S>` (`{ load, save, migrate }`): that is the\n * runtime's whole-state persistence boundary keyed at a fixed cell, whereas a\n * checkpoint store is keyed, value-typed, and host-supplied.\n *\n * - `put(key, value)` — write the checkpoint. Last-write-wins on `key`.\n * - `get(key)` — read the latest checkpoint back, or `null` if none. Used by\n * a recovery path to seed `init`'s `loaded` from the durable checkpoint\n * when the runtime's own `Store<S>` blob is gone.\n *\n * No `@cloudflare/workers-types` import — an R2 bucket, a KV namespace, a DO\n * sub-key, or an in-memory `Map` all satisfy this shape with a 3-line adapter\n * at the host.\n */\nexport interface SnapshotStore<V> {\n get(key: string): Promise<V | null>;\n put(key: string, value: V): Promise<void>;\n}\n\n/**\n * The ports `handlers` binds. Just the store today — named so a future port\n * (a metrics sink, a clock) can grow without breaking the `handlers(ports)`\n * call shape.\n */\nexport interface SnapshotPorts<V> {\n readonly store: SnapshotStore<V>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmd + Msg — the write effect and its durable acknowledgement.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided. The handler performs the `put` and routes the result back to a\n * `confirm` / fail Msg.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n */\nexport type SnapshotWriteCmd<V> = Cmd<\"snapshot_write\"> & {\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n};\n\n/**\n * The Msg the write handler dispatches on a SUCCESSFUL `put`. Fold it back\n * through `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. The\n * `seq` matches the emitting Cmd so a late acknowledgement of an older write\n * can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg = {\n readonly type: \"snapshot_saved\";\n readonly seq: number;\n readonly at: number;\n};\n\n/**\n * The Msg the write handler dispatches on a FAILED `put`. Errors are data\n * (never swallowed): the consumer's reducer decides whether a failed checkpoint\n * is fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `seq` identifies which checkpoint failed; `error` is carried, never\n * interpreted here.\n */\nexport type SnapshotFailedMsg = {\n readonly type: \"snapshot_failed\";\n readonly seq: number;\n readonly error: unknown;\n};\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler performs `store.get(key)` and routes the result back to a\n * `snapshot_loaded` (Ok, payload-or-null) / `snapshot_load_failed` (Err) Msg.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport type SnapshotLoadCmd = Cmd<\"snapshot_load\"> & {\n readonly key: string;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` RESOLVES. `payload` is\n * the recovered checkpoint value, or `null` when the store has no checkpoint\n * under `key` (fresh boot — never checkpointed, or a wiped store). The consumer\n * folds this into `init`'s loaded branch: `payload === null` → keep the fresh\n * `init` state; non-null → seed the run slice from the durable checkpoint and\n * `boot` the snapshot slice's cadence counter.\n *\n * A `null` payload is NOT an error — \"no checkpoint yet\" is a normal first-run\n * state, distinct from \"the read failed\" (`snapshot_load_failed`).\n */\nexport type SnapshotLoadedMsg<V> = {\n readonly type: \"snapshot_loaded\";\n readonly payload: V | null;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` REJECTS. Errors are data\n * (never swallowed — the field-guide rule): a failed recovery read is a visible\n * signal the consumer's reducer decides on (fatal-abort, retry via\n * `@demlik/tea/retry-backoff`, or boot-fresh-and-log). `error` is carried, never\n * interpreted here. Kept separate from `snapshot_loaded`'s `null` so \"the store\n * has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg = {\n readonly type: \"snapshot_load_failed\";\n readonly error: unknown;\n};\n\n/** Construct a `snapshot_saved` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotSaved(seq: number, at: number): SnapshotSavedMsg {\n return { type: \"snapshot_saved\", seq, at };\n}\n\n/** Construct a `snapshot_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotFailed(seq: number, error: unknown): SnapshotFailedMsg {\n return { type: \"snapshot_failed\", seq, error };\n}\n\n/** Construct a `snapshot_loaded` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoaded<V>(payload: V | null): SnapshotLoadedMsg<V> {\n return { type: \"snapshot_loaded\", payload };\n}\n\n/** Construct a `snapshot_load_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoadFailed(error: unknown): SnapshotLoadFailedMsg {\n return { type: \"snapshot_load_failed\", error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns — the uniform knob contract. `subs` is\n * omitted: a checkpoint is driven by progress (a `record` call from the\n * consumer's reducer), not by a timer, so this module has no subscriptions.\n */\nexport interface SnapshotKnob<V> {\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged `seq` / `at`, but only forward: an\n * out-of-order acknowledgement of an OLDER `seq` (a slow write landing after a\n * newer one) is ignored, so the confirmed watermark is monotonic. Fold the\n * `snapshot_saved` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_saved` cell without the caller\n * re-wrapping a bare State.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint (`get` from the store), so the\n * cadence counter starts fresh from there. `seq` / `lastSavedSeq` /\n * `lastSavedAt` are preserved (they describe what is durable, which a reload\n * does not change). Call from a `boot` Msg the host dispatches once after\n * `run(...)`, never from `init`'s rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_loaded` cell without the caller\n * re-wrapping a bare State.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_loaded` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)`, folds `requestLoad` here, and the emitted Cmd's handler\n * reads `store.get(key)` and dispatches `snapshot_loaded` / `snapshot_load_failed`.\n *\n * Returns `readonly [SnapshotState, readonly Cmd[]]` (the reducer-cell shape)\n * so it drops straight into a `boot: (s) => snap.requestLoad(s)` cell.\n */\n requestLoad(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * The `interpret` map for this knob's two effect Cmds:\n *\n * - `snapshot_write` — binds the store and performs the `put`, routing\n * Ok → `snapshot_saved` / Err → `snapshot_failed`.\n * - `snapshot_load` — binds the store and performs the `get`, routing\n * Ok → `snapshot_loaded` (payload-or-null) / Err → `snapshot_load_failed`.\n *\n * Railway throughout — neither effect throws into the runtime; the outcome is\n * always a Msg. Spread into your machine's `interpret`.\n */\n handlers(\n ports: SnapshotPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n >;\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `handlers({ store })` supplies the write `interpret` cell.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n const cmd: SnapshotWriteCmd<V> = {\n type: \"snapshot_write\",\n key,\n seq,\n at,\n payload,\n };\n return [next, [cmd]];\n }\n\n return {\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, Cmd.none];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [\n { ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },\n Cmd.none,\n ];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_loaded` (the consumer's reducer), not here.\n const cmd: SnapshotLoadCmd = { type: \"snapshot_load\", key };\n return [state, [cmd]];\n },\n\n handlers(ports) {\n const { store } = ports;\n return {\n // The write effect. Performs the `put`; on success the saved `seq`/`at`\n // ride the `snapshot_saved` Msg back into `confirm`; on failure the\n // error rides `snapshot_failed`. Railway via the core's `tryInterpret`\n // (matching every other module's handler) — the handler always resolves\n // with a Msg, never rejects into the runtime.\n snapshot_write: tryInterpret<\n SnapshotWriteCmd<V>,\n void,\n SnapshotSavedMsg | SnapshotFailedMsg,\n unknown\n >(\n (cmd) => store.put(cmd.key, cmd.payload),\n (_value, cmd) => snapshotSaved(cmd.seq, cmd.at),\n (error, cmd) => snapshotFailed(cmd.seq, error),\n ),\n // The read effect — the recovery half. Performs the `get`; on success\n // the recovered payload (or `null` when no checkpoint exists) rides the\n // `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;\n // on failure the error rides `snapshot_load_failed`. Same Railway\n // discipline via `tryInterpret` — the read never rejects into the runtime.\n snapshot_load: tryInterpret<\n SnapshotLoadCmd,\n V | null,\n SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg,\n unknown\n >(\n (cmd) => store.get(cmd.key),\n (payload) => snapshotLoaded(payload),\n (error) => snapshotLoadFailed(error),\n ),\n };\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/snapshot/index.ts"],"names":[],"mappings":"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","file":"chunk-MVY5DAMF.js","sourcesContent":["/**\n * @demlik/tea/snapshot — periodic state checkpoint to a host store (R2/KV-shaped).\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: this is the same knob contract every L2 composition follows —\n * `createSnapshot(config)` hands back a `{ init, <verbs>, handlers }` bundle you\n * splice into your machine. The state it owns is a Model slice (durable +\n * replayable, the two non-negotiables); the verbs are PURE decisions; the I/O\n * (the actual `put` to the store) lives only in `handlers`.\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd` / `Interpret`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The Store port is deliberately ABSTRACT — `{ get, put }`, the lowest common\n * shape of R2 and KV — so this module never imports `@cloudflare/workers-types`\n * and can be checkpointed to anything (R2, KV, a DO sub-key, an in-memory map in\n * tests). The host binds the concrete adapter at `handlers({ store })`.\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_saved: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_loaded: (s, msg) => {\n * if (msg.payload === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.payload, snapshot }, cmds];\n * },\n * snapshot_load_failed: (s) => [s, []], // errors are data — decide here\n *\n * // splice the I/O — both effect cells (write + load):\n * interpret: { ...snap.handlers({ store: r2Adapter }) },\n */\n\nimport { Cmd, type Interpret, tryInterpret } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Store port — abstract R2/KV-shaped checkpoint sink.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The abstract checkpoint sink — the intersection of R2's and KV's surface,\n * narrowed to exactly the two methods a rolling checkpoint needs. Deliberately\n * NOT the substrate's `Store<S>` (`{ load, save, migrate }`): that is the\n * runtime's whole-state persistence boundary keyed at a fixed cell, whereas a\n * checkpoint store is keyed, value-typed, and host-supplied.\n *\n * - `put(key, value)` — write the checkpoint. Last-write-wins on `key`.\n * - `get(key)` — read the latest checkpoint back, or `null` if none. Used by\n * a recovery path to seed `init`'s `loaded` from the durable checkpoint\n * when the runtime's own `Store<S>` blob is gone.\n *\n * No `@cloudflare/workers-types` import — an R2 bucket, a KV namespace, a DO\n * sub-key, or an in-memory `Map` all satisfy this shape with a 3-line adapter\n * at the host.\n */\nexport interface SnapshotStore<V> {\n get(key: string): Promise<V | null>;\n put(key: string, value: V): Promise<void>;\n}\n\n/**\n * The ports `handlers` binds. Just the store today — named so a future port\n * (a metrics sink, a clock) can grow without breaking the `handlers(ports)`\n * call shape.\n */\nexport interface SnapshotPorts<V> {\n readonly store: SnapshotStore<V>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmd + Msg — the write effect and its durable acknowledgement.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided. The handler performs the `put` and routes the result back to a\n * `confirm` / fail Msg.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n */\nexport type SnapshotWriteCmd<V> = Cmd<\"snapshot_write\"> & {\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n};\n\n/**\n * The Msg the write handler dispatches on a SUCCESSFUL `put`. Fold it back\n * through `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. The\n * `seq` matches the emitting Cmd so a late acknowledgement of an older write\n * can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg = {\n readonly type: \"snapshot_saved\";\n readonly seq: number;\n readonly at: number;\n};\n\n/**\n * The Msg the write handler dispatches on a FAILED `put`. Errors are data\n * (never swallowed): the consumer's reducer decides whether a failed checkpoint\n * is fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `seq` identifies which checkpoint failed; `error` is carried, never\n * interpreted here.\n */\nexport type SnapshotFailedMsg = {\n readonly type: \"snapshot_failed\";\n readonly seq: number;\n readonly error: unknown;\n};\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler performs `store.get(key)` and routes the result back to a\n * `snapshot_loaded` (Ok, payload-or-null) / `snapshot_load_failed` (Err) Msg.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport type SnapshotLoadCmd = Cmd<\"snapshot_load\"> & {\n readonly key: string;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` RESOLVES. `payload` is\n * the recovered checkpoint value, or `null` when the store has no checkpoint\n * under `key` (fresh boot — never checkpointed, or a wiped store). The consumer\n * folds this into `init`'s loaded branch: `payload === null` → keep the fresh\n * `init` state; non-null → seed the run slice from the durable checkpoint and\n * `boot` the snapshot slice's cadence counter.\n *\n * A `null` payload is NOT an error — \"no checkpoint yet\" is a normal first-run\n * state, distinct from \"the read failed\" (`snapshot_load_failed`).\n */\nexport type SnapshotLoadedMsg<V> = {\n readonly type: \"snapshot_loaded\";\n readonly payload: V | null;\n};\n\n/**\n * The Msg the load handler dispatches when `store.get` REJECTS. Errors are data\n * (never swallowed — the field-guide rule): a failed recovery read is a visible\n * signal the consumer's reducer decides on (fatal-abort, retry via\n * `@demlik/tea/retry-backoff`, or boot-fresh-and-log). `error` is carried, never\n * interpreted here. Kept separate from `snapshot_loaded`'s `null` so \"the store\n * has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg = {\n readonly type: \"snapshot_load_failed\";\n readonly error: unknown;\n};\n\n/** Construct a `snapshot_saved` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotSaved(seq: number, at: number): SnapshotSavedMsg {\n return { type: \"snapshot_saved\", seq, at };\n}\n\n/** Construct a `snapshot_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotFailed(seq: number, error: unknown): SnapshotFailedMsg {\n return { type: \"snapshot_failed\", seq, error };\n}\n\n/** Construct a `snapshot_loaded` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoaded<V>(payload: V | null): SnapshotLoadedMsg<V> {\n return { type: \"snapshot_loaded\", payload };\n}\n\n/** Construct a `snapshot_load_failed` Msg. Shared by the handler and tests so the wire shape can't drift. */\nexport function snapshotLoadFailed(error: unknown): SnapshotLoadFailedMsg {\n return { type: \"snapshot_load_failed\", error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns — the uniform knob contract. `subs` is\n * omitted: a checkpoint is driven by progress (a `record` call from the\n * consumer's reducer), not by a timer, so this module has no subscriptions.\n */\nexport interface SnapshotKnob<V> {\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged `seq` / `at`, but only forward: an\n * out-of-order acknowledgement of an OLDER `seq` (a slow write landing after a\n * newer one) is ignored, so the confirmed watermark is monotonic. Fold the\n * `snapshot_saved` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_saved` cell without the caller\n * re-wrapping a bare State.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint (`get` from the store), so the\n * cadence counter starts fresh from there. `seq` / `lastSavedSeq` /\n * `lastSavedAt` are preserved (they describe what is durable, which a reload\n * does not change). Call from a `boot` Msg the host dispatches once after\n * `run(...)`, never from `init`'s rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_loaded` cell without the caller\n * re-wrapping a bare State.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_loaded` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)`, folds `requestLoad` here, and the emitted Cmd's handler\n * reads `store.get(key)` and dispatches `snapshot_loaded` / `snapshot_load_failed`.\n *\n * Returns `readonly [SnapshotState, readonly Cmd[]]` (the reducer-cell shape)\n * so it drops straight into a `boot: (s) => snap.requestLoad(s)` cell.\n */\n requestLoad(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * The `interpret` map for this knob's two effect Cmds:\n *\n * - `snapshot_write` — binds the store and performs the `put`, routing\n * Ok → `snapshot_saved` / Err → `snapshot_failed`.\n * - `snapshot_load` — binds the store and performs the `get`, routing\n * Ok → `snapshot_loaded` (payload-or-null) / Err → `snapshot_load_failed`.\n *\n * Railway throughout — neither effect throws into the runtime; the outcome is\n * always a Msg. Spread into your machine's `interpret`.\n */\n handlers(\n ports: SnapshotPorts<V>,\n ): Interpret<\n | SnapshotSavedMsg\n | SnapshotFailedMsg\n | SnapshotLoadedMsg<V>\n | SnapshotLoadFailedMsg,\n SnapshotWriteCmd<V> | SnapshotLoadCmd,\n unknown\n >;\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `handlers({ store })` supplies the write `interpret` cell.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly Cmd[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n const cmd: SnapshotWriteCmd<V> = {\n type: \"snapshot_write\",\n key,\n seq,\n at,\n payload,\n };\n return [next, [cmd]];\n }\n\n return {\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, Cmd.none];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && msg.seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [\n { ...state, lastSavedSeq: msg.seq, lastSavedAt: msg.at },\n Cmd.none,\n ];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_loaded` (the consumer's reducer), not here.\n const cmd: SnapshotLoadCmd = { type: \"snapshot_load\", key };\n return [state, [cmd]];\n },\n\n handlers(ports) {\n const { store } = ports;\n return {\n // The write effect. Performs the `put`; on success the saved `seq`/`at`\n // ride the `snapshot_saved` Msg back into `confirm`; on failure the\n // error rides `snapshot_failed`. Railway via the core's `tryInterpret`\n // (matching every other module's handler) — the handler always resolves\n // with a Msg, never rejects into the runtime.\n snapshot_write: tryInterpret<\n SnapshotWriteCmd<V>,\n void,\n SnapshotSavedMsg | SnapshotFailedMsg,\n unknown\n >(\n (cmd) => store.put(cmd.key, cmd.payload),\n (_value, cmd) => snapshotSaved(cmd.seq, cmd.at),\n (error, cmd) => snapshotFailed(cmd.seq, error),\n ),\n // The read effect — the recovery half. Performs the `get`; on success\n // the recovered payload (or `null` when no checkpoint exists) rides the\n // `snapshot_loaded` Msg the consumer folds into `init`'s loaded branch;\n // on failure the error rides `snapshot_load_failed`. Same Railway\n // discipline via `tryInterpret` — the read never rejects into the runtime.\n snapshot_load: tryInterpret<\n SnapshotLoadCmd,\n V | null,\n SnapshotLoadedMsg<V> | SnapshotLoadFailedMsg,\n unknown\n >(\n (cmd) => store.get(cmd.key),\n (payload) => snapshotLoaded(payload),\n (error) => snapshotLoadFailed(error),\n ),\n };\n },\n };\n}\n"]}
@@ -1,9 +1,15 @@
1
- import { historyTracker } from './chunk-MMH7CW5K.js';
1
+ import { describeError } from './chunk-G5E74IJB.js';
2
+ import { historyTracker } from './chunk-BP2R7TPP.js';
2
3
 
3
4
  // src/extension/bridge.ts
4
- function bridgeRuntime(runtime, { channel, serialize, historySize }) {
5
- const dispatchType = `${channel}:dispatch`;
6
- const hydrateType = `${channel}:hydrate`;
5
+ function channelTypes(channel) {
6
+ return {
7
+ dispatchType: `${channel}:dispatch`,
8
+ hydrateType: `${channel}:hydrate`
9
+ };
10
+ }
11
+ function bridgeRuntime(runtime, { channel, parseMsg, serialize, historySize }) {
12
+ const { dispatchType, hydrateType } = channelTypes(channel);
7
13
  const project = serialize ?? ((state) => state);
8
14
  const tracker = historySize !== void 0 && historySize > 0 ? historyTracker(runtime, historySize) : null;
9
15
  const sendBroadcast = (msg, state) => {
@@ -37,13 +43,17 @@ function bridgeRuntime(runtime, { channel, serialize, historySize }) {
37
43
  return false;
38
44
  }
39
45
  if (obj.type === dispatchType) {
40
- const msg = obj.msg;
46
+ const msg = parseMsg(obj.msg);
47
+ if (msg === null) {
48
+ sendResponse({ ok: false, error: "dispatch msg failed parse" });
49
+ return false;
50
+ }
41
51
  runtime.dispatch(msg).then(() => {
42
52
  sendResponse({ ok: true });
43
53
  }).catch((err) => {
44
54
  sendResponse({
45
55
  ok: false,
46
- error: err instanceof Error ? err.message : String(err)
56
+ error: describeError(err)
47
57
  });
48
58
  });
49
59
  return true;
@@ -61,96 +71,43 @@ function bridgeRuntime(runtime, { channel, serialize, historySize }) {
61
71
  function passThroughMsg(raw) {
62
72
  return raw;
63
73
  }
64
- function bridgeClient({
65
- channel,
66
- parseState,
67
- parseMsg
68
- }) {
69
- const dispatchType = `${channel}:dispatch`;
70
- const hydrateType = `${channel}:hydrate`;
71
- let latestState = null;
72
- const listeners = /* @__PURE__ */ new Set();
73
- const client = {
74
- async hydrate() {
75
- const req = { type: hydrateType };
76
- const res = await chrome.runtime.sendMessage(req);
77
- replayBacklog(res?.backlog, parseState, parseMsg, listeners, (next) => {
78
- latestState = next;
79
- });
80
- const raw = res?.state;
81
- if (raw === void 0 || raw === null) return null;
82
- const parsed = parseState(raw);
83
- if (parsed === null) return null;
84
- latestState = parsed;
85
- for (const l of listeners) l(null, parsed);
86
- return parsed;
87
- },
88
- async dispatch(msg) {
89
- const env = { type: dispatchType, msg };
90
- await chrome.runtime.sendMessage(env);
91
- },
92
- subscribe(listener) {
93
- listeners.add(listener);
94
- const onMessage = (message) => {
95
- if (!message || typeof message !== "object") return;
96
- const obj = message;
97
- if (obj.type !== channel) return;
98
- const state = parseState(obj.state);
99
- if (state === null) return;
100
- let msg;
101
- if (obj.msg === null || obj.msg === void 0) {
102
- msg = null;
103
- } else {
104
- msg = parseMsg(obj.msg);
105
- if (msg === null) return;
106
- }
107
- latestState = state;
108
- listener(msg, state);
109
- };
110
- chrome.runtime.onMessage.addListener(onMessage);
111
- return () => {
112
- chrome.runtime.onMessage.removeListener(onMessage);
113
- listeners.delete(listener);
114
- };
115
- },
116
- getSnapshot() {
117
- return latestState;
118
- }
119
- };
120
- return client;
74
+ function parseEnvelope(obj, parseState, parseMsg) {
75
+ const state = parseState(obj.state);
76
+ if (state === null) return null;
77
+ let msg;
78
+ if (obj.msg === null || obj.msg === void 0) {
79
+ msg = null;
80
+ } else {
81
+ msg = parseMsg(obj.msg);
82
+ if (msg === null) return null;
83
+ }
84
+ return { msg, state };
121
85
  }
122
86
  function replayBacklog(raw, parseState, parseMsg, listeners, commitState) {
123
87
  if (!Array.isArray(raw)) return;
124
88
  for (const entryRaw of raw) {
125
89
  if (!entryRaw || typeof entryRaw !== "object") continue;
126
90
  const entry = entryRaw;
127
- const state = parseState(entry.state);
128
- if (state === null) continue;
129
- let msg;
130
- if (entry.msg === null || entry.msg === void 0) {
131
- msg = null;
132
- } else {
133
- msg = parseMsg(entry.msg);
134
- if (msg === null) continue;
135
- }
136
- commitState(state);
137
- for (const l of listeners) l(msg, state);
91
+ const parsed = parseEnvelope(entry, parseState, parseMsg);
92
+ if (parsed === null) continue;
93
+ commitState(parsed.state);
94
+ for (const l of listeners) l(parsed.msg, parsed.state);
138
95
  }
139
96
  }
140
- function bridgeTabClient({
141
- tabId,
97
+ function makeBridgeClient({
142
98
  channel,
143
99
  parseState,
144
- parseMsg
100
+ parseMsg,
101
+ send,
102
+ senderMatches = () => true
145
103
  }) {
146
- const dispatchType = `${channel}:dispatch`;
147
- const hydrateType = `${channel}:hydrate`;
104
+ const { dispatchType, hydrateType } = channelTypes(channel);
148
105
  let latestState = null;
149
106
  const listeners = /* @__PURE__ */ new Set();
150
107
  const client = {
151
108
  async hydrate() {
152
109
  const req = { type: hydrateType };
153
- const res = await chrome.tabs.sendMessage(tabId, req);
110
+ const res = await send(req);
154
111
  replayBacklog(res?.backlog, parseState, parseMsg, listeners, (next) => {
155
112
  latestState = next;
156
113
  });
@@ -164,26 +121,19 @@ function bridgeTabClient({
164
121
  },
165
122
  async dispatch(msg) {
166
123
  const env = { type: dispatchType, msg };
167
- await chrome.tabs.sendMessage(tabId, env);
124
+ await send(env);
168
125
  },
169
126
  subscribe(listener) {
170
127
  listeners.add(listener);
171
128
  const onMessage = (message, sender) => {
172
- if (sender.tab?.id !== tabId) return;
129
+ if (!senderMatches(sender)) return;
173
130
  if (!message || typeof message !== "object") return;
174
131
  const obj = message;
175
132
  if (obj.type !== channel) return;
176
- const state = parseState(obj.state);
177
- if (state === null) return;
178
- let msg;
179
- if (obj.msg === null || obj.msg === void 0) {
180
- msg = null;
181
- } else {
182
- msg = parseMsg(obj.msg);
183
- if (msg === null) return;
184
- }
185
- latestState = state;
186
- listener(msg, state);
133
+ const parsed = parseEnvelope(obj, parseState, parseMsg);
134
+ if (parsed === null) return;
135
+ latestState = parsed.state;
136
+ listener(parsed.msg, parsed.state);
187
137
  };
188
138
  chrome.runtime.onMessage.addListener(onMessage);
189
139
  return () => {
@@ -197,7 +147,38 @@ function bridgeTabClient({
197
147
  };
198
148
  return client;
199
149
  }
150
+ function bridgeClient({
151
+ channel,
152
+ parseState,
153
+ parseMsg
154
+ }) {
155
+ return makeBridgeClient({
156
+ channel,
157
+ parseState,
158
+ parseMsg,
159
+ send: (message) => chrome.runtime.sendMessage(message)
160
+ });
161
+ }
162
+ function bridgeTabClient({
163
+ tabId,
164
+ channel,
165
+ parseState,
166
+ parseMsg
167
+ }) {
168
+ return makeBridgeClient({
169
+ channel,
170
+ parseState,
171
+ parseMsg,
172
+ // SW → content transport: `chrome.runtime.sendMessage` does NOT reach a
173
+ // tab's content script; `chrome.tabs.sendMessage(tabId, …)` does.
174
+ send: (message) => chrome.tabs.sendMessage(tabId, message),
175
+ // Every tab's content script broadcasts through the same SW
176
+ // `chrome.runtime.onMessage`; filter to this tab so a client subscribed
177
+ // to one tab never sees another's broadcasts.
178
+ senderMatches: (sender) => sender.tab?.id === tabId
179
+ });
180
+ }
200
181
 
201
182
  export { bridgeClient, bridgeRuntime, bridgeTabClient, passThroughMsg };
202
- //# sourceMappingURL=chunk-LP7VQBRG.js.map
203
- //# sourceMappingURL=chunk-LP7VQBRG.js.map
183
+ //# sourceMappingURL=chunk-NPERUUUE.js.map
184
+ //# sourceMappingURL=chunk-NPERUUUE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/extension/bridge.ts"],"names":[],"mappings":"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","file":"chunk-NPERUUUE.js","sourcesContent":["/**\n * Runtime bridge — expose a `@demlik/tea` runtime running in one chrome context\n * (typically the background service worker) to consumers in other contexts\n * (sidepanel, popup, devtools panel) over `chrome.runtime.sendMessage` /\n * `chrome.runtime.onMessage`.\n *\n * Two halves, one channel name string.\n *\n * ┌──── background (host) ────┐ ┌──── react surface ────┐\n * │ bridgeRuntime(rt, {ch}) │ chrome.* │ useBackgroundRuntime │\n * │ ├ observe → broadcast │ ──────────▶ │ ├ hydrate (initial) │\n * │ └ onMessage(\":dispatch\") │ ◀────────── │ └ dispatch (msgs) │\n * │ └ onMessage(\":hydrate\") │ │ │\n * └────────────────────────────┘ └───────────────────────┘\n *\n * Wire-shape (single `channel` string, three messages):\n *\n * { type: channel, msg, state } // background → surfaces (broadcast)\n * { type: `${channel}:hydrate` } // surface → background (request)\n * { type: `${channel}:dispatch`, msg } // surface → background (input)\n *\n * The `:hydrate` reply is the current `runtime.getState()` snapshot —\n * lets a newly-opened surface render the right view BEFORE the next\n * transition broadcast arrives.\n *\n * The bridge is intentionally agnostic to `chrome.runtime.sendMessage`\n * failure modes (no receiver, sender gone, etc.) — every cross-context send\n * is fire-and-forget with `.catch(() => {})`. Surfaces that need\n * delivery guarantees should layer their own retry, not bake it here.\n */\n\nimport { describeError } from \"../describe-error\";\nimport { type HistoryTracker, historyTracker, type Runtime } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Wire types — internal. Consumers don't import these directly; they pick\n// the channel string and the bridge handles serialization.\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface BroadcastEnvelope<S, M> {\n type: string; // === `channel`\n msg: M | null;\n state: S;\n}\n\ninterface DispatchEnvelope<M> {\n type: string; // === `${channel}:dispatch`\n msg: M;\n}\n\ninterface HydrateRequest {\n type: string; // === `${channel}:hydrate`\n}\n\n/**\n * Wire shape of one entry in the hydrate `backlog`. Mirrors a single\n * broadcast envelope, but without the `type` discriminator (the parent\n * `HydrateResponse` already identified the channel).\n */\ninterface BacklogEntry<V, M> {\n msg: M | null;\n state: V;\n}\n\n/**\n * Single source for the two derived wire-type strings. The `:dispatch` /\n * `:hydrate` suffix convention lives here and nowhere else — `bridgeRuntime`\n * and both clients derive their type constants from this so the convention\n * can't drift across the three functions (or the wire-shape comments above).\n */\nfunction channelTypes(channel: string): {\n dispatchType: string;\n hydrateType: string;\n} {\n return {\n dispatchType: `${channel}:dispatch`,\n hydrateType: `${channel}:hydrate`,\n };\n}\n\ninterface HydrateResponse<S, M> {\n state: S;\n /**\n * The runtime's history (most recent transitions, oldest first) projected\n * through `serialize` if provided. Present when the host runtime was\n * started with `historySize > 0`; an empty array otherwise. The surface\n * replays each entry through registered listeners before firing the\n * current-state marker, so a late subscriber sees the recent msg log AND\n * the current state on connect — not just the current state.\n */\n backlog: BacklogEntry<S, M>[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Background side — bridgeRuntime(runtime, { channel, serialize? })\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BridgeRuntimeOpts<S, M, V = S> {\n channel: string;\n /**\n * REQUIRED parse for inbound `msg` payloads on `:dispatch` envelopes.\n * Chrome runtime messages are an `unknown` boundary (Inv 8) — the same\n * JSON/chrome serialization seam the symmetric client side parses via\n * `BridgeClientOpts.parseMsg`. An untrusted surface can drive the host\n * reducer with an arbitrary `M`, so the host parses too — never casts. A\n * `msg` that parses to `null` is dropped: the dispatch never reaches the\n * reducer and the surface's `dispatch()` rejects with `{ ok: false }`.\n *\n * Hosts that trust the dispatch channel and treat the msg as opaque opt\n * out EXPLICITLY by passing the {@link passThroughMsg} helper — the unsound\n * cast is then a visible call-site decision, not a silent default.\n */\n parseMsg: (raw: unknown) => M | null;\n /**\n * Optional projection from the runtime's live state `S` to a JSON-safe\n * view `V` that goes over the wire (broadcast + hydrate). Required when\n * `S` carries non-serializable values (DOM refs, Map, Date, …).\n *\n * Identity passthrough by default — existing callers with POJO state pass\n * `serialize` unset and `V = S`.\n *\n * Wire shape with `serialize`:\n * { type: channel, msg, state: serialize(currentState) } // broadcast\n * { type: hydrate } → { state: serialize(currentState) } // hydrate\n *\n * Dispatch is NOT affected — it carries `M` end-to-end. Surfaces parse the\n * `V` they receive (they should pick a type parameter matching the\n * projection).\n */\n serialize?: (state: S) => V;\n /**\n * If > 0, the bridge composes a `historyTracker(runtime, size)` over the\n * runtime and returns the captured `(msg, state)` backlog in every\n * `:hydrate` reply. Late subscribers receive recent traffic on connect —\n * the inspector then sees prior tool calls / phase transitions instead\n * of just the current state snapshot.\n *\n * Defaults to 0 (no tracker, empty backlog). Composition stays opt-in:\n * the substrate is untouched, the dispatch loop is untouched, and the\n * bridge attaches the tracker only when this opt is set. Cleanup is\n * the bridge's responsibility — the disposer returned by `bridgeRuntime`\n * stops the tracker along with the observer.\n *\n * Memory cost: ~size × (msg + state) bytes, in-memory only, dies with\n * the bridge. Privacy: backlog entries flow to every `:hydrate` reply —\n * any surface that can hydrate sees them. Do not enable on bridges\n * carrying user secrets.\n */\n historySize?: number;\n}\n\n/**\n * Install the bridge for a runtime. Returns a cleanup function that removes\n * the observer and the chrome listener.\n *\n * - **Broadcast:** every (msg, state) transition emits\n * `{ type: channel, msg, state }` via `chrome.runtime.sendMessage`. The\n * send is fire-and-forget — when no surface is listening, chrome.runtime\n * rejects with \"Could not establish connection. Receiving end does not\n * exist.\" which we swallow. If `serialize` is provided, `state` carries\n * `serialize(state)` instead of the raw state.\n * - **Hydrate:** `{ type: \":hydrate\" }` is answered via `sendResponse` with\n * the current (optionally projected) `runtime.getState()`. If the runtime\n * is mid-boot (store load not yet resolved), `getState()` throws — we\n * swallow and reply with `state: null`; surfaces should treat null as\n * \"not ready yet\" and wait for the first broadcast.\n * - **Dispatch:** `{ type: \":dispatch\", msg }` is parsed through `parseMsg`\n * (Inv 8 — inbound msgs are an `unknown` boundary) and, if non-null, routes\n * to `runtime.dispatch`. Returns `true` from the listener so chrome keeps the\n * message channel open until dispatch's promise settles (we ack with\n * `{ ok: true }`). A msg that fails the parse is dropped — the reducer never\n * sees it and we reply `{ ok: false }`.\n */\nexport function bridgeRuntime<S, M extends { type: string }, V = S>(\n runtime: Runtime<S, M>,\n { channel, parseMsg, serialize, historySize }: BridgeRuntimeOpts<S, M, V>,\n): () => void {\n const { dispatchType, hydrateType } = channelTypes(channel);\n // Default to identity passthrough when no projection is provided. The\n // type parameter defaults `V = S` so callers with no projection see no\n // change in the wire-shape — the broadcast still carries `S`.\n const project: (state: S) => V =\n serialize ?? ((state: S): V => state as unknown as V);\n // Compose a history tracker only when the caller opted in. Substrate\n // (`@demlik/tea`'s `run()`) knows nothing about history — the tracker is a\n // pure composition over `runtime.observe(...)`, owned by the bridge.\n // `null` when unset means \"no backlog in :hydrate replies\"; the wire\n // shape stays uniform (backlog is always an array, possibly empty).\n const tracker: HistoryTracker<S, M> | null =\n historySize !== undefined && historySize > 0\n ? historyTracker(runtime, historySize)\n : null;\n\n // The broadcast wire still carries `msg: M | null` — surfaces depend on the\n // boot frame (`msg: null`) to hydrate from the bridge. Applied transitions now\n // arrive via `observe` with a total `msg`; the boot frame arrives via `onBoot`\n // (#47). One helper builds the envelope for both.\n const sendBroadcast = (msg: M | null, state: S): void => {\n const envelope: BroadcastEnvelope<V, M | null> = {\n type: channel,\n msg,\n state: project(state),\n };\n chrome.runtime.sendMessage(envelope).catch(() => {\n // No surface listening — drop silently. Costs are negligible at\n // typical audit-event rates; surfaces poll on hydrate on open.\n });\n };\n const unobserve = runtime.observe((msg, state) => {\n sendBroadcast(msg, state);\n });\n const unboot = runtime.onBoot((state) => {\n sendBroadcast(null, state);\n });\n\n const onMessage = (\n message: unknown,\n _sender: chrome.runtime.MessageSender,\n sendResponse: (response: unknown) => void,\n ): boolean | undefined => {\n if (!message || typeof message !== \"object\") return undefined;\n const obj = message as Record<string, unknown>;\n\n if (obj.type === hydrateType) {\n let state: V | null;\n try {\n state = project(runtime.getState());\n } catch {\n // Runtime still booting (store.load pending). Surfaces fall back to\n // waiting for the first broadcast.\n state = null;\n }\n // Read the tracker's snapshot and project each entry's state through\n // `serialize`. When `historySize` was unset (tracker === null) we\n // send `[]` — uniform wire shape means surfaces never branch on\n // \"did the bridge enable history\" — they always iterate.\n const backlog: BacklogEntry<V, M>[] = tracker\n ? tracker\n .snapshot()\n .map((entry) => ({ msg: entry.msg, state: project(entry.state) }))\n : [];\n const response: HydrateResponse<V | null, M> = { state, backlog };\n sendResponse(response);\n return false;\n }\n\n if (obj.type === dispatchType) {\n // Inv 8: the inbound msg crosses the same JSON/chrome boundary the\n // client side parses — parse here too, never cast. A null parse drops\n // the dispatch (the reducer never sees an arbitrary/malformed `M`) and\n // the surface's dispatch() rejects on the `{ ok: false }` reply.\n const msg = parseMsg(obj.msg);\n if (msg === null) {\n sendResponse({ ok: false, error: \"dispatch msg failed parse\" });\n return false;\n }\n runtime\n .dispatch(msg)\n .then(() => {\n sendResponse({ ok: true });\n })\n .catch((err: unknown) => {\n sendResponse({\n ok: false,\n error: describeError(err),\n });\n });\n return true; // keep channel open for async sendResponse\n }\n\n return undefined;\n };\n\n chrome.runtime.onMessage.addListener(onMessage);\n\n return () => {\n unobserve();\n unboot();\n chrome.runtime.onMessage.removeListener(onMessage);\n // Detach the history observer too — leaving it attached after the\n // bridge tears down would silently retain memory and keep observing\n // a runtime whose backlog can no longer reach any consumer.\n tracker?.stop();\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Surface side — the bridge half that lives in a React surface. Exported as\n// a pure helper so the React adapter (or a test) wires the listeners and the\n// hydrate round-trip with the same wire format the host emits.\n//\n// Note: this file deliberately has NO React import. The React hook lives in\n// `react.ts` and consumes these helpers. Keeps the bridge testable in plain\n// vitest without happy-dom + React renderers.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The explicit opt-out of inbound msg parsing: an identity pass-through that\n * casts the raw `unknown` to `M`. Surfaces that gate only on state and treat\n * the broadcast msg as advisory pass this as `parseMsg` to acknowledge — at\n * the call site, in the types — that they are NOT parsing the msg boundary.\n *\n * This is the same behavior the old optional-`parseMsg` default had, but the\n * unsound cast now lives at one named, greppable, opt-in seam instead of being\n * the silent default for everyone. (Inv 8: the boundary parses; this helper is\n * the documented exception, chosen explicitly.)\n */\nexport function passThroughMsg<M>(raw: unknown): M | null {\n return raw as M;\n}\n\nexport interface BridgeClient<S, M> {\n /** Send the hydrate request; resolves with the initial snapshot (or null\n * if the host is still booting OR the response failed to parse). Also\n * primes `getSnapshot()` so React's `useSyncExternalStore` sees the\n * hydrated value on the next render. */\n hydrate(): Promise<S | null>;\n /** Send a dispatch envelope; resolves once the host's dispatch settles. */\n dispatch(msg: M): Promise<void>;\n /** Subscribe to broadcasts. Returns a cleanup function. Broadcasts whose\n * payload fails `parseState` or `parseMsg` are dropped silently — the\n * listener is not called. Each successful broadcast updates\n * `getSnapshot()`. */\n subscribe(listener: (msg: M | null, state: S) => void): () => void;\n /** Synchronous accessor for the latest known state, or `null` until the\n * first successful hydrate or broadcast lands. Required reference stability\n * for `useSyncExternalStore`: returns the SAME object reference when the\n * underlying state hasn't changed since the prior call. */\n getSnapshot(): S | null;\n}\n\nexport interface BridgeClientOpts<S, M> {\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads (from `:hydrate` reply AND\n * every broadcast). Chrome runtime messages are an `unknown` boundary\n * (Inv 8) — the surface is the only party that knows the domain type `S`,\n * so it owns the boundary parse. Returning `null` drops the payload\n * silently (subscriber not called; hydrate resolves to `null`). The\n * parser MUST NOT throw — model rejection as `null`.\n */\n parseState: (raw: unknown) => S | null;\n /**\n * REQUIRED parse for inbound `msg` payloads on broadcasts. Chrome runtime\n * messages are an `unknown` boundary (Inv 8) just like the `state` payload,\n * so the msg crosses the same JSON serialization seam and must be parsed —\n * never cast. Returning `null` drops the broadcast silently.\n *\n * Surfaces that treat the msg union as advisory and only gate on state opt\n * out EXPLICITLY by passing the {@link passThroughMsg} helper — the opt-out\n * is now a visible call-site decision, not a hidden default that lets an\n * unparsed `unknown` reach listeners typed as `M`.\n */\n parseMsg: (raw: unknown) => M | null;\n}\n\n/**\n * The state+msg boundary parse (Inv 8) for one inbound envelope, in ONE\n * place. Chrome runtime messages are an `unknown` boundary: a failed\n * `parseState` drops the envelope (null state is indistinguishable from\n * \"host still booting\" and is not a useful signal), and a non-null `msg`\n * that fails `parseMsg` drops it too. `msg: null | undefined` on the wire is\n * the boot/current-state marker and passes through as `null`.\n *\n * Returns `null` to signal \"drop\"; callers translate that to `return`\n * (live `subscribe`) or `continue` (backlog replay). Living here keeps the\n * live-broadcast and backlog-replay paths from drifting apart.\n */\nfunction parseEnvelope<S, M extends { type: string }>(\n obj: Record<string, unknown>,\n parseState: (raw: unknown) => S | null,\n parseMsg: (raw: unknown) => M | null,\n): { msg: M | null; state: S } | null {\n const state = parseState(obj.state);\n if (state === null) return null;\n let msg: M | null;\n if (obj.msg === null || obj.msg === undefined) {\n msg = null;\n } else {\n msg = parseMsg(obj.msg);\n if (msg === null) return null;\n }\n return { msg, state };\n}\n\n/**\n * Shared backlog-replay helper used by both bridge clients. Iterates raw\n * entries from the wire, runs each through the same boundary-parse rules a\n * live broadcast would (via {@link parseEnvelope}), and fires the listeners\n * with the parsed `(msg, state)` pair. Updates the closure-held `latestState`\n * via `commitState` so post-replay `getSnapshot()` returns the most recent\n * backlog state if the current-state marker is absent or unparseable.\n *\n * Extracted so the two clients can't drift in how they parse backlog\n * payloads — both must stay symmetric with their live `subscribe` parse.\n */\nfunction replayBacklog<S, M extends { type: string }>(\n raw: unknown,\n parseState: (raw: unknown) => S | null,\n parseMsg: (raw: unknown) => M | null,\n listeners: Set<(msg: M | null, state: S) => void>,\n commitState: (next: S) => void,\n): void {\n if (!Array.isArray(raw)) return;\n for (const entryRaw of raw) {\n if (!entryRaw || typeof entryRaw !== \"object\") continue;\n const entry = entryRaw as Record<string, unknown>;\n const parsed = parseEnvelope(entry, parseState, parseMsg);\n if (parsed === null) continue;\n commitState(parsed.state);\n for (const l of listeners) l(parsed.msg, parsed.state);\n }\n}\n\n/**\n * Options for the shared client factory. On top of the public\n * `BridgeClient` contract (channel + the two boundary parsers), the factory\n * takes the two axes on which the runtime and tab clients actually differ:\n *\n * - `send` abstracts the request transport — `chrome.runtime.sendMessage`\n * for the runtime client, `chrome.tabs.sendMessage(tabId, …)` for the tab\n * client — used by both `hydrate()` and `dispatch()`.\n * - `senderMatches` filters inbound broadcasts by their `sender`. Defaults\n * to accept-all (runtime client); the tab client passes\n * `sender => sender.tab?.id === tabId` so a per-tab client only sees its\n * own tab's broadcasts.\n */\ninterface MakeBridgeClientOpts<S, M> {\n channel: string;\n parseState: (raw: unknown) => S | null;\n parseMsg: (raw: unknown) => M | null;\n send: (message: unknown) => Promise<unknown>;\n senderMatches?: (sender: chrome.runtime.MessageSender) => boolean;\n}\n\n/**\n * The single client implementation behind {@link bridgeClient} and\n * {@link bridgeTabClient}. Both public constructors are thin wrappers that\n * pick the transport (`send`) and, for the tab client, the sender filter.\n * Everything else — the `latestState`/`listeners` closures, the hydrate\n * round-trip + backlog replay, dispatch, the live-broadcast parse, and\n * `getSnapshot` reference stability — lives here so the two clients cannot\n * drift.\n */\nfunction makeBridgeClient<S, M extends { type: string }>({\n channel,\n parseState,\n parseMsg,\n send,\n senderMatches = () => true,\n}: MakeBridgeClientOpts<S, M>): BridgeClient<S, M> {\n const { dispatchType, hydrateType } = channelTypes(channel);\n // Closure-held latest snapshot. `useSyncExternalStore` reads this via\n // `getSnapshot`; the reference is stable across calls until a hydrate\n // reply or broadcast overwrites it. Identity matters: returning a new\n // object for the same logical state would loop React. `null` is the\n // pre-hydrate sentinel.\n let latestState: S | null = null;\n // Each active subscribe() registers a listener. Hydrate replies notify\n // every subscriber with `msg: null` (no host transition occurred — the\n // change source is the hydrate round-trip) so `useSyncExternalStore`\n // sees the snapshot update on first mount.\n const listeners = new Set<(msg: M | null, state: S) => void>();\n\n const client: BridgeClient<S, M> = {\n async hydrate(): Promise<S | null> {\n const req: HydrateRequest = { type: hydrateType };\n const res = (await send(req)) as\n | { state: unknown; backlog?: unknown }\n | undefined;\n // Replay the backlog FIRST — listeners observing for a msg log see\n // historical (msg, state) pairs in oldest-first order before the\n // current-state marker. Backlog entries that fail boundary parse are\n // dropped silently (same posture as live broadcasts). When the host\n // didn't enable history, `backlog` is `[]` and this loop is a no-op.\n replayBacklog(res?.backlog, parseState, parseMsg, listeners, (next) => {\n latestState = next;\n });\n const raw = res?.state;\n if (raw === undefined || raw === null) return null;\n const parsed = parseState(raw);\n if (parsed === null) return null;\n latestState = parsed;\n // Fire the current-state marker (msg = null) — distinct from any\n // backlog entry. Surfaces that show \"X just connected\" key off this.\n for (const l of listeners) l(null, parsed);\n return parsed;\n },\n\n async dispatch(msg: M): Promise<void> {\n const env: DispatchEnvelope<M> = { type: dispatchType, msg };\n await send(env);\n },\n\n subscribe(listener: (msg: M | null, state: S) => void): () => void {\n listeners.add(listener);\n const onMessage = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n ): void => {\n if (!senderMatches(sender)) return;\n if (!message || typeof message !== \"object\") return;\n const obj = message as Record<string, unknown>;\n if (obj.type !== channel) return;\n const parsed = parseEnvelope(obj, parseState, parseMsg);\n if (parsed === null) return;\n latestState = parsed.state;\n listener(parsed.msg, parsed.state);\n };\n chrome.runtime.onMessage.addListener(onMessage);\n return () => {\n chrome.runtime.onMessage.removeListener(onMessage);\n listeners.delete(listener);\n };\n },\n\n getSnapshot(): S | null {\n return latestState;\n },\n };\n\n return client;\n}\n\nexport function bridgeClient<S, M extends { type: string }>({\n channel,\n parseState,\n parseMsg,\n}: BridgeClientOpts<S, M>): BridgeClient<S, M> {\n return makeBridgeClient({\n channel,\n parseState,\n parseMsg,\n send: (message) => chrome.runtime.sendMessage(message),\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SW-side client of a content-script-hosted runtime.\n//\n// `bridgeClient` above sends via `chrome.runtime.sendMessage`, which lands on\n// the SW's `chrome.runtime.onMessage`. That's the surface-to-SW direction.\n//\n// For the SW-to-content direction we need:\n// - SENDS via `chrome.tabs.sendMessage(tabId, …)` — `chrome.runtime.sendMessage`\n// does NOT reach content scripts running in a tab.\n// - BROADCAST subscribers must filter on `sender.tab?.id === tabId`, since\n// every tab's content script broadcasts through the same SW\n// `chrome.runtime.onMessage`. Without the filter, a SW client subscribed\n// to \"tab 17\" would also see broadcasts from tabs 22, 31, ….\n//\n// Shape mirrors `BridgeClient<S, M>` so consumers can pick the constructor by\n// context and use the rest of the surface identically.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BridgeTabClientOpts<S, M> {\n tabId: number;\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads. See `BridgeClientOpts.parseState`\n * — same contract, same rationale. Chrome runtime messages from a tab are\n * an `unknown` boundary; the SW client owns the parse.\n */\n parseState: (raw: unknown) => S | null;\n /**\n * REQUIRED parse for inbound `msg` payloads. See `BridgeClientOpts.parseMsg`\n * — same contract, same rationale. Opt out of msg parsing with\n * {@link passThroughMsg}.\n */\n parseMsg: (raw: unknown) => M | null;\n}\n\n/**\n * SW-side client of a `bridgeRuntime` hosted in a content script.\n *\n * Identical contract to `bridgeClient` (same `BridgeClient<S, M>` shape).\n * Differences are confined to transport:\n *\n * - `hydrate()` / `dispatch(msg)` send via `chrome.tabs.sendMessage(tabId, …)`.\n * If the tab is gone, `chrome.tabs.sendMessage` rejects (same shape as\n * `chrome.runtime.sendMessage` with no receiver) — the promise propagates\n * the rejection. Callers decide whether to retry, drop, or surface; the\n * bridge has no opinion. Fire-and-forget posture matches `bridgeRuntime`.\n * - `subscribe(listener)` registers a `chrome.runtime.onMessage` listener\n * filtered by both `envelope.type === channel` AND `sender.tab?.id === tabId`,\n * so broadcasts from other tabs are ignored (no cross-contamination across\n * per-tab content-script runtimes).\n */\nexport function bridgeTabClient<S, M extends { type: string }>({\n tabId,\n channel,\n parseState,\n parseMsg,\n}: BridgeTabClientOpts<S, M>): BridgeClient<S, M> {\n return makeBridgeClient({\n channel,\n parseState,\n parseMsg,\n // SW → content transport: `chrome.runtime.sendMessage` does NOT reach a\n // tab's content script; `chrome.tabs.sendMessage(tabId, …)` does.\n send: (message) => chrome.tabs.sendMessage(tabId, message),\n // Every tab's content script broadcasts through the same SW\n // `chrome.runtime.onMessage`; filter to this tab so a client subscribed\n // to one tab never sees another's broadcasts.\n senderMatches: (sender) => sender.tab?.id === tabId,\n });\n}\n"]}