experimental-a2 0.16.0 → 0.17.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 (334) hide show
  1. package/CHANGELOG.md +360 -0
  2. package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
  3. package/dist/actor-DBbe5xeE.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +2 -5
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +13 -4
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -3
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +20 -10
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  13. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  14. package/dist/actor.d.ts +1 -1
  15. package/dist/actor.js +42 -23
  16. package/dist/actor.js.map +1 -1
  17. package/dist/{ai-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
  18. package/dist/ai-UrWfKeuv.js.map +1 -0
  19. package/dist/ai-client.d.ts +27 -0
  20. package/dist/ai-client.d.ts.map +1 -0
  21. package/dist/ai-client.js +29 -0
  22. package/dist/ai-client.js.map +1 -0
  23. package/dist/ai-react.d.ts +96 -0
  24. package/dist/ai-react.d.ts.map +1 -0
  25. package/dist/ai-react.js +142 -0
  26. package/dist/ai-react.js.map +1 -0
  27. package/dist/ai-server.browser.js +2 -2
  28. package/dist/ai-server.browser.js.map +1 -1
  29. package/dist/ai-server.d.ts +80 -36
  30. package/dist/ai-server.d.ts.map +1 -1
  31. package/dist/ai-server.js +466 -265
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
  34. package/dist/ai-vpGtcaCB.d.ts.map +1 -0
  35. package/dist/ai.d.ts +2 -2
  36. package/dist/ai.js +2 -2
  37. package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
  38. package/dist/client-COaiaDub.js.map +1 -0
  39. package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
  40. package/dist/client-qRnRUzBS.d.ts.map +1 -0
  41. package/dist/client.d.ts +4 -2
  42. package/dist/client.js +1 -1
  43. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  44. package/dist/contract-BYKASlCV.js.map +1 -0
  45. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  46. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  47. package/dist/index.d.ts +6 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
  51. package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
  52. package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
  53. package/dist/internal-CbVvEwad.js.map +1 -0
  54. package/dist/otel.d.ts +1 -1
  55. package/dist/react-BbJ-g6Uu.d.ts +86 -0
  56. package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
  57. package/dist/react.d.ts +2 -122
  58. package/dist/react.js +107 -100
  59. package/dist/react.js.map +1 -1
  60. package/dist/reducer-BAXc8JyF.js +87 -0
  61. package/dist/reducer-BAXc8JyF.js.map +1 -0
  62. package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
  63. package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
  64. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  65. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +3 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +33 -10
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  71. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -9
  73. package/dist/scheduler-vercel.d.ts.map +1 -1
  74. package/dist/scheduler-vercel.js +25 -44
  75. package/dist/scheduler-vercel.js.map +1 -1
  76. package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
  77. package/dist/server-CGM8WzRG.d.ts.map +1 -0
  78. package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
  79. package/dist/server-Rj64fNvX.js.map +1 -0
  80. package/dist/server.d.ts +4 -4
  81. package/dist/server.js +2 -2
  82. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  83. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  84. package/dist/session-clear-Dshpuhff.d.ts +12 -0
  85. package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
  86. package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
  87. package/dist/store-Gpkq7WBo.d.ts.map +1 -0
  88. package/dist/store-N8PXxDAS.js.map +1 -1
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +182 -69
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
  94. package/dist/store-polling-4_y8lETm.js.map +1 -0
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +399 -108
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
  100. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.d.ts.map +1 -1
  103. package/dist/store-redis-http.js +293 -11
  104. package/dist/store-redis-http.js.map +1 -1
  105. package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
  106. package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
  107. package/dist/store-redis.d.ts +1 -1
  108. package/dist/store-redis.d.ts.map +1 -1
  109. package/dist/store-redis.js +11 -11
  110. package/dist/store-redis.js.map +1 -1
  111. package/dist/store-sqlite.d.ts +1 -1
  112. package/dist/store-sqlite.d.ts.map +1 -1
  113. package/dist/store-sqlite.js +166 -32
  114. package/dist/store-sqlite.js.map +1 -1
  115. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  116. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  117. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
  118. package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
  119. package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
  120. package/dist/wire-DjfHOvdW.js.map +1 -0
  121. package/docs/01-quickstart.mdx +2 -2
  122. package/docs/actors/01-introduction.mdx +4 -0
  123. package/docs/actors/04-routes.mdx +14 -1
  124. package/docs/concepts/01-contracts.mdx +95 -0
  125. package/docs/concepts/04-state.mdx +3 -3
  126. package/docs/guides/01-timers.mdx +22 -14
  127. package/docs/guides/02-cancellation.mdx +1 -1
  128. package/docs/guides/03-react.mdx +268 -328
  129. package/docs/guides/04-local-first.mdx +32 -73
  130. package/docs/guides/05-production.mdx +40 -27
  131. package/docs/guides/06-ai-agents.mdx +680 -580
  132. package/docs/guides/09-presence.mdx +3 -7
  133. package/docs/guides/10-transports.mdx +5 -5
  134. package/docs/index.mdx +10 -18
  135. package/docs/reference/01-api.mdx +662 -195
  136. package/docs/reference/02-errors.mdx +2 -1
  137. package/examples/playground/DEPLOY.md +52 -6
  138. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  139. package/examples/playground/app/a0/[appId]/page.tsx +48 -0
  140. package/examples/playground/app/a0/a0.css +818 -0
  141. package/examples/playground/app/a0/app-list.tsx +211 -0
  142. package/examples/playground/app/a0/app-view.tsx +55 -0
  143. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
  145. package/examples/playground/app/a0/apps.test.ts +116 -0
  146. package/examples/playground/app/a0/apps.ts +87 -0
  147. package/examples/playground/app/a0/chat.tsx +364 -0
  148. package/examples/playground/app/a0/columns.tsx +109 -0
  149. package/examples/playground/app/a0/context-menu.tsx +116 -0
  150. package/examples/playground/app/a0/cookies.ts +46 -0
  151. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  152. package/examples/playground/app/a0/crashes/route.ts +3 -0
  153. package/examples/playground/app/a0/crashes.test.ts +33 -0
  154. package/examples/playground/app/a0/crashes.ts +28 -0
  155. package/examples/playground/app/a0/devtools.tsx +164 -0
  156. package/examples/playground/app/a0/events/route.ts +6 -0
  157. package/examples/playground/app/a0/flags.ts +30 -0
  158. package/examples/playground/app/a0/layout.tsx +38 -0
  159. package/examples/playground/app/a0/list/route.ts +10 -0
  160. package/examples/playground/app/a0/model.test.ts +56 -0
  161. package/examples/playground/app/a0/model.ts +58 -0
  162. package/examples/playground/app/a0/page.tsx +14 -0
  163. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  164. package/examples/playground/app/a0/preview.test.ts +53 -0
  165. package/examples/playground/app/a0/preview.ts +26 -0
  166. package/examples/playground/app/a0/sandbox-log.ts +69 -0
  167. package/examples/playground/app/a0/sandbox.ts +191 -0
  168. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  169. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  170. package/examples/playground/app/a0/server.ts +158 -0
  171. package/examples/playground/app/a0/session.ts +47 -0
  172. package/examples/playground/app/a0/shell.tsx +150 -0
  173. package/examples/playground/app/a0/short/events/route.ts +9 -0
  174. package/examples/playground/app/a0/splitter.tsx +120 -0
  175. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  176. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  177. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  178. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  179. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  180. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  181. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  182. package/examples/playground/app/agent/events/route.ts +5 -2
  183. package/examples/playground/app/agent/ingress.test.ts +98 -0
  184. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  185. package/examples/playground/app/agent/model-input.tsx +115 -0
  186. package/examples/playground/app/agent/model.test.ts +73 -0
  187. package/examples/playground/app/agent/model.ts +58 -26
  188. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  189. package/examples/playground/app/agent/page.tsx +4 -4
  190. package/examples/playground/app/agent/server.ts +15 -14
  191. package/examples/playground/app/agent/session.ts +5 -6
  192. package/examples/playground/app/agent/telemetry.test.ts +168 -0
  193. package/examples/playground/app/agent/telemetry.ts +30 -0
  194. package/examples/playground/app/agent/trace.test.ts +23 -0
  195. package/examples/playground/app/agent/trace.ts +18 -0
  196. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
  197. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  198. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  199. package/examples/playground/app/canvas/model.ts +53 -53
  200. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  201. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  202. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  203. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  204. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  205. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  206. package/examples/playground/app/chat/agent/route.ts +9 -0
  207. package/examples/playground/app/chat/model.test.ts +46 -119
  208. package/examples/playground/app/chat/model.ts +82 -128
  209. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  210. package/examples/playground/app/chat/server.test.ts +108 -0
  211. package/examples/playground/app/chat/server.ts +69 -73
  212. package/examples/playground/app/components/activity-feed.tsx +30 -1
  213. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  214. package/examples/playground/app/components/demo-icons.tsx +89 -0
  215. package/examples/playground/app/components/demos.tsx +33 -0
  216. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  217. package/examples/playground/app/components/fast-link.tsx +55 -0
  218. package/examples/playground/app/components/site-header.tsx +96 -0
  219. package/examples/playground/app/counter/counter-client.tsx +3 -3
  220. package/examples/playground/app/counter/model.test.ts +3 -3
  221. package/examples/playground/app/counter/model.ts +11 -21
  222. package/examples/playground/app/counter/page.tsx +2 -15
  223. package/examples/playground/app/counter/session.ts +2 -2
  224. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  225. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  226. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  227. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  228. package/examples/playground/app/documents/model.ts +27 -27
  229. package/examples/playground/app/globals.css +420 -207
  230. package/examples/playground/app/initial-state.test.tsx +54 -0
  231. package/examples/playground/app/layout.tsx +7 -23
  232. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  233. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  234. package/examples/playground/app/orders/model.ts +49 -49
  235. package/examples/playground/app/page.tsx +13 -102
  236. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  237. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  238. package/examples/playground/app/recovery/model.ts +30 -30
  239. package/examples/playground/instrumentation.ts +5 -0
  240. package/examples/playground/package.json +10 -2
  241. package/examples/playground/scripts/a0-template.ts +77 -0
  242. package/examples/playground/vercel.json +18 -0
  243. package/package.json +3 -1
  244. package/src/actor-client.ts +16 -4
  245. package/src/actor-react.ts +25 -11
  246. package/src/actor-shared.ts +21 -6
  247. package/src/actor.ts +76 -31
  248. package/src/ai-agent-actions.ts +219 -0
  249. package/src/ai-agent.ts +2343 -0
  250. package/src/ai-app-state.ts +91 -0
  251. package/src/ai-client-state.ts +39 -2
  252. package/src/ai-client.ts +62 -0
  253. package/src/ai-context-schema.ts +25 -5
  254. package/src/ai-context.ts +90 -40
  255. package/src/ai-control-server.ts +94 -55
  256. package/src/ai-control.ts +2 -0
  257. package/src/ai-coordinator.ts +23 -22
  258. package/src/ai-message-base.ts +209 -0
  259. package/src/ai-message-projection.ts +382 -58
  260. package/src/ai-progress-batches.ts +26 -0
  261. package/src/ai-projector.ts +95 -14
  262. package/src/ai-react.ts +146 -0
  263. package/src/ai-server.browser.ts +2 -2
  264. package/src/ai-server.ts +20 -1951
  265. package/src/ai-stored-state.ts +48 -163
  266. package/src/ai.ts +343 -276
  267. package/src/bootstrap-stream.ts +72 -0
  268. package/src/bounded-cache.ts +55 -0
  269. package/src/client-render-session.ts +49 -0
  270. package/src/client.ts +688 -151
  271. package/src/contract.ts +92 -42
  272. package/src/deterministic-id.ts +37 -0
  273. package/src/equal-json.ts +32 -0
  274. package/src/errors.ts +13 -1
  275. package/src/index.ts +5 -1
  276. package/src/initial-snapshot.ts +39 -0
  277. package/src/internal.ts +45 -0
  278. package/src/postgres-notification-scope.ts +14 -3
  279. package/src/react.ts +171 -206
  280. package/src/redis-http-reply.ts +256 -0
  281. package/src/redis-http-stream.ts +73 -0
  282. package/src/redis-http-subscriptions.ts +13 -5
  283. package/src/reducer-runtime.ts +52 -0
  284. package/src/reducer.ts +92 -10
  285. package/src/scheduler-qstash.ts +49 -20
  286. package/src/scheduler-task.ts +34 -2
  287. package/src/scheduler-vercel.ts +32 -84
  288. package/src/server-fetch.ts +139 -35
  289. package/src/server.ts +957 -237
  290. package/src/session-clear.ts +23 -0
  291. package/src/session-socket.ts +154 -49
  292. package/src/sse.ts +71 -4
  293. package/src/store-memory.ts +220 -78
  294. package/src/store-polling.ts +18 -4
  295. package/src/store-postgres.ts +461 -114
  296. package/src/store-redis-core.ts +1260 -434
  297. package/src/store-redis-http.ts +18 -2
  298. package/src/store-redis-notify.ts +260 -85
  299. package/src/store-redis.ts +19 -9
  300. package/src/store-sqlite.ts +259 -45
  301. package/src/store.ts +46 -1
  302. package/src/stream-buffer.ts +43 -0
  303. package/src/telemetry.ts +13 -1
  304. package/src/wire.ts +241 -13
  305. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  306. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  307. package/dist/ai-CrLYwNEx.js.map +0 -1
  308. package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
  309. package/dist/client-BYzHjkwU.js.map +0 -1
  310. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  311. package/dist/contract-CKRg_E4q.js.map +0 -1
  312. package/dist/errors-DCk6ch5n.js.map +0 -1
  313. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  314. package/dist/internal-Dq2qYxou.js.map +0 -1
  315. package/dist/react.d.ts.map +0 -1
  316. package/dist/reducer-DEMjEY_O.js +0 -29
  317. package/dist/reducer-DEMjEY_O.js.map +0 -1
  318. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  319. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  320. package/dist/server-BD5ckxZb.js.map +0 -1
  321. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  322. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  323. package/dist/store-polling-CmxUbV93.js.map +0 -1
  324. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  325. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  326. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  327. package/dist/validate-XKT4FSNn.js +0 -30
  328. package/dist/validate-XKT4FSNn.js.map +0 -1
  329. package/dist/wire-BO5wWCb1.js.map +0 -1
  330. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  331. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  332. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  333. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  334. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -1,10 +1,50 @@
1
- import { n as validateSync } from "./validate-XKT4FSNn.js";
2
- import { n as NonRetriableError, r as asStoreUnavailable, t as A2Error } from "./errors-DCk6ch5n.js";
3
- import { c as STREAM_TIMINGS, g as serverInternals, h as nullProtoRecord, i as POLL_TIMINGS, m as markSchedulerSendFailure, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS } from "./internal-Dq2qYxou.js";
4
- import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-CmxUbV93.js";
1
+ import { a as NonRetriableError, i as A2Error, n as isSessionCleared, o as asStoreUnavailable, r as sessionClearedError } from "./session-clear-DGI4jTQ5.js";
2
+ import { n as foldEvent, s as validateSync, t as cloneInitial } from "./reducer-BAXc8JyF.js";
3
+ import { n as encodeState, t as decodeState } from "./reducer-runtime-DQwz_J59.js";
4
+ import { _ as serverInternals, c as STATE_CACHE, g as nullProtoRecord, h as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as stateTailReaders } from "./internal-CbVvEwad.js";
5
+ import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-4_y8lETm.js";
6
+ import { i as takeBuffered, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-Cx7AAPGq.js";
5
7
  import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
6
- import { _ as socketErrorAckFor, a as errorToWire, b as socketUnsubscribedFor, g as socketAckFor, h as presenceSnapshotToWire, i as errorStatus, n as asA2Error, p as presencePatchToWire, s as eventToWire, t as SOCKET_PING_FRAME, v as socketFrameFor, y as socketSubscribedFor } from "./wire-BO5wWCb1.js";
8
+ import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-DjfHOvdW.js";
7
9
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
10
+ //#region src/bounded-cache.ts
11
+ function boundedCache({ maxEntries, maxBytes }) {
12
+ const entries = /* @__PURE__ */ new Map();
13
+ let total = 0;
14
+ const remove = (key) => {
15
+ const entry = entries.get(key);
16
+ if (!entry) return;
17
+ entries.delete(key);
18
+ total -= entry.bytes;
19
+ };
20
+ return {
21
+ get(key) {
22
+ const entry = entries.get(key);
23
+ if (!entry) return void 0;
24
+ entries.delete(key);
25
+ entries.set(key, entry);
26
+ return entry.value;
27
+ },
28
+ set(key, value, bytes) {
29
+ remove(key);
30
+ if (bytes > maxBytes / 4) return;
31
+ entries.set(key, {
32
+ value,
33
+ bytes
34
+ });
35
+ total += bytes;
36
+ for (const oldest of entries.keys()) {
37
+ if (entries.size <= maxEntries && total <= maxBytes) break;
38
+ remove(oldest);
39
+ }
40
+ },
41
+ delete: remove,
42
+ deleteWhere(match) {
43
+ for (const key of entries.keys()) if (match(key)) remove(key);
44
+ }
45
+ };
46
+ }
47
+ //#endregion
8
48
  //#region src/push-envelope.ts
9
49
  /**
10
50
  * The push envelope's trust boundary for `{ sessionId, events, presence? }`,
@@ -191,25 +231,43 @@ function socketShell(socket, release, onFrame, deadlineAt) {
191
231
  track
192
232
  };
193
233
  }
194
- const socketSubscription = (target, startAfter, presence) => ({
195
- target,
196
- iterator: (presence ? target.stream({
197
- startAfter,
198
- presence: true
199
- }) : target.stream({ startAfter }))[Symbol.asyncIterator](),
200
- stopped: false
201
- });
234
+ const socketSubscription = (options) => {
235
+ const { target, startAfter, presence, attempt } = options;
236
+ return {
237
+ target,
238
+ iterator: (presence ? target.stream({
239
+ startAfter,
240
+ presence: true
241
+ }) : target.stream({ startAfter }))[Symbol.asyncIterator](),
242
+ stopped: false,
243
+ ...attempt === void 0 ? {} : { attempt }
244
+ };
245
+ };
202
246
  const stopSubscription = (subscription) => {
203
247
  subscription.stopped = true;
204
248
  subscription.iterator.return?.()?.catch(() => {});
205
249
  };
206
- const pumpSubscription = async (shell, subscription, frameFor) => {
250
+ /**
251
+ * Sends a subscription's items until it ends. With `batch`, an event and
252
+ * the events its iterator already holds go out as one frame.
253
+ */
254
+ const pumpSubscription = async (options) => {
255
+ const { shell, subscription, frameFor, batch } = options;
207
256
  for (;;) {
208
257
  while (!shell.torn() && !subscription.stopped && shell.buffered() > SOCKET_TIMINGS.highWaterMarkBytes) await new Promise((wake) => setTimeout(wake, SOCKET_TIMINGS.resumePollMs));
209
258
  if (shell.torn() || subscription.stopped) return;
210
259
  const { value, done } = await subscription.iterator.next();
211
260
  if (shell.torn() || subscription.stopped || done) return;
212
- shell.send(frameFor(value));
261
+ if (!batch || !isStreamEvent(value)) {
262
+ shell.send(frameFor(value));
263
+ continue;
264
+ }
265
+ const { events } = heldEventBatch({
266
+ first: value,
267
+ iterator: subscription.iterator,
268
+ encode: compactEventJson
269
+ });
270
+ shell.send(batch(events));
213
271
  }
214
272
  };
215
273
  const handleSocketPush = async (shell, target, sessionId, frame, req, ackSessionId, gatePush, activate) => {
@@ -255,7 +313,11 @@ const applySocketPresence = async (shell, target, sessionId, frame, gatePush) =>
255
313
  };
256
314
  function sessionSocket(sessionId, session, socket, options) {
257
315
  const target = session;
258
- const subscription = socketSubscription(target, options?.startAfter ?? 0, options?.presence === true);
316
+ const subscription = socketSubscription({
317
+ target,
318
+ startAfter: options?.startAfter ?? 0,
319
+ presence: options?.presence === true
320
+ });
259
321
  const handleFrame = (frame) => {
260
322
  switch (frame["kind"]) {
261
323
  case "push": {
@@ -286,20 +348,41 @@ function sessionSocket(sessionId, session, socket, options) {
286
348
  }
287
349
  };
288
350
  const shell = socketShell(socket, () => stopSubscription(subscription), handleFrame, options?.deadline);
289
- pumpSubscription(shell, subscription, socketFrameFor).then(() => shell.shutdown(1e3), () => shell.shutdown(1011, "stream failed"));
351
+ pumpSubscription({
352
+ shell,
353
+ subscription,
354
+ frameFor: socketFrameFor,
355
+ ...options?.wire === 2 ? { batch: (events) => socketEventsFor(events) } : {}
356
+ }).then(() => shell.shutdown(1e3), () => shell.shutdown(1011, "stream failed"));
290
357
  }
291
358
  const parseSubscribeEntries = (value) => {
292
359
  if (!Array.isArray(value) || value.length === 0) return null;
293
360
  const entries = [];
294
361
  for (const entry of value) {
295
362
  if (entry === null || typeof entry !== "object") return null;
296
- const { id, index } = entry;
363
+ const { id, attempt, index, bootstrap, reducer } = entry;
297
364
  if (typeof id !== "string" || id.length === 0) return null;
298
- if (typeof index !== "number" || !Number.isInteger(index) || index < 0) return null;
299
- entries.push({
365
+ if (attempt !== void 0 && (typeof attempt !== "number" || !Number.isSafeInteger(attempt) || attempt < 0)) return null;
366
+ if (reducer !== void 0 && (typeof reducer !== "string" || reducer.length === 0)) return null;
367
+ const lane = {
300
368
  id,
301
- index
302
- });
369
+ ...attempt === void 0 ? {} : { attempt }
370
+ };
371
+ if (bootstrap === true) {
372
+ if (index !== void 0 || typeof reducer !== "string") return null;
373
+ entries.push({
374
+ ...lane,
375
+ bootstrap: true,
376
+ reducer
377
+ });
378
+ } else {
379
+ if (bootstrap !== void 0 || typeof index !== "number" || !Number.isSafeInteger(index) || index < 0) return null;
380
+ entries.push({
381
+ ...lane,
382
+ index,
383
+ ...reducer === void 0 ? {} : { reducer }
384
+ });
385
+ }
303
386
  }
304
387
  return entries;
305
388
  };
@@ -322,18 +405,34 @@ function sessionsSocket(resolve, socket, options) {
322
405
  if (subscriptions.get(id) === subscription) subscriptions.delete(id);
323
406
  };
324
407
  const runSubscription = async (id, subscription) => {
408
+ const { attempt } = subscription;
325
409
  try {
326
- await pumpSubscription(shell, subscription, (item) => socketFrameFor(item, id));
410
+ await pumpSubscription({
411
+ shell,
412
+ subscription,
413
+ frameFor: (item) => socketFrameFor(item, id),
414
+ ...options?.wire === 2 ? { batch: (events) => socketEventsFor(events, id) } : {}
415
+ });
327
416
  if (shell.torn() || subscription.stopped) return;
328
417
  retire(id, subscription);
329
- shell.send(socketUnsubscribedFor(id));
418
+ shell.send(socketUnsubscribedFor({
419
+ sessionId: id,
420
+ attempt
421
+ }));
330
422
  } catch (error) {
331
423
  if (shell.torn() || subscription.stopped) return;
332
424
  retire(id, subscription);
333
- shell.send(socketUnsubscribedFor(id, "stream failed", asA2Error(error)));
425
+ shell.send(socketUnsubscribedFor({
426
+ sessionId: id,
427
+ attempt,
428
+ reason: "stream failed",
429
+ error: asA2Error(error)
430
+ }));
334
431
  }
335
432
  };
336
- const startSubscription = async (id, index) => {
433
+ const startSubscription = async (entry) => {
434
+ const { id, attempt } = entry;
435
+ const index = entry.index ?? 0;
337
436
  const existing = subscriptions.get(id);
338
437
  if (existing) {
339
438
  stopSubscription(existing);
@@ -341,19 +440,37 @@ function sessionsSocket(resolve, socket, options) {
341
440
  }
342
441
  let session;
343
442
  try {
344
- session = await resolve(id, index);
443
+ session = await resolve(id, index, entry);
345
444
  } catch (error) {
346
- if (!shell.torn()) shell.send(socketUnsubscribedFor(id, "stream failed", asA2Error(error)));
445
+ if (!shell.torn()) shell.send(socketUnsubscribedFor({
446
+ sessionId: id,
447
+ attempt,
448
+ reason: "stream failed",
449
+ error: asA2Error(error)
450
+ }));
347
451
  return;
348
452
  }
349
453
  if (shell.torn()) return;
350
454
  if (session === null) {
351
- shell.send(socketUnsubscribedFor(id, "subscribe rejected", new A2Error("FORBIDDEN", "the subscription is not authorized")));
455
+ shell.send(socketUnsubscribedFor({
456
+ sessionId: id,
457
+ attempt,
458
+ reason: "subscribe rejected",
459
+ error: new A2Error("FORBIDDEN", "the subscription is not authorized")
460
+ }));
352
461
  return;
353
462
  }
354
- const subscription = socketSubscription(session, index, presence);
463
+ const subscription = socketSubscription({
464
+ target: session,
465
+ startAfter: index,
466
+ presence,
467
+ attempt
468
+ });
355
469
  subscriptions.set(id, subscription);
356
- shell.send(socketSubscribedFor(id));
470
+ shell.send(socketSubscribedFor({
471
+ sessionId: id,
472
+ attempt
473
+ }));
357
474
  runSubscription(id, subscription);
358
475
  };
359
476
  let controlTail = Promise.resolve();
@@ -384,7 +501,7 @@ function sessionsSocket(resolve, socket, options) {
384
501
  shell.malformedFrame("malformed subscribe frame");
385
502
  return;
386
503
  }
387
- for (const entry of entries) control(() => startSubscription(entry.id, entry.index));
504
+ for (const entry of entries) control(() => startSubscription(entry));
388
505
  return;
389
506
  }
390
507
  case "unsubscribe": {
@@ -449,7 +566,10 @@ function sessionsSocket(resolve, socket, options) {
449
566
  */
450
567
  /**
451
568
  * Pipe a live event iterable into an SSE `Response`. Each event is one
452
- * frame — `id:` carries the event-log index, `data:` the JSON event. A
569
+ * frame — `id:` carries the event-log index, `data:` the JSON event.
570
+ * Frames for events the iterable already holds are written together. With
571
+ * `wire: 2` those events share one frame instead: `data:` is a JSON array of
572
+ * `CompactWireEvent`s and `id:` the last one's index. A
453
573
  * `stream({ presence: true })` iterable also yields presence items,
454
574
  * which ride as named frames old clients skip: `event:
455
575
  * presence-snapshot` first (the map, ISO dates), then `event: presence`
@@ -463,7 +583,8 @@ function sessionsSocket(resolve, socket, options) {
463
583
  * closes cleanly one second before it so the client reconnects without
464
584
  * a platform-timeout failure.
465
585
  */
466
- function sseResponse(iterable) {
586
+ function sseResponse(iterable, options = {}) {
587
+ const write = options.wire === 2 ? batchedFrames : eventFrames;
467
588
  const iterator = iterable[Symbol.asyncIterator]();
468
589
  const encoder = new TextEncoder();
469
590
  let heartbeat;
@@ -529,7 +650,7 @@ function sseResponse(iterable) {
529
650
  controller.close();
530
651
  return;
531
652
  }
532
- controller.enqueue(encoder.encode(frameFor(value)));
653
+ controller.enqueue(encoder.encode(write(value, iterator)));
533
654
  } catch (error) {
534
655
  if (closed) return;
535
656
  closed = true;
@@ -554,12 +675,114 @@ function sseResponse(iterable) {
554
675
  }
555
676
  });
556
677
  }
678
+ /** Version 1: one frame per event, then each event the iterator holds. */
679
+ function eventFrames(first, iterator) {
680
+ let frames = frameFor(first);
681
+ while (frames.length < EVENT_WRITE_CHARS) {
682
+ const event = takeBuffered(iterator);
683
+ if (!event) break;
684
+ frames += frameFor(event);
685
+ }
686
+ return frames;
687
+ }
688
+ /**
689
+ * Version 2: events this write carries share one frame, a JSON array whose
690
+ * `id:` is its last index. Held events are always events, so only `first`
691
+ * can be a snapshot or presence frame.
692
+ */
693
+ function batchedFrames(first, iterator) {
694
+ const lead = isStreamEvent(first) ? "" : frameFor(first);
695
+ const head = isStreamEvent(first) ? first : takeBuffered(iterator);
696
+ if (!head) return lead;
697
+ const { events, last } = heldEventBatch({
698
+ first: head,
699
+ iterator,
700
+ encode: compactJsonLine
701
+ });
702
+ return `${lead}id: ${last.index}\ndata: [${events.join(",")}]\n\n`;
703
+ }
704
+ const compactJsonLine = (event) => isSerializedEvent(event) && /[\r\n]/.test(event.payloadJson) ? compactEventJson(parseSerializedEvent(event)) : compactEventJson(event);
557
705
  function frameFor(item) {
706
+ if ("kind" in item) return `event: a2-snapshot\ndata: ${JSON.stringify(stateSnapshotToWire(item))}\n\n`;
558
707
  if ("snapshot" in item) return `event: presence-snapshot\ndata: ${JSON.stringify(presenceSnapshotToWire(item))}\n\n`;
559
708
  if ("participant" in item) return `event: presence\ndata: ${JSON.stringify(presencePatchToWire(item))}\n\n`;
709
+ if (isSerializedEvent(item)) return /[\r\n]/.test(item.payloadJson) ? `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(parseSerializedEvent(item)))}\n\n` : `id: ${item.index}\ndata: ${serializedEventToWireJson(item)}\n\n`;
560
710
  return `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(item))}\n\n`;
561
711
  }
562
712
  //#endregion
713
+ //#region src/bootstrap-stream.ts
714
+ function bootstrapStream(options) {
715
+ return { [Symbol.asyncIterator]() {
716
+ let closed = false;
717
+ let index;
718
+ let source;
719
+ let stopActivity;
720
+ const abort = new AbortController();
721
+ const activity = createStreamActivity(() => {
722
+ if (source) streamActivities.get(source)?.activate();
723
+ });
724
+ const iterator = {
725
+ async next() {
726
+ if (closed) return {
727
+ done: true,
728
+ value: void 0
729
+ };
730
+ if (index === void 0) {
731
+ let cancel;
732
+ const cancelled = new Promise((resolve) => {
733
+ cancel = () => resolve(void 0);
734
+ abort.signal.addEventListener("abort", cancel, { once: true });
735
+ });
736
+ try {
737
+ const snapshot = await Promise.race([options.snapshot(), cancelled]);
738
+ if (closed || snapshot === void 0) return {
739
+ done: true,
740
+ value: void 0
741
+ };
742
+ index = snapshot.index;
743
+ return {
744
+ done: false,
745
+ value: snapshot
746
+ };
747
+ } finally {
748
+ if (cancel) abort.signal.removeEventListener("abort", cancel);
749
+ }
750
+ }
751
+ if (!source) {
752
+ source = options.stream(index)[Symbol.asyncIterator]();
753
+ const sourceActivity = streamActivities.get(source);
754
+ if (sourceActivity) {
755
+ activity.setIdle(sourceActivity.idle);
756
+ stopActivity = sourceActivity.subscribe(activity.setIdle);
757
+ }
758
+ }
759
+ const result = await source.next();
760
+ return closed ? {
761
+ done: true,
762
+ value: void 0
763
+ } : result;
764
+ },
765
+ async return() {
766
+ if (closed) return {
767
+ done: true,
768
+ value: void 0
769
+ };
770
+ closed = true;
771
+ abort.abort();
772
+ stopActivity?.();
773
+ await source?.return?.();
774
+ return {
775
+ done: true,
776
+ value: void 0
777
+ };
778
+ }
779
+ };
780
+ streamActivities.set(iterator, activity);
781
+ bufferedEvents.set(iterator, () => closed || !source ? void 0 : takeBuffered(source));
782
+ return iterator;
783
+ } };
784
+ }
785
+ //#endregion
563
786
  //#region src/server-fetch.ts
564
787
  const serverFetchHooks = /* @__PURE__ */ new WeakMap();
565
788
  function setServerFetchHooks(server, hooks) {
@@ -581,6 +804,23 @@ const parseResumeIndex = (raw) => {
581
804
  if (!Number.isSafeInteger(index) || index < 0) throw new A2Error("INVALID_PAYLOAD", "stream index must be a non-negative safe integer");
582
805
  return index;
583
806
  };
807
+ const parseSubscription = (params) => {
808
+ const reducer = params.get("reducer");
809
+ const bootstrap = params.get("bootstrap");
810
+ const index = params.get("index");
811
+ if (reducer === "") throw new A2Error("INVALID_PAYLOAD", "empty reducer name");
812
+ if (bootstrap !== null) {
813
+ if (bootstrap !== "1" || index !== null || reducer === null) throw new A2Error("INVALID_PAYLOAD", "bootstrap requires a reducer and no index");
814
+ return {
815
+ bootstrap: true,
816
+ reducer
817
+ };
818
+ }
819
+ return {
820
+ index: parseResumeIndex(index),
821
+ ...reducer === null ? {} : { reducer }
822
+ };
823
+ };
584
824
  const errorResponse = (error) => {
585
825
  const a2error = asA2Error(error);
586
826
  return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
@@ -630,17 +870,54 @@ function createServerFetch(server) {
630
870
  return asA2Error(error);
631
871
  }
632
872
  };
633
- const attachMultiplexed = (socket) => {
873
+ const selectedSession = (sessionId, selection) => {
874
+ const reducer = server.contract.reducer;
875
+ if (selection.reducer !== void 0 && reducer !== void 0 && reducer.name !== selection.reducer) throw new A2Error("INVALID_PAYLOAD", "client reducer does not match the public contract reducer");
876
+ const target = server.session(sessionId);
877
+ const streamTarget = target;
878
+ if (!selection.bootstrap) return streamTarget;
879
+ if (!reducer) throw new A2Error("INVALID_PAYLOAD", "contract has no public reducer");
880
+ return {
881
+ ...streamTarget,
882
+ stream: (streamOptions) => bootstrapStream({
883
+ snapshot: async () => {
884
+ const initial = await target.state(reducer);
885
+ return {
886
+ kind: "snapshot",
887
+ sessionId,
888
+ name: reducer.name,
889
+ index: initial.index,
890
+ state: encodeState({
891
+ reducer,
892
+ state: initial.state
893
+ }),
894
+ ...initial.clearIndex === void 0 ? {} : { clearIndex: initial.clearIndex }
895
+ };
896
+ },
897
+ stream: (index) => {
898
+ const bounds = {
899
+ startAfter: index,
900
+ presence: streamOptions.presence === true
901
+ };
902
+ return target.stream(bounds);
903
+ }
904
+ })
905
+ };
906
+ };
907
+ const attachMultiplexed = (wire) => (socket) => {
634
908
  const deadline = invocationDeadlineMs();
635
- sessionsSocket(async (sessionId, startAfter) => {
636
- return await authorized({
909
+ sessionsSocket(async (sessionId, startAfter, selection = { index: startAfter }) => {
910
+ const operation = {
637
911
  type: "stream",
638
912
  sessionId,
639
913
  startAfter,
914
+ ...selection.bootstrap ? { bootstrap: true } : {},
640
915
  transport: "websocket"
641
- }) ? server.session(sessionId) : null;
916
+ };
917
+ return await authorized(operation) ? selectedSession(sessionId, selection) : null;
642
918
  }, socket, {
643
919
  presence,
920
+ wire,
644
921
  ...deadline === null ? {} : { deadline },
645
922
  gatePush: gateSocketPush
646
923
  });
@@ -649,23 +926,27 @@ function createServerFetch(server) {
649
926
  if (request.method === "GET") {
650
927
  if (request.headers.get("upgrade")?.toLowerCase() === "websocket") {
651
928
  if (options?.upgradeWebSocket === void 0) return new Response("WebSocket upgrade requested, but server.fetch() has no upgrade implementation", { status: 426 });
652
- if (options.multiplexWebSocket === true) return await options.upgradeWebSocket(attachMultiplexed);
653
929
  const { searchParams } = new URL(request.url);
930
+ const wire = wireVersion(searchParams.get("wire"));
931
+ if (options.multiplexWebSocket === true) return await options.upgradeWebSocket(attachMultiplexed(wire));
654
932
  const sessionId = searchParams.get("sessionId");
655
933
  if (sessionId === null || sessionId.length === 0) throw new A2Error("INVALID_PAYLOAD", "missing sessionId");
656
- const startAfter = parseResumeIndex(searchParams.get("index"));
934
+ const selection = parseSubscription(searchParams);
935
+ const startAfter = selection.index ?? 0;
657
936
  await authorize({
658
937
  type: "stream",
659
938
  sessionId,
660
939
  startAfter,
940
+ ...selection.bootstrap ? { bootstrap: true } : {},
661
941
  transport: "websocket"
662
942
  });
663
- const target = server.session(sessionId);
943
+ const target = selectedSession(sessionId, selection);
664
944
  const attach = (socket) => {
665
945
  const deadline = invocationDeadlineMs();
666
946
  sessionSocket(sessionId, target, socket, {
667
947
  startAfter,
668
948
  presence,
949
+ wire,
669
950
  ...deadline === null ? {} : { deadline },
670
951
  gatePush: gateSocketPush
671
952
  });
@@ -685,22 +966,28 @@ function createServerFetch(server) {
685
966
  ...bounds,
686
967
  transport: "http"
687
968
  });
688
- const events = await server.session(sessionId).history(bounds);
689
- const covered = events.length === bounds.lte - bounds.gte + 1;
690
- return Response.json(events.map(eventToWire), { headers: { "a2-history-covered": String(covered) } });
969
+ const { events, clearIndex } = await server.historyPage(sessionId, bounds);
970
+ const first = Math.max(bounds.gte, (clearIndex ?? 0) + 1);
971
+ const covered = events.length === Math.max(0, bounds.lte - first + 1);
972
+ return Response.json(events.map(eventToWire), { headers: {
973
+ "a2-history-covered": String(covered),
974
+ ...clearIndex === void 0 ? {} : { "a2-clear-index": String(clearIndex) }
975
+ } });
691
976
  }
692
- const startAfter = parseResumeIndex(searchParams.get("index"));
977
+ const selection = parseSubscription(searchParams);
978
+ const startAfter = selection.index ?? 0;
693
979
  await authorize({
694
980
  type: "stream",
695
981
  sessionId,
696
982
  startAfter,
983
+ ...selection.bootstrap ? { bootstrap: true } : {},
697
984
  transport: "http"
698
985
  });
699
- const target = server.session(sessionId);
986
+ const target = selectedSession(sessionId, selection);
700
987
  return sseResponse(presence ? target.stream({
701
988
  startAfter,
702
989
  presence: true
703
- }) : target.stream({ startAfter }));
990
+ }) : target.stream({ startAfter }), { wire: wireVersion(searchParams.get("wire")) });
704
991
  }
705
992
  if (request.method === "POST") {
706
993
  const body = await parsePushBody(request);
@@ -746,6 +1033,7 @@ const APPEND_NAMESPACE = "a2:handler-append:v2";
746
1033
  const RETURN_NAMESPACE = "a2:handler-return:v1";
747
1034
  const SCHEDULE_NAMESPACE = "a2:schedule:v1";
748
1035
  const SCHEDULED_EVENT_NAMESPACE = "a2:scheduled-event:v1";
1036
+ const SCHEDULE_CLEAR_NAMESPACE = "a2:schedule-clear:v1";
749
1037
  async function deterministicIdFromInput(input) {
750
1038
  const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
751
1039
  const bytes = new Uint8Array(digest).slice(0, 16);
@@ -773,13 +1061,31 @@ async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
773
1061
  itemOrdinal
774
1062
  ]);
775
1063
  }
776
- async function deterministicScheduleId(contract, sessionId, triggerEventId, name) {
1064
+ /**
1065
+ * The id of a scheduled append. `clearIndex` is the lifetime the trigger was
1066
+ * claimed in; it joins the input only when positive, so a trigger id reused
1067
+ * after a clear schedules a distinct task while sessions never cleared keep
1068
+ * the ids they always had.
1069
+ */
1070
+ async function deterministicScheduleId(contract, sessionId, triggerEventId, name, clearIndex = 0) {
777
1071
  return deterministicIdFromInput(JSON.stringify([
778
1072
  SCHEDULE_NAMESPACE,
779
1073
  contract,
780
1074
  sessionId,
781
1075
  triggerEventId,
782
- name
1076
+ name,
1077
+ ...clearIndex > 0 ? [clearIndex] : []
1078
+ ]));
1079
+ }
1080
+ async function deterministicScheduleClearId({ contract, sessionId, triggerEventId, clearIndex, name, ifLastIndex }) {
1081
+ return deterministicIdFromInput(JSON.stringify([
1082
+ SCHEDULE_CLEAR_NAMESPACE,
1083
+ contract,
1084
+ sessionId,
1085
+ triggerEventId,
1086
+ clearIndex,
1087
+ name,
1088
+ ifLastIndex
783
1089
  ]));
784
1090
  }
785
1091
  async function deterministicScheduledEventId(scheduleId, itemOrdinal) {
@@ -812,14 +1118,42 @@ const CONSOLE_TELEMETRY = { span: (name, attributes, fn) => fn({
812
1118
  */
813
1119
  async function deliverSchedulerAppend(server, task) {
814
1120
  if (server.contract.name !== task.contract) throw new TypeError(`a2 scheduler: append task for contract '${task.contract}' cannot be delivered to '${server.contract.name}'`);
1121
+ const { originIndex } = task;
1122
+ if (originIndex !== void 0 && (!Number.isSafeInteger(originIndex) || originIndex < 1)) throw new TypeError("a2 scheduler: append task originIndex must be a positive safe integer");
815
1123
  const internals = serverInternals.get(server);
816
1124
  if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled appends`);
817
- await internals.schedulerAppend(task.sessionId, task.events);
1125
+ await internals.schedulerAppend(task.sessionId, task.events, originIndex);
1126
+ }
1127
+ /**
1128
+ * Deliver one authenticated scheduler clear: a conditional
1129
+ * `clear({ ifLastIndex })` that resolves after its deletion finishes, so a
1130
+ * provider retry resumes an interrupted deletion. Custom scheduler adapters
1131
+ * call this after validating their transport envelope; application code uses
1132
+ * `session.schedule.clear`.
1133
+ */
1134
+ async function deliverSchedulerClear(server, task) {
1135
+ if (server.contract.name !== task.contract) throw new TypeError(`a2 scheduler: clear task for contract '${task.contract}' cannot be delivered to '${server.contract.name}'`);
1136
+ if (task.kind !== "clear" || !Number.isSafeInteger(task.ifLastIndex) || task.ifLastIndex < 0) throw new TypeError("a2 scheduler: a clear task needs kind \"clear\" and a non-negative safe integer ifLastIndex");
1137
+ const internals = serverInternals.get(server);
1138
+ if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled clears`);
1139
+ await internals.schedulerClear(task.sessionId, task.ifLastIndex);
818
1140
  }
819
1141
  function handler(entry) {
820
1142
  return typeof entry === "function" ? { handler: entry } : entry;
821
1143
  }
822
1144
  const MAX_FAILURES = 10;
1145
+ /** Abort a lost attempt's handler and release its drain slot. */
1146
+ function endSuperseded(entry, message) {
1147
+ entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", message));
1148
+ entry.fence({
1149
+ status: "superseded",
1150
+ opensLane: false,
1151
+ appendsEvents: false
1152
+ });
1153
+ }
1154
+ /** Pause between clear-deletion batches that found only locked rows. */
1155
+ const RECLAIM_STALL_MS = 20;
1156
+ const RECLAIM_STALL_LIMIT = 50;
823
1157
  /** Default: how long a presence value survives without a refreshing set. */
824
1158
  const PRESENCE_TTL_MS = 6e4;
825
1159
  function createDrainSignal() {
@@ -927,6 +1261,17 @@ function foldPresenceRows(rows) {
927
1261
  };
928
1262
  return map;
929
1263
  }
1264
+ const addSnapshotWrite = (writes, index, state, pinEventIndex) => {
1265
+ let write = writes.get(index);
1266
+ if (!write) {
1267
+ write = {
1268
+ state: cloneInitial(state),
1269
+ pinEventIndexes: /* @__PURE__ */ new Set()
1270
+ };
1271
+ writes.set(index, write);
1272
+ }
1273
+ if (pinEventIndex !== void 0) write.pinEventIndexes.add(pinEventIndex);
1274
+ };
930
1275
  /** Implement a contract: bind its vocabulary to storage and reactions. */
931
1276
  function createServer(options) {
932
1277
  const serverContract = options?.contract;
@@ -1107,7 +1452,7 @@ function createServer(options) {
1107
1452
  * the drain it schedules is created in the append's active context —
1108
1453
  * with a real tracer, causal work remains connected.
1109
1454
  */
1110
- const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds, requireDurableInitialArm = false) => telemetry.span("a2.append", {
1455
+ const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds, requireDurableInitialArm = false, originIndex) => telemetry.span("a2.append", {
1111
1456
  "a2.contract": name,
1112
1457
  "a2.session_id": sessionId,
1113
1458
  "a2.append.source": source,
@@ -1126,9 +1471,9 @@ function createServer(options) {
1126
1471
  let rows;
1127
1472
  let shouldHealSession;
1128
1473
  try {
1129
- const result = await store.append(nsId(sessionId), validated);
1474
+ const result = await store.append(nsId(sessionId), validated, originIndex === void 0 ? void 0 : { originIndex });
1130
1475
  rows = result.events;
1131
- shouldHealSession = result.hasPending;
1476
+ shouldHealSession = result.hasPending && !(source === "handler" && validated.every((event) => event.settled === true));
1132
1477
  } catch (err) {
1133
1478
  throw asStoreUnavailable(err);
1134
1479
  }
@@ -1204,113 +1549,263 @@ function createServer(options) {
1204
1549
  }
1205
1550
  return plans;
1206
1551
  };
1552
+ const persistSnapshots = ({ store, reducer, sessionId, snapshots }) => {
1553
+ const persistence = Promise.resolve().then(() => telemetry.span("a2.snapshot", {
1554
+ "a2.contract": name,
1555
+ "a2.session_id": sessionId,
1556
+ "a2.snapshot.reducer": reducer.name,
1557
+ "a2.snapshot.index": snapshots.at(-1).index,
1558
+ "a2.snapshot.count": snapshots.length
1559
+ }, async (span) => {
1560
+ try {
1561
+ await store.putSnapshots(nsId(sessionId), reducer.name, snapshots);
1562
+ } catch (error) {
1563
+ try {
1564
+ span.recordError(error);
1565
+ } catch (telemetryError) {
1566
+ throw new AggregateError([error, telemetryError], "Snapshot write and error reporting failed", { cause: telemetryError });
1567
+ }
1568
+ }
1569
+ })).catch((error) => {
1570
+ console.error("[a2] error in a2.snapshot telemetry", {
1571
+ sessionId,
1572
+ reducer: reducer.name,
1573
+ index: snapshots.at(-1).index
1574
+ }, error);
1575
+ });
1576
+ track(persistence);
1577
+ platformWaitUntil(persistence);
1578
+ };
1579
+ const stateCache = boundedCache(STATE_CACHE);
1580
+ const cacheState = (key, next) => {
1581
+ const current = stateCache.get(key);
1582
+ if (current?.reducer === next.reducer && (current.index > next.index || current.index === next.index && current.persistedIndex >= next.persistedIndex)) return;
1583
+ stateCache.set(key, next, next.bytes);
1584
+ };
1585
+ const writeSnapshots = ({ store, reducer, sessionId, writes }) => {
1586
+ const snapshots = [...writes].toSorted(([a], [b]) => a - b).map(([index, write]) => {
1587
+ const snapshot = {
1588
+ index,
1589
+ state: encodeState({
1590
+ reducer,
1591
+ state: write.state
1592
+ })
1593
+ };
1594
+ if (write.pinEventIndexes.size > 0) snapshot.pinEventIndexes = [...write.pinEventIndexes];
1595
+ return snapshot;
1596
+ });
1597
+ if (snapshots.length > 0) persistSnapshots({
1598
+ store,
1599
+ reducer,
1600
+ sessionId,
1601
+ snapshots
1602
+ });
1603
+ return snapshots;
1604
+ };
1605
+ /**
1606
+ * Folds `rows` from `position` in bound order, resolving each entry with a
1607
+ * clone once the rows through its bound are folded. `visit` sees each
1608
+ * entry's state before later rows are folded into it.
1609
+ */
1610
+ const foldEntries = ({ reducer, entries, rows, position, state, index, clearIndex, snapshot, visit }) => {
1611
+ const start = position;
1612
+ const ordered = entries.toSorted((a, b) => (a.throughIndex ?? Number.POSITIVE_INFINITY) - (b.throughIndex ?? Number.POSITIVE_INFINITY));
1613
+ for (const entry of ordered) {
1614
+ const target = entry.throughIndex ?? Number.POSITIVE_INFINITY;
1615
+ while (position < rows.length && rows[position].index <= target) {
1616
+ const row = rows[position];
1617
+ state = foldEvent(reducer, state, toPublic(row));
1618
+ index = row.index;
1619
+ position += 1;
1620
+ }
1621
+ if (entry.triggerIndex !== void 0 && entry.triggerIndex <= clearIndex || entry.throughIndex !== void 0 && entry.throughIndex > 0 && entry.throughIndex < clearIndex) {
1622
+ entry.reject(sessionClearedError(`session '${entry.sessionId}' was cleared at index ${clearIndex}`));
1623
+ continue;
1624
+ }
1625
+ entry.span.setAttribute("a2.state.snapshot", snapshot);
1626
+ entry.span.setAttribute("a2.state.folded", position - start);
1627
+ entry.span.setAttribute("a2.state.index", index);
1628
+ visit(entry, state, index, position - start);
1629
+ entry.resolve({
1630
+ state: cloneInitial(state),
1631
+ index,
1632
+ ...clearIndex > 0 && index >= clearIndex ? { clearIndex } : {}
1633
+ });
1634
+ }
1635
+ return {
1636
+ state,
1637
+ index,
1638
+ position
1639
+ };
1640
+ };
1641
+ /**
1642
+ * Events from `fromIndex` (inclusive) through `throughIndex` or the newest
1643
+ * event, or null when the tail is too long to be worth folding in memory.
1644
+ */
1645
+ const readCachedTail = async (store, sessionId, fromIndex, throughIndex) => {
1646
+ const readTail = stateTailReaders.get(store);
1647
+ if (readTail) try {
1648
+ return await readTail(nsId(sessionId), fromIndex, throughIndex);
1649
+ } catch (err) {
1650
+ throw asStoreUnavailable(err);
1651
+ }
1652
+ const limit = fromIndex + STATE_CACHE.maxTail;
1653
+ const capped = throughIndex === void 0 || throughIndex > limit;
1654
+ const rows = await readHistory(sessionId, {
1655
+ gte: fromIndex,
1656
+ lte: capped ? limit : throughIndex
1657
+ });
1658
+ return capped && rows.at(-1)?.index === limit ? null : rows;
1659
+ };
1660
+ /**
1661
+ * Serves a plan from the cached state when every read is at or past the
1662
+ * cached index. A read bounded at the cached index by an event whose id
1663
+ * matches the cached one (a handler's own trigger) needs no store request;
1664
+ * any other read fetches the tail from the cached event onward and checks
1665
+ * that event's id. Returns false when the store must answer instead.
1666
+ */
1667
+ const foldCachedPlan = async (store, reducer, plan) => {
1668
+ const key = stateCacheKey(plan.sessionId, reducer.name);
1669
+ const cached = stateCache.get(key);
1670
+ if (cached?.reducer !== reducer) return false;
1671
+ if (plan.entries.some((entry) => entry.throughIndex !== void 0 && entry.throughIndex < cached.index)) return false;
1672
+ let rows = [];
1673
+ if (plan.entries.some((entry) => entry.throughIndex !== cached.index || entry.throughEventId !== cached.lastId)) {
1674
+ const tail = await readCachedTail(store, plan.sessionId, cached.index, plan.throughIndex);
1675
+ if (!tail) return false;
1676
+ if (tail[0]?.index !== cached.index || tail[0].id !== cached.lastId || tail.some((row, position) => position > 0 && isSessionCleared(row))) {
1677
+ stateCache.delete(key);
1678
+ return false;
1679
+ }
1680
+ rows = tail;
1681
+ }
1682
+ const writes = /* @__PURE__ */ new Map();
1683
+ const folded = foldEntries({
1684
+ reducer,
1685
+ entries: plan.entries,
1686
+ rows,
1687
+ position: 1,
1688
+ state: rows.length > 1 ? cloneInitial(cached.state) : cached.state,
1689
+ index: cached.index,
1690
+ clearIndex: cached.clearIndex,
1691
+ snapshot: "memory",
1692
+ visit: (entry, state, index) => {
1693
+ if (entry.pinEventIndex !== void 0) addSnapshotWrite(writes, index, state, entry.pinEventIndex);
1694
+ }
1695
+ });
1696
+ const added = Math.max(0, folded.position - 1);
1697
+ const rewritesHead = folded.index - cached.persistedIndex >= STATE_CACHE.snapshotEvery;
1698
+ if (rewritesHead && !writes.has(folded.index)) writes.set(folded.index, {
1699
+ state: folded.state,
1700
+ pinEventIndexes: /* @__PURE__ */ new Set()
1701
+ });
1702
+ const snapshots = writeSnapshots({
1703
+ store,
1704
+ reducer,
1705
+ sessionId: plan.sessionId,
1706
+ writes
1707
+ });
1708
+ const measure = rewritesHead || cached.unmeasured + added >= STATE_CACHE.measureEvery;
1709
+ let grown = 0;
1710
+ if (!measure) for (let position = 1; position < folded.position; position += 1) grown += JSON.stringify(rows[position].payload)?.length ?? 0;
1711
+ cacheState(key, {
1712
+ reducer,
1713
+ index: folded.index,
1714
+ lastId: added > 0 ? rows[folded.position - 1].id : cached.lastId,
1715
+ state: folded.state,
1716
+ bytes: measure ? measuredBytes(reducer, folded.state, folded.index, snapshots) : cached.bytes + grown,
1717
+ unmeasured: measure ? 0 : cached.unmeasured + added,
1718
+ persistedIndex: Math.max(cached.persistedIndex, snapshots.at(-1)?.index ?? 0),
1719
+ clearIndex: cached.clearIndex
1720
+ });
1721
+ return true;
1722
+ };
1207
1723
  const foldStatePlan = async (store, reducer, plan, stateRead) => {
1208
1724
  let state = cloneInitial(reducer.initialState);
1209
1725
  let index = 0;
1210
1726
  let snapshotOutcome = "miss";
1211
1727
  const snap = stateRead.snapshot;
1212
1728
  let rows = stateRead.events;
1729
+ let clearIndex = stateRead.clearIndex ?? 0;
1213
1730
  if (snap) {
1214
- if (reducer.stateSchema) {
1215
- const result = validateSync(reducer.stateSchema, snap.state, "the stateSchema");
1731
+ let decoded;
1732
+ try {
1733
+ decoded = decodeState({
1734
+ reducer,
1735
+ state: snap.state
1736
+ });
1737
+ } catch {
1738
+ snapshotOutcome = "rejected";
1739
+ }
1740
+ if (snapshotOutcome !== "rejected" && reducer.stateSchema) {
1741
+ const result = validateSync(reducer.stateSchema, decoded, "the stateSchema");
1216
1742
  if (result.issues) snapshotOutcome = "rejected";
1217
1743
  else {
1218
1744
  state = result.value;
1219
1745
  index = snap.index;
1220
1746
  snapshotOutcome = "hit";
1221
1747
  }
1222
- } else {
1223
- state = snap.state;
1748
+ } else if (snapshotOutcome !== "rejected") {
1749
+ state = decoded;
1224
1750
  index = snap.index;
1225
1751
  snapshotOutcome = "hit";
1226
1752
  }
1227
1753
  }
1228
- if (snapshotOutcome === "rejected") try {
1229
- rows = await store.read(nsId(plan.sessionId), plan.throughIndex === void 0 ? void 0 : { throughIndex: plan.throughIndex });
1230
- } catch (err) {
1231
- throw asStoreUnavailable(err);
1232
- }
1233
- const ordered = plan.entries.toSorted((a, b) => (a.throughIndex ?? Number.POSITIVE_INFINITY) - (b.throughIndex ?? Number.POSITIVE_INFINITY));
1234
- const writes = /* @__PURE__ */ new Map();
1235
- let rowPosition = 0;
1236
- for (const entry of ordered) {
1237
- const target = entry.throughIndex ?? Number.POSITIVE_INFINITY;
1238
- while (rowPosition < rows.length && rows[rowPosition].index <= target) {
1239
- const row = rows[rowPosition];
1240
- state = reducer.fold(state, toPublic(row));
1241
- index = row.index;
1242
- rowPosition += 1;
1243
- }
1244
- const resultState = cloneInitial(state);
1245
- const folded = rowPosition;
1246
- entry.span.setAttribute("a2.state.snapshot", snapshotOutcome);
1247
- entry.span.setAttribute("a2.state.folded", folded);
1248
- entry.span.setAttribute("a2.state.index", index);
1249
- if (index > 0 && (folded > 0 || entry.pinEventIndex !== void 0)) {
1250
- const advancesHead = stateRead.headIndex === null || index > stateRead.headIndex;
1251
- const repairsHead = snapshotOutcome === "rejected" && index === stateRead.headIndex;
1252
- if (!advancesHead && !repairsHead && entry.pinEventIndex === void 0) {
1253
- entry.resolve({
1254
- state: resultState,
1255
- index
1256
- });
1257
- continue;
1258
- }
1259
- let write = writes.get(index);
1260
- if (!write) {
1261
- write = {
1262
- state: cloneInitial(resultState),
1263
- pinEventIndexes: /* @__PURE__ */ new Set()
1264
- };
1265
- writes.set(index, write);
1266
- }
1267
- if (entry.pinEventIndex !== void 0) write.pinEventIndexes.add(entry.pinEventIndex);
1754
+ if (snapshotOutcome === "rejected") {
1755
+ try {
1756
+ rows = await store.read(nsId(plan.sessionId), plan.throughIndex === void 0 ? void 0 : { throughIndex: plan.throughIndex });
1757
+ } catch (err) {
1758
+ throw asStoreUnavailable(err);
1268
1759
  }
1269
- entry.resolve({
1270
- state: resultState,
1271
- index
1272
- });
1760
+ if (rows[0] && isSessionCleared(rows[0])) clearIndex = Math.max(clearIndex, rows[0].index);
1273
1761
  }
1274
- if (writes.size > 0) {
1275
- const snapshots = [];
1276
- for (const [snapshotIndex, write] of writes) {
1277
- const snapshot = {
1278
- index: snapshotIndex,
1279
- state: write.state
1280
- };
1281
- if (write.pinEventIndexes.size > 0) snapshot.pinEventIndexes = [...write.pinEventIndexes];
1282
- snapshots.push(snapshot);
1762
+ const writes = /* @__PURE__ */ new Map();
1763
+ const folded = foldEntries({
1764
+ reducer,
1765
+ entries: plan.entries,
1766
+ rows,
1767
+ position: 0,
1768
+ state,
1769
+ index,
1770
+ clearIndex,
1771
+ snapshot: snapshotOutcome,
1772
+ visit: (entry, entryState, entryIndex, count) => {
1773
+ if (entryIndex === 0) return;
1774
+ if (count === 0 && entry.pinEventIndex === void 0) return;
1775
+ const advancesHead = stateRead.headIndex === null || entryIndex > stateRead.headIndex;
1776
+ const repairsHead = snapshotOutcome === "rejected" && entryIndex === stateRead.headIndex;
1777
+ if (!advancesHead && !repairsHead && entry.pinEventIndex === void 0) return;
1778
+ addSnapshotWrite(writes, entryIndex, entryState, entry.pinEventIndex);
1283
1779
  }
1284
- const persistence = Promise.resolve().then(() => telemetry.span("a2.snapshot", {
1285
- "a2.contract": name,
1286
- "a2.session_id": plan.sessionId,
1287
- "a2.snapshot.reducer": reducer.name,
1288
- "a2.snapshot.index": snapshots.at(-1).index,
1289
- "a2.snapshot.count": snapshots.length
1290
- }, async (span) => {
1291
- try {
1292
- await store.putSnapshots(nsId(plan.sessionId), reducer.name, snapshots);
1293
- } catch (error) {
1294
- try {
1295
- span.recordError(error);
1296
- } catch (telemetryError) {
1297
- throw new AggregateError([error, telemetryError], "Snapshot write and error reporting failed", { cause: telemetryError });
1298
- }
1299
- }
1300
- })).catch((error) => {
1301
- console.error("[a2] error in a2.snapshot telemetry", {
1302
- sessionId: plan.sessionId,
1303
- reducer: reducer.name,
1304
- index: snapshots.at(-1).index
1305
- }, error);
1306
- });
1307
- track(persistence);
1308
- platformWaitUntil(persistence);
1309
- }
1780
+ });
1781
+ const snapshots = writeSnapshots({
1782
+ store,
1783
+ reducer,
1784
+ sessionId: plan.sessionId,
1785
+ writes
1786
+ });
1787
+ if (folded.position > 0 && STATE_CACHE.maxEntries > 0) cacheState(stateCacheKey(plan.sessionId, reducer.name), {
1788
+ reducer,
1789
+ index: folded.index,
1790
+ lastId: rows[folded.position - 1].id,
1791
+ state: folded.state,
1792
+ bytes: measuredBytes(reducer, folded.state, folded.index, snapshots),
1793
+ unmeasured: 0,
1794
+ persistedIndex: Math.min(Math.max(stateRead.headIndex ?? 0, ...writes.keys()), folded.index),
1795
+ clearIndex
1796
+ });
1310
1797
  };
1311
1798
  const pendingStateReads = /* @__PURE__ */ new Map();
1312
1799
  const flushStateReads = async (store, reducer, batch) => {
1313
- const plans = planStateReads(batch);
1800
+ const plans = [];
1801
+ await Promise.all(planStateReads(batch).map(async (plan) => {
1802
+ try {
1803
+ if (!await foldCachedPlan(store, reducer, plan)) plans.push(plan);
1804
+ } catch (error) {
1805
+ for (const entry of plan.entries) entry.reject(error);
1806
+ }
1807
+ }));
1808
+ if (plans.length === 0) return;
1314
1809
  let reads;
1315
1810
  try {
1316
1811
  reads = store.readStates ? await store.readStates(plans.map((plan) => ({
@@ -1321,7 +1816,7 @@ function createServer(options) {
1321
1816
  if (reads.length !== plans.length) throw new TypeError("store.readStates must return one result per request");
1322
1817
  } catch (error) {
1323
1818
  const failure = asStoreUnavailable(error);
1324
- for (const entry of batch) entry.reject(failure);
1819
+ for (const plan of plans) for (const entry of plan.entries) entry.reject(failure);
1325
1820
  return;
1326
1821
  }
1327
1822
  await Promise.all(plans.map(async (plan, position) => {
@@ -1332,7 +1827,7 @@ function createServer(options) {
1332
1827
  }
1333
1828
  }));
1334
1829
  };
1335
- const enqueueStateRead = (store, sessionId, reducer, throughIndex, pinEventIndex, span) => new Promise((resolve, reject) => {
1830
+ const enqueueStateRead = (store, sessionId, reducer, bounds, span) => new Promise((resolve, reject) => {
1336
1831
  const reducerName = reducer.name;
1337
1832
  let batch = pendingStateReads.get(reducerName);
1338
1833
  if (!batch) {
@@ -1346,25 +1841,25 @@ function createServer(options) {
1346
1841
  }
1347
1842
  batch.push({
1348
1843
  sessionId,
1349
- ...throughIndex === void 0 ? {} : { throughIndex },
1350
- ...pinEventIndex === void 0 ? {} : { pinEventIndex },
1844
+ ...bounds,
1351
1845
  span,
1352
1846
  resolve,
1353
1847
  reject
1354
1848
  });
1355
1849
  });
1356
- const readState = async (sessionId, reducer, throughIndex, pinEventIndex) => {
1850
+ const readState = async (sessionId, reducer, bounds) => {
1357
1851
  const store = await resolveStore();
1358
1852
  return telemetry.span("a2.state", {
1359
1853
  "a2.contract": name,
1360
1854
  "a2.session_id": sessionId,
1361
1855
  "a2.state.reducer": reducer.name
1362
- }, (span) => enqueueStateRead(store, sessionId, reducer, throughIndex, pinEventIndex, span));
1856
+ }, (span) => enqueueStateRead(store, sessionId, reducer, bounds, span));
1363
1857
  };
1364
1858
  const makeSchedule = (sessionId, trigger) => {
1365
1859
  const triggerEventId = trigger?.id ?? null;
1366
1860
  const triggerCreatedAt = trigger?.createdAt.getTime();
1367
- return async (scheduleName, timing, ...events) => {
1861
+ const clearIndex = trigger?.clearIndex ?? 0;
1862
+ const schedule = async (scheduleName, timing, ...events) => {
1368
1863
  if (!scheduler) throw new TypeError("session.schedule requires a configured scheduler adapter");
1369
1864
  assertScheduleName(scheduleName);
1370
1865
  if (events.length === 0) throw new TypeError("schedule requires at least one event");
@@ -1379,7 +1874,7 @@ function createServer(options) {
1379
1874
  id: event.id
1380
1875
  });
1381
1876
  const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
1382
- const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName);
1877
+ const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex);
1383
1878
  const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
1384
1879
  id: event.id ?? await deterministicScheduledEventId(scheduleId, item),
1385
1880
  type: event.type,
@@ -1394,12 +1889,45 @@ function createServer(options) {
1394
1889
  contract: name,
1395
1890
  sessionId,
1396
1891
  dueAt,
1397
- events: scheduledEvents
1892
+ events: scheduledEvents,
1893
+ ...trigger ? { originIndex: trigger.index } : {}
1398
1894
  });
1399
1895
  } catch (error) {
1400
1896
  throw markSchedulerSendFailure(error);
1401
1897
  }
1402
1898
  };
1899
+ const clear = async (scheduleName, clearOptions) => {
1900
+ if (!scheduler) throw new TypeError("session.schedule.clear requires a configured scheduler adapter");
1901
+ assertScheduleName(scheduleName);
1902
+ if (clearOptions === null || typeof clearOptions !== "object") throw new TypeError("schedule.clear requires an ifLastIndex");
1903
+ const { ifLastIndex, ...timing } = clearOptions;
1904
+ assertClearIndex(ifLastIndex);
1905
+ const dueAt = scheduleDueAt(timing, triggerCreatedAt ?? Date.now());
1906
+ const store = await resolveStore();
1907
+ if (!store.clear || !store.reclaim) throw new TypeError("session.schedule.clear requires a store that supports clearing");
1908
+ const id = await deterministicScheduleClearId({
1909
+ contract: name,
1910
+ sessionId,
1911
+ triggerEventId,
1912
+ clearIndex,
1913
+ name: scheduleName,
1914
+ ifLastIndex
1915
+ });
1916
+ try {
1917
+ await scheduler.schedule({
1918
+ version: 1,
1919
+ kind: "clear",
1920
+ id,
1921
+ contract: name,
1922
+ sessionId,
1923
+ dueAt,
1924
+ ifLastIndex
1925
+ });
1926
+ } catch (error) {
1927
+ throw markSchedulerSendFailure(error);
1928
+ }
1929
+ };
1930
+ return Object.assign(schedule, { clear });
1403
1931
  };
1404
1932
  const toSessionEvent = (id, event) => ({
1405
1933
  id: event.id,
@@ -1459,6 +1987,13 @@ function createServer(options) {
1459
1987
  }
1460
1988
  };
1461
1989
  streamActivities.set(stream, activity);
1990
+ bufferedEvents.set(stream, () => {
1991
+ const event = closed || !inner ? void 0 : takeBuffered(inner);
1992
+ if (!event) return void 0;
1993
+ if (!isSerializedEvent(event)) return toSessionEvent(id, event);
1994
+ event.sessionId = id;
1995
+ return event;
1996
+ });
1462
1997
  return stream;
1463
1998
  } };
1464
1999
  };
@@ -1514,6 +2049,7 @@ function createServer(options) {
1514
2049
  const ns = nsId(id);
1515
2050
  let closed = false;
1516
2051
  let events;
2052
+ let pendingEvent = null;
1517
2053
  let unsubscribe;
1518
2054
  let sleeper = null;
1519
2055
  let wakeQueue = null;
@@ -1655,7 +2191,6 @@ function createServer(options) {
1655
2191
  }
1656
2192
  return null;
1657
2193
  };
1658
- let pendingEvent = null;
1659
2194
  try {
1660
2195
  unsubscribe = api.subscribe?.(ns, enqueue);
1661
2196
  events = store.stream(ns, { startAfter })[Symbol.asyncIterator]();
@@ -1745,18 +2280,86 @@ function createServer(options) {
1745
2280
  }
1746
2281
  };
1747
2282
  streamActivities.set(stream, activity);
2283
+ bufferedEvents.set(stream, () => {
2284
+ if (closed || !events || pendingEvent) return void 0;
2285
+ const event = takeBuffered(events);
2286
+ if (!event) return void 0;
2287
+ if (!isSerializedEvent(event)) return toSessionEvent(id, event);
2288
+ event.sessionId = id;
2289
+ return event;
2290
+ });
1748
2291
  return stream;
1749
2292
  } };
1750
2293
  };
1751
- const makeSession = (id, append, trigger) => ({
2294
+ const reclaimAll = async (store, ns) => {
2295
+ for (let stalls = 0;;) {
2296
+ let batch;
2297
+ try {
2298
+ batch = await store.reclaim(ns);
2299
+ } catch (err) {
2300
+ throw asStoreUnavailable(err);
2301
+ }
2302
+ if (batch.done) return;
2303
+ if (batch.deleted > 0) {
2304
+ stalls = 0;
2305
+ continue;
2306
+ }
2307
+ stalls += 1;
2308
+ if (stalls === RECLAIM_STALL_LIMIT) throw new A2Error("STORE_UNAVAILABLE", `deleting the cleared history of session '${stripNs(ns)}' made no progress`);
2309
+ await defaultSleep(RECLAIM_STALL_MS).promise;
2310
+ }
2311
+ };
2312
+ const clearSession = async (sessionId, clearOptions, attempt) => {
2313
+ const ifLastIndex = clearOptions?.ifLastIndex;
2314
+ if (ifLastIndex !== void 0) assertClearIndex(ifLastIndex);
2315
+ const store = await resolveStore();
2316
+ if (!store.clear || !store.reclaim) throw new TypeError("session.clear requires a store that supports clearing");
2317
+ const ns = nsId(sessionId);
2318
+ let result;
2319
+ try {
2320
+ result = await store.clear({
2321
+ sessionId: ns,
2322
+ ...ifLastIndex === void 0 ? {} : { ifLastIndex },
2323
+ ...attempt ? { cause: attempt.cause } : {}
2324
+ });
2325
+ } catch (err) {
2326
+ throw asStoreUnavailable(err);
2327
+ }
2328
+ if (result.cleared) {
2329
+ const cachePrefix = stateCacheKey(sessionId, "");
2330
+ stateCache.deleteWhere((key) => key.startsWith(cachePrefix));
2331
+ }
2332
+ const deletion = reclaimAll(store, ns);
2333
+ if (attempt) {
2334
+ if (attempt.cause.index <= result.clearIndex) attempt.end();
2335
+ const kept = deletion.catch(() => void 0);
2336
+ track(kept);
2337
+ platformWaitUntil(kept);
2338
+ }
2339
+ await deletion;
2340
+ return {
2341
+ cleared: result.cleared,
2342
+ index: result.index
2343
+ };
2344
+ };
2345
+ const makeSession = (id, append, trigger, attempt) => ({
1752
2346
  id,
1753
2347
  append,
1754
2348
  schedule: makeSchedule(id, trigger),
1755
- history: async (bounds) => (await readHistory(id, bounds)).map(toPublic),
1756
- state: (reducer, stateOptions) => {
2349
+ clear: (clearOptions) => clearSession(id, clearOptions, attempt),
2350
+ history: async (bounds) => (await readHistory(id, bounds)).filter((row) => !isSessionCleared(row)).map(toPublic),
2351
+ state: (reducer = serverContract.reducer, stateOptions) => {
2352
+ if (!reducer) throw new A2Error("INVALID_PAYLOAD", "contract has no public reducer");
1757
2353
  const through = stateOptions?.through ?? trigger?.index;
1758
2354
  if (through !== void 0 && through !== "latest") assertStateIndex(through);
1759
- return readState(id, reducer, through === "latest" ? void 0 : through, trigger && through !== "latest" ? trigger.index : void 0);
2355
+ const bounds = {};
2356
+ if (through !== void 0 && through !== "latest") {
2357
+ bounds.throughIndex = through;
2358
+ if (trigger) bounds.pinEventIndex = trigger.index;
2359
+ if (through === trigger?.index && !attempt?.signal.aborted) bounds.throughEventId = trigger.id;
2360
+ }
2361
+ if (trigger) bounds.triggerIndex = trigger.index;
2362
+ return readState(id, reducer, bounds);
1760
2363
  },
1761
2364
  stream: (opts) => {
1762
2365
  const startAfter = opts?.startAfter ?? 0;
@@ -1768,7 +2371,7 @@ function createServer(options) {
1768
2371
  presence: () => readPresenceMap(id)
1769
2372
  } : {}
1770
2373
  });
1771
- const makeCtx = (trigger, attempt, signal) => {
2374
+ const makeCtx = (trigger, attempt, signal, endAttempt) => {
1772
2375
  const publicSessionId = stripNs(trigger.sessionId);
1773
2376
  const append = async (appendName, ...events) => {
1774
2377
  assertAppendName(appendName);
@@ -1789,7 +2392,14 @@ function createServer(options) {
1789
2392
  return {
1790
2393
  event: toPublic(trigger),
1791
2394
  attempt,
1792
- session: makeSession(publicSessionId, append, trigger),
2395
+ session: makeSession(publicSessionId, append, trigger, {
2396
+ cause: {
2397
+ index: trigger.index,
2398
+ attempt
2399
+ },
2400
+ end: endAttempt,
2401
+ signal
2402
+ }),
1793
2403
  signal
1794
2404
  };
1795
2405
  };
@@ -1988,12 +2598,7 @@ function createServer(options) {
1988
2598
  for (const index of result.superseded) {
1989
2599
  const entry = active.get(index);
1990
2600
  if (!entry) continue;
1991
- entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", `a newer attempt owns event ${index} in session '${sessionId}'`));
1992
- entry.fence({
1993
- status: "superseded",
1994
- opensLane: false,
1995
- appendsEvents: false
1996
- });
2601
+ endSuperseded(entry, `a newer attempt owns event ${index} in session '${sessionId}'`);
1997
2602
  }
1998
2603
  if (window.deadlineCapped) stopRenewing = true;
1999
2604
  scheduleLapse();
@@ -2007,7 +2612,7 @@ function createServer(options) {
2007
2612
  renewal = tracked;
2008
2613
  return tracked;
2009
2614
  };
2010
- const runEvent = async (event, supersedeSignal) => {
2615
+ const runEvent = async (event, supersedeSignal, endAttempt) => {
2011
2616
  const registration = handlers.get(event.type);
2012
2617
  return telemetry.span("a2.event", {
2013
2618
  "a2.contract": name,
@@ -2027,7 +2632,7 @@ function createServer(options) {
2027
2632
  subscription = await subscribeAbort(store, ns, event, registration.abortOn, event.attemptCount);
2028
2633
  handlerSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
2029
2634
  }
2030
- const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal));
2635
+ const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal, endAttempt));
2031
2636
  const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
2032
2637
  if (subscription) {
2033
2638
  await subscription.close();
@@ -2044,12 +2649,22 @@ function createServer(options) {
2044
2649
  };
2045
2650
  }
2046
2651
  eventSpan.recordError(err);
2047
- const failure = await store.failAttempt({
2048
- sessionId: ns,
2049
- index: event.index,
2050
- attempt: event.attemptCount,
2051
- error: describeError(err),
2052
- maxFailures: err instanceof NonRetriableError ? 1 : MAX_FAILURES
2652
+ const failure = await telemetry.span("a2.commit", {
2653
+ "a2.contract": name,
2654
+ "a2.session_id": sessionId,
2655
+ "a2.commit.operation": "fail",
2656
+ "a2.commit.index": event.index,
2657
+ "a2.commit.attempt": event.attemptCount
2658
+ }, async (commitSpan) => {
2659
+ const result = await store.failAttempt({
2660
+ sessionId: ns,
2661
+ index: event.index,
2662
+ attempt: event.attemptCount,
2663
+ error: describeError(err),
2664
+ maxFailures: err instanceof NonRetriableError ? 1 : MAX_FAILURES
2665
+ });
2666
+ commitSpan.setAttribute("a2.commit.outcome", result.outcome);
2667
+ return result;
2053
2668
  });
2054
2669
  eventSpan.setAttribute("a2.event.outcome", failure.outcome);
2055
2670
  return {
@@ -2061,11 +2676,21 @@ function createServer(options) {
2061
2676
  await subscription?.close();
2062
2677
  }
2063
2678
  await renewal;
2064
- const completion = await store.completeAttempt({
2065
- sessionId: ns,
2066
- index: event.index,
2067
- attempt: event.attemptCount,
2068
- events: returned
2679
+ const completion = await telemetry.span("a2.commit", {
2680
+ "a2.contract": name,
2681
+ "a2.session_id": sessionId,
2682
+ "a2.commit.operation": "complete",
2683
+ "a2.commit.index": event.index,
2684
+ "a2.commit.attempt": event.attemptCount
2685
+ }, async (commitSpan) => {
2686
+ const result = await store.completeAttempt({
2687
+ sessionId: ns,
2688
+ index: event.index,
2689
+ attempt: event.attemptCount,
2690
+ events: returned
2691
+ });
2692
+ commitSpan.setAttribute("a2.commit.outcome", result.outcome);
2693
+ return result;
2069
2694
  });
2070
2695
  eventSpan.setAttribute("a2.event.outcome", completion.outcome === "completed" ? "processed" : completion.outcome);
2071
2696
  if (completion.outcome === "superseded") return {
@@ -2084,21 +2709,18 @@ function createServer(options) {
2084
2709
  let claimAgain = false;
2085
2710
  const start = (event, expiresAtMs) => {
2086
2711
  const previous = active.get(event.index);
2087
- if (previous) {
2088
- previous.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", `a newer attempt owns event ${event.index} in session '${sessionId}'`));
2089
- previous.fence({
2090
- status: "superseded",
2091
- opensLane: false,
2092
- appendsEvents: false
2093
- });
2094
- }
2712
+ if (previous) endSuperseded(previous, `a newer attempt owns event ${event.index} in session '${sessionId}'`);
2095
2713
  const supersede = new AbortController();
2096
2714
  let fence;
2097
2715
  const fenced = new Promise((resolve) => {
2098
2716
  fence = resolve;
2099
2717
  });
2718
+ const endAttempt = () => endSuperseded({
2719
+ supersede,
2720
+ fence
2721
+ }, `event ${event.index} in session '${sessionId}' was cleared`);
2100
2722
  let entry;
2101
- const execution = Promise.race([runEvent(event, supersede.signal), fenced]).then((eventOutcome) => {
2723
+ const execution = Promise.race([runEvent(event, supersede.signal, endAttempt), fenced]).then((eventOutcome) => {
2102
2724
  if (active.get(event.index) === entry) active.delete(event.index);
2103
2725
  if (eventOutcome.status === "processed") processed += 1;
2104
2726
  if (eventOutcome.status === "failed") {
@@ -2131,12 +2753,20 @@ function createServer(options) {
2131
2753
  let claim;
2132
2754
  if (shouldClaim) {
2133
2755
  const window = nextClaimWindow();
2134
- claim = await store.claimAvailable({
2135
- sessionId: ns,
2136
- holder,
2137
- ttlMs: window.ttlMs,
2138
- ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
2139
- ...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
2756
+ claim = await telemetry.span("a2.claim", {
2757
+ "a2.contract": name,
2758
+ "a2.session_id": sessionId
2759
+ }, async (claimSpan) => {
2760
+ const result = await store.claimAvailable({
2761
+ sessionId: ns,
2762
+ holder,
2763
+ ttlMs: window.ttlMs,
2764
+ ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
2765
+ ...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
2766
+ });
2767
+ claimSpan.setAttribute("a2.claim.outcome", result.outcome);
2768
+ claimSpan.setAttribute("a2.claim.count", result.outcome === "claimed" ? result.events.length : 0);
2769
+ return result;
2140
2770
  });
2141
2771
  if (claim.outcome === "claimed") {
2142
2772
  requestWatchdog(window.watchdogAtMs);
@@ -2195,8 +2825,8 @@ function createServer(options) {
2195
2825
  ...schedulerArm ? { schedulerArm } : {}
2196
2826
  };
2197
2827
  });
2198
- const appendExternal = async (sessionId, mode, events, requireDurableInitialArm = false) => {
2199
- return appendCore(sessionId, events, "external", mode, (_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt), void 0, void 0, requireDurableInitialArm);
2828
+ const appendExternal = async (sessionId, mode, events, requireDurableInitialArm = false, originIndex) => {
2829
+ return appendCore(sessionId, events, "external", mode, (_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt), void 0, void 0, requireDurableInitialArm, originIndex);
2200
2830
  };
2201
2831
  const session = (id) => {
2202
2832
  assertSessionId(id);
@@ -2205,11 +2835,21 @@ function createServer(options) {
2205
2835
  const append = Object.assign(appendInline, { dispatch: appendDispatch });
2206
2836
  return makeSession(id, append, null);
2207
2837
  };
2838
+ const historyPage = async (sessionId, bounds) => {
2839
+ assertSessionId(sessionId);
2840
+ const rows = await readHistory(sessionId, bounds);
2841
+ const marker = rows[0] && isSessionCleared(rows[0]) ? rows[0] : void 0;
2842
+ return {
2843
+ events: (marker ? rows.slice(1) : rows).map(toPublic),
2844
+ ...marker ? { clearIndex: marker.index } : {}
2845
+ };
2846
+ };
2208
2847
  const self = {
2209
2848
  contract: serverContract,
2210
2849
  fetch: createServerFetch({
2211
2850
  contract: serverContract,
2212
- session
2851
+ session,
2852
+ historyPage
2213
2853
  }),
2214
2854
  session,
2215
2855
  async drain(sessionId) {
@@ -2226,10 +2866,21 @@ function createServer(options) {
2226
2866
  assertSessionId(sessionId);
2227
2867
  return drainSession(sessionId, opts?.watchdogDueAt);
2228
2868
  },
2229
- schedulerAppend: async (sessionId, events) => {
2869
+ schedulerAppend: async (sessionId, events, originIndex) => {
2230
2870
  if (!scheduler) throw new TypeError("scheduled append delivery requires a configured scheduler adapter");
2231
2871
  assertSessionId(sessionId);
2232
- await appendExternal(sessionId, "inline", [...events], true);
2872
+ try {
2873
+ await appendExternal(sessionId, "inline", [...events], true, originIndex);
2874
+ } catch (error) {
2875
+ if (originIndex !== void 0 && error instanceof A2Error && error.code === "SESSION_CLEARED") return;
2876
+ throw error;
2877
+ }
2878
+ },
2879
+ historyWithMarkers: async (sessionId, gte) => (await readHistory(sessionId, { gte })).map(toPublic),
2880
+ schedulerClear: async (sessionId, ifLastIndex) => {
2881
+ assertSessionId(sessionId);
2882
+ assertClearIndex(ifLastIndex);
2883
+ await clearSession(sessionId, { ifLastIndex }, void 0);
2233
2884
  }
2234
2885
  });
2235
2886
  return self;
@@ -2247,6 +2898,9 @@ function assertStreamIndex(value) {
2247
2898
  function assertStateIndex(value) {
2248
2899
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
2249
2900
  }
2901
+ function assertClearIndex(value) {
2902
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("ifLastIndex must be a non-negative safe integer");
2903
+ }
2250
2904
  function assertAppendName(name) {
2251
2905
  if (typeof name !== "string" || name.length === 0) throw new TypeError("handler append name must be a non-empty string");
2252
2906
  }
@@ -2328,14 +2982,12 @@ function isJsonValue(value, ancestors) {
2328
2982
  ancestors.delete(value);
2329
2983
  }
2330
2984
  }
2331
- function cloneInitial(initial) {
2332
- try {
2333
- return structuredClone(initial);
2334
- } catch {
2335
- return initial;
2336
- }
2337
- }
2985
+ const measuredBytes = (reducer, state, index, snapshots) => JSON.stringify(snapshots.find((snapshot) => snapshot.index === index)?.state ?? encodeState({
2986
+ reducer,
2987
+ state
2988
+ }))?.length ?? 0;
2989
+ const stateCacheKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
2338
2990
  //#endregion
2339
- export { sseResponse as a, setServerFetchHooks as i, deliverSchedulerAppend as n, parsePresenceSibling as o, handler as r, createServer as t };
2991
+ export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c, handler as i, boundedCache as l, deliverSchedulerAppend as n, setServerFetchHooks as o, deliverSchedulerClear as r, sseResponse as s, createServer as t };
2340
2992
 
2341
- //# sourceMappingURL=server-BD5ckxZb.js.map
2993
+ //# sourceMappingURL=server-Rj64fNvX.js.map