experimental-a2 0.16.1 → 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 (333) hide show
  1. package/CHANGELOG.md +352 -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-DgOBltJ_.js → ai-UrWfKeuv.js} +621 -216
  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 +432 -258
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
  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 +671 -581
  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 +656 -194
  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 +6 -3
  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 +92 -72
  256. package/src/ai-coordinator.ts +23 -22
  257. package/src/ai-message-base.ts +209 -0
  258. package/src/ai-message-projection.ts +262 -64
  259. package/src/ai-progress-batches.ts +26 -0
  260. package/src/ai-projector.ts +95 -14
  261. package/src/ai-react.ts +146 -0
  262. package/src/ai-server.browser.ts +2 -2
  263. package/src/ai-server.ts +20 -1964
  264. package/src/ai-stored-state.ts +48 -163
  265. package/src/ai.ts +339 -155
  266. package/src/bootstrap-stream.ts +72 -0
  267. package/src/bounded-cache.ts +55 -0
  268. package/src/client-render-session.ts +49 -0
  269. package/src/client.ts +688 -151
  270. package/src/contract.ts +92 -42
  271. package/src/deterministic-id.ts +37 -0
  272. package/src/equal-json.ts +32 -0
  273. package/src/errors.ts +13 -1
  274. package/src/index.ts +5 -1
  275. package/src/initial-snapshot.ts +39 -0
  276. package/src/internal.ts +45 -0
  277. package/src/postgres-notification-scope.ts +14 -3
  278. package/src/react.ts +171 -206
  279. package/src/redis-http-reply.ts +256 -0
  280. package/src/redis-http-stream.ts +73 -0
  281. package/src/redis-http-subscriptions.ts +13 -5
  282. package/src/reducer-runtime.ts +52 -0
  283. package/src/reducer.ts +92 -10
  284. package/src/scheduler-qstash.ts +49 -20
  285. package/src/scheduler-task.ts +34 -2
  286. package/src/scheduler-vercel.ts +32 -84
  287. package/src/server-fetch.ts +139 -35
  288. package/src/server.ts +957 -237
  289. package/src/session-clear.ts +23 -0
  290. package/src/session-socket.ts +154 -49
  291. package/src/sse.ts +71 -4
  292. package/src/store-memory.ts +220 -78
  293. package/src/store-polling.ts +18 -4
  294. package/src/store-postgres.ts +461 -114
  295. package/src/store-redis-core.ts +1260 -434
  296. package/src/store-redis-http.ts +18 -2
  297. package/src/store-redis-notify.ts +260 -85
  298. package/src/store-redis.ts +19 -9
  299. package/src/store-sqlite.ts +259 -45
  300. package/src/store.ts +46 -1
  301. package/src/stream-buffer.ts +43 -0
  302. package/src/telemetry.ts +13 -1
  303. package/src/wire.ts +241 -13
  304. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  305. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  306. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  307. package/dist/ai-DgOBltJ_.js.map +0 -1
  308. package/dist/client-BYzHjkwU.js.map +0 -1
  309. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  310. package/dist/contract-CKRg_E4q.js.map +0 -1
  311. package/dist/errors-DCk6ch5n.js.map +0 -1
  312. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  313. package/dist/internal-Dq2qYxou.js.map +0 -1
  314. package/dist/react.d.ts.map +0 -1
  315. package/dist/reducer-DEMjEY_O.js +0 -29
  316. package/dist/reducer-DEMjEY_O.js.map +0 -1
  317. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  318. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  319. package/dist/server-BD5ckxZb.js.map +0 -1
  320. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  321. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  322. package/dist/store-polling-CmxUbV93.js.map +0 -1
  323. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  324. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  325. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  326. package/dist/validate-XKT4FSNn.js +0 -30
  327. package/dist/validate-XKT4FSNn.js.map +0 -1
  328. package/dist/wire-BO5wWCb1.js.map +0 -1
  329. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  330. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  331. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  332. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  333. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -1,12 +1,12 @@
1
- import { t as A2Error } from "./errors-DCk6ch5n.js";
2
- import { h as nullProtoRecord, r as NOTIFY_TIMINGS } from "./internal-Dq2qYxou.js";
3
- import { n as pollingStream, t as defaultSleep } from "./store-polling-CmxUbV93.js";
1
+ import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-DGI4jTQ5.js";
2
+ import { g as nullProtoRecord, r as NOTIFY_TIMINGS } from "./internal-CbVvEwad.js";
3
+ import { n as pollingStream, t as defaultSleep } from "./store-polling-4_y8lETm.js";
4
4
  import { r as platformWaitUntil } from "./platform-B4TnJtWu.js";
5
5
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
6
- import { t as idempotentReplay } from "./idempotent-replay-DVOlyYbx.js";
6
+ import { t as idempotentReplay } from "./idempotent-replay-JdgMQRRm.js";
7
7
  import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
8
8
  import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
9
- import { t as pollPresence } from "./store-presence-polling-C7-XZyW9.js";
9
+ import { t as pollPresence } from "./store-presence-polling-Q1C-k2dd.js";
10
10
  import { createHash } from "node:crypto";
11
11
  //#region src/postgres-notifications.ts
12
12
  const postgresNotificationChannel = ({ kind, sessionId }) => `a2_${kind}_${createHash("md5").update(sessionId).digest("hex")}`;
@@ -232,11 +232,14 @@ function postgresNotificationScope(load) {
232
232
  let stopped = false;
233
233
  let after = startAfter;
234
234
  let buffer = [];
235
+ let position = 0;
235
236
  let version = 0;
236
237
  let waiting;
237
238
  let lease;
238
239
  const stop = () => {
239
240
  stopped = true;
241
+ buffer = [];
242
+ position = 0;
240
243
  waiting?.cancel();
241
244
  lease?.stop();
242
245
  state.stops.delete(stop);
@@ -262,20 +265,28 @@ function postgresNotificationScope(load) {
262
265
  done: true,
263
266
  value: void 0
264
267
  };
265
- const row = buffer.shift();
268
+ const row = buffer[position];
266
269
  if (row) {
267
270
  after = row.index;
271
+ buffer[position] = void 0;
272
+ position += 1;
273
+ if (position === buffer.length) {
274
+ buffer = [];
275
+ position = 0;
276
+ }
268
277
  return {
269
278
  done: false,
270
279
  value: row
271
280
  };
272
281
  }
273
282
  const before = version;
274
- buffer = await read(after);
283
+ const rows = await read(after);
275
284
  if (stopped || state.closed) return {
276
285
  done: true,
277
286
  value: void 0
278
287
  };
288
+ buffer = rows;
289
+ position = 0;
279
290
  if (buffer.length || before !== version) continue;
280
291
  waiting = defaultSleep(NOTIFY_TIMINGS.safetyReadMs);
281
292
  await waiting.promise;
@@ -563,6 +574,11 @@ create table if not exists a2_snapshot_pins (
563
574
  create index if not exists a2_snapshot_pins_checkpoint
564
575
  on a2_snapshot_pins (session_id, reducer_name, up_to_index);
565
576
 
577
+ create table if not exists a2_clears (
578
+ session_id text primary key,
579
+ clear_index bigint not null
580
+ );
581
+
566
582
  create table if not exists a2_presence (
567
583
  ns text not null,
568
584
  participant text not null,
@@ -575,6 +591,14 @@ create table if not exists a2_presence (
575
591
  );
576
592
  `;
577
593
  const asNumber = (value) => Number(value);
594
+ /**
595
+ * The session's latest clear marker, or 0, as a primary-key subquery. Read
596
+ * it in a statement that runs after the operation's lock: a statement's
597
+ * snapshot starts with the statement, so one that waited for the lock could
598
+ * miss a clear that committed meanwhile.
599
+ */
600
+ const clearIndexOf = (session) => `coalesce((select clear_index from a2_clears where session_id = ${session}), 0)`;
601
+ const RECLAIM_BATCH = 256;
578
602
  const toDate = (ms) => ms === null || ms === void 0 ? null : new Date(asNumber(ms));
579
603
  const toCause = (raw) => {
580
604
  if (raw === null || raw === void 0) return null;
@@ -755,17 +779,23 @@ function postgres(options = {}) {
755
779
  "throughIndex" bigint,
756
780
  "snapshotThroughIndex" bigint
757
781
  )
782
+ ), bounded as materialized (
783
+ select request.*, coalesce(clears.clear_index, 0) as clear_index
784
+ from request
785
+ left join a2_clears clears on clears.session_id = request."sessionId"
758
786
  ), selected as materialized (
759
787
  select request.position,
760
788
  request."sessionId" as session_id,
761
789
  request."throughIndex" as through_index,
790
+ request.clear_index,
762
791
  head.up_to_index as head_index,
763
792
  candidate.up_to_index as snapshot_index,
764
793
  candidate.state as snapshot_state
765
- from request
794
+ from bounded request
766
795
  left join a2_snapshots head
767
796
  on head.session_id = request."sessionId"
768
797
  and head.reducer_name = $2
798
+ and head.up_to_index >= request.clear_index
769
799
  left join lateral (
770
800
  select up_to_index, state from (
771
801
  select head.up_to_index, head.state
@@ -776,6 +806,7 @@ function postgres(options = {}) {
776
806
  from a2_snapshot_history history
777
807
  where history.session_id = request."sessionId"
778
808
  and history.reducer_name = $2
809
+ and history.up_to_index >= request.clear_index
779
810
  and (
780
811
  request."snapshotThroughIndex" is null
781
812
  or history.up_to_index <= request."snapshotThroughIndex"
@@ -787,7 +818,7 @@ function postgres(options = {}) {
787
818
  )
788
819
  select selected.position, 0 as row_order, selected.session_id,
789
820
  selected.head_index, selected.snapshot_index,
790
- selected.snapshot_state,
821
+ selected.snapshot_state, selected.clear_index,
791
822
  null::bigint as idx, null::text as event_type,
792
823
  null::jsonb as payload, null::text as event_id,
793
824
  null::bigint as created_at
@@ -795,12 +826,15 @@ function postgres(options = {}) {
795
826
  union all
796
827
  select selected.position, 1 as row_order, event.session_id,
797
828
  selected.head_index, null::bigint as snapshot_index,
798
- null::jsonb as snapshot_state,
829
+ null::jsonb as snapshot_state, selected.clear_index,
799
830
  event.idx, event.event_type, event.payload,
800
831
  event.event_id, event.created_at
801
832
  from selected
802
833
  join a2_events event on event.session_id = selected.session_id
803
- where event.idx > coalesce(selected.snapshot_index, 0)
834
+ where event.idx > greatest(
835
+ coalesce(selected.snapshot_index, 0),
836
+ selected.clear_index - 1
837
+ )
804
838
  and (
805
839
  selected.through_index is null
806
840
  or event.idx <= selected.through_index
@@ -814,6 +848,8 @@ function postgres(options = {}) {
814
848
  for (const row of rows) {
815
849
  const read = reads[asNumber(row["position"])];
816
850
  if (row["row_order"] === 0 || row["row_order"] === "0") {
851
+ const clearIndex = asNumber(row["clear_index"]);
852
+ if (clearIndex > 0) read.clearIndex = clearIndex;
817
853
  read.headIndex = row["head_index"] === null ? null : asNumber(row["head_index"]);
818
854
  if (row["snapshot_index"] !== null) read.snapshot = {
819
855
  index: asNumber(row["snapshot_index"]),
@@ -854,12 +890,66 @@ function postgres(options = {}) {
854
890
  }
855
891
  });
856
892
  };
893
+ /**
894
+ * The rows owning `ids`. This session's rows below its clear marker do
895
+ * not own their ids: they are deleted here so the ids can be reused.
896
+ */
897
+ const liveOwnersOf = async ({ query, sessionId, clearIndex, ids }) => {
898
+ if (ids.length === 0) return [];
899
+ const { rows } = await query("select * from a2_events where event_id = any($1::text[]) order by idx", [ids]);
900
+ const stale = rows.filter((row) => row["session_id"] === sessionId && asNumber(row["idx"]) < clearIndex);
901
+ if (stale.length === 0) return rows;
902
+ const { rows: deleted } = await query(`with stale as materialized (
903
+ select idx from a2_events
904
+ where session_id = $1 and event_id = any($2::text[])
905
+ order by idx
906
+ for update skip locked
907
+ )
908
+ delete from a2_events event using stale
909
+ where event.session_id = $1 and event.idx = stale.idx
910
+ returning event.idx`, [sessionId, stale.map((row) => row["event_id"])]);
911
+ if (deleted.length < stale.length) throw new A2Error("STORE_UNAVAILABLE", `an event id being reused is still held by cleared work in session '${sessionId}'`);
912
+ return rows.filter((row) => !stale.includes(row));
913
+ };
914
+ /** Whether `index` is at or below the boundary, read in a fresh statement. */
915
+ const clearedAt = async ({ query, sessionId, index }) => {
916
+ const { rows } = await query(`select ${clearIndexOf("$1")} as clear_index`, [sessionId]);
917
+ return index <= asNumber(rows[0]?.["clear_index"]);
918
+ };
919
+ /** Insert one event; its lane is ready unless an unfinished lane event of this lifetime precedes it. */
920
+ const insertEvent = async ({ query, row, clearIndex, returning = false }) => {
921
+ const { rows } = await query(`insert into a2_events
922
+ (session_id, idx, event_type, payload, event_id, created_at,
923
+ cause, lane, lane_ready, processed_at)
924
+ values (
925
+ $1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,
926
+ $8::text is null or not exists (
927
+ select 1 from a2_events
928
+ where session_id = $1 and lane = $8 and processed_at is null
929
+ and idx > $10
930
+ ),
931
+ $9
932
+ )${returning ? " returning *" : ""}`, [
933
+ row.sessionId,
934
+ row.index,
935
+ row.type,
936
+ jsonb(row.payload),
937
+ row.id,
938
+ row.createdAt,
939
+ row.cause ? jsonb(row.cause) : null,
940
+ row.lane,
941
+ row.processedAt,
942
+ clearIndex
943
+ ]);
944
+ return rows[0];
945
+ };
857
946
  const store = {
858
- async append(sessionId, events) {
947
+ async append(sessionId, events, appendOptions) {
859
948
  if (events.length === 0) return wrap(async () => {
860
949
  const { rows } = await (await client()).query(`select exists(
861
950
  select 1 from a2_events
862
951
  where session_id = $1 and processed_at is null
952
+ and idx > ${clearIndexOf("$1")}
863
953
  ) as has_pending`, [sessionId]);
864
954
  return {
865
955
  events: [],
@@ -868,28 +958,41 @@ function postgres(options = {}) {
868
958
  });
869
959
  return wrap(() => withTx(async (query) => {
870
960
  await query("select pg_advisory_xact_lock(hashtext($1))", [sessionId]);
961
+ const { rows: statusRows } = await query(`select coalesce(max(idx), 0) as max,
962
+ ${clearIndexOf("$1")} as clear_index,
963
+ exists(
964
+ select 1 from a2_events pending
965
+ where pending.session_id = $1
966
+ and pending.processed_at is null
967
+ and pending.idx > ${clearIndexOf("$1")}
968
+ ) as session_has_pending
969
+ from a2_events
970
+ where session_id = $1`, [sessionId]);
971
+ const base = asNumber(statusRows[0]?.["max"]);
972
+ const clearIndex = asNumber(statusRows[0]?.["clear_index"]);
973
+ const sessionHasPending = statusRows[0]?.["session_has_pending"] === true;
871
974
  const suppliedIds = events.filter((e) => e.id !== void 0).map((e) => e.id);
872
975
  if (new Set(suppliedIds).size !== suppliedIds.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
873
976
  if (suppliedIds.length > 0) {
874
- const { rows: existing } = await query(`select stored.*,
875
- exists(
876
- select 1 from a2_events pending
877
- where pending.session_id = $2
878
- and pending.processed_at is null
879
- ) as session_has_pending
880
- from a2_events stored
881
- where stored.event_id = any($1)
882
- order by stored.idx`, [suppliedIds, sessionId]);
977
+ const existing = await liveOwnersOf({
978
+ query,
979
+ sessionId,
980
+ clearIndex,
981
+ ids: suppliedIds
982
+ });
883
983
  if (existing.length > 0) {
884
984
  const foreign = existing.find((row) => row["session_id"] !== sessionId);
885
985
  if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${String(foreign["event_id"])}' already exists in another session`);
886
986
  if (existing.length === events.length) return {
887
987
  events: idempotentReplay(events, existing.map(toStored)),
888
- hasPending: existing[0]?.["session_has_pending"] === true
988
+ hasPending: sessionHasPending
889
989
  };
890
990
  throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
891
991
  }
892
992
  }
993
+ const originIndex = appendOptions?.originIndex;
994
+ if (originIndex !== void 0 && originIndex <= clearIndex) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
995
+ for (const e of events) if (e.cause && e.cause.index <= clearIndex) throw supersededAttemptError(sessionId, e.cause);
893
996
  const causeIndexes = [...new Set(events.filter((e) => e.cause !== void 0).map((e) => e.cause.index))];
894
997
  if (causeIndexes.length > 0) {
895
998
  const { rows: parents } = await query(`select idx, attempt_count, failed_at from a2_events
@@ -900,45 +1003,30 @@ function postgres(options = {}) {
900
1003
  if (!e.cause) continue;
901
1004
  const parent = byIndex.get(e.cause.index);
902
1005
  if (!parent) throw new TypeError(`no event at index ${e.cause.index} in session '${sessionId}'`);
903
- if (asNumber(parent["attempt_count"]) !== e.cause.attempt || parent["failed_at"] !== null) throw new A2Error("SUPERSEDED_ATTEMPT", `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`);
1006
+ if (asNumber(parent["attempt_count"]) !== e.cause.attempt || parent["failed_at"] !== null) throw supersededAttemptError(sessionId, e.cause);
904
1007
  }
905
1008
  }
906
- const { rows: maxRows } = await query(`select coalesce(max(idx), 0) as max,
907
- exists(
908
- select 1 from a2_events pending
909
- where pending.session_id = $1
910
- and pending.processed_at is null
911
- ) as session_has_pending
912
- from a2_events
913
- where session_id = $1`, [sessionId]);
914
- const base = asNumber(maxRows[0]?.["max"]);
915
1009
  const now = clock.now().getTime();
916
1010
  const inserted = [];
917
1011
  for (let i = 0; i < events.length; i += 1) {
918
1012
  const e = events[i];
919
1013
  const id = e.id ?? generateId();
920
1014
  const index = base + 1 + i;
921
- await query(`insert into a2_events
922
- (session_id, idx, event_type, payload, event_id, created_at,
923
- cause, lane, lane_ready, processed_at)
924
- values (
925
- $1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,
926
- $8::text is null or not exists (
927
- select 1 from a2_events
928
- where session_id = $1 and lane = $8 and processed_at is null
929
- ),
930
- $9
931
- )`, [
932
- sessionId,
933
- index,
934
- e.type,
935
- jsonb(e.payload),
936
- id,
937
- now,
938
- e.cause ? jsonb(e.cause) : null,
939
- e.lane ?? null,
940
- e.settled ? now : null
941
- ]);
1015
+ await insertEvent({
1016
+ query,
1017
+ row: {
1018
+ sessionId,
1019
+ index,
1020
+ type: e.type,
1021
+ payload: e.payload,
1022
+ id,
1023
+ createdAt: now,
1024
+ cause: e.cause ?? null,
1025
+ lane: e.lane ?? null,
1026
+ processedAt: e.settled ? now : null
1027
+ },
1028
+ clearIndex
1029
+ });
942
1030
  inserted.push({
943
1031
  id,
944
1032
  type: e.type,
@@ -965,7 +1053,7 @@ function postgres(options = {}) {
965
1053
  }
966
1054
  return {
967
1055
  events: inserted,
968
- hasPending: maxRows[0]?.["session_has_pending"] === true || events.some((event) => event.settled !== true)
1056
+ hasPending: sessionHasPending || events.some((event) => event.settled !== true)
969
1057
  };
970
1058
  }));
971
1059
  },
@@ -982,6 +1070,7 @@ function postgres(options = {}) {
982
1070
  params.push(opts.throughIndex);
983
1071
  conditions.push(`idx <= $${params.length}`);
984
1072
  }
1073
+ conditions.push(`idx >= ${clearIndexOf("$1")}`);
985
1074
  const { rows } = await c.query(`select * from a2_events where ${conditions.join(" and ")} order by idx`, params);
986
1075
  return rows.map(toStored);
987
1076
  });
@@ -1000,6 +1089,7 @@ function postgres(options = {}) {
1000
1089
  params.push(opts.throughIndex);
1001
1090
  conditions.push(`idx <= $${params.length}`);
1002
1091
  }
1092
+ conditions.push(`idx >= ${clearIndexOf("$1")}`);
1003
1093
  const { rows } = await c.query(`select session_id, idx, event_id, event_type, payload, created_at
1004
1094
  from a2_events where ${conditions.join(" and ")} order by idx`, params);
1005
1095
  return rows.map(toEvent);
@@ -1008,6 +1098,7 @@ function postgres(options = {}) {
1008
1098
  async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes = [] }) {
1009
1099
  return wrap(() => withTx(async (query) => {
1010
1100
  const now = clock.now().getTime();
1101
+ const expiresAt = expiresAtMs ?? now + ttlMs;
1011
1102
  const { rows } = await query(`with eligible as materialized (
1012
1103
  select event.idx
1013
1104
  from a2_events event
@@ -1015,6 +1106,7 @@ function postgres(options = {}) {
1015
1106
  and event.processed_at is null
1016
1107
  and event.failed_at is null
1017
1108
  and event.lane_ready
1109
+ and event.idx > ${clearIndexOf("$1")}
1018
1110
  and (event.claim_expires_at is null or event.claim_expires_at <= $2)
1019
1111
  and not (event.idx = any($5::bigint[]))
1020
1112
  for update of event
@@ -1027,16 +1119,21 @@ function postgres(options = {}) {
1027
1119
  claim_expires_at = $4
1028
1120
  from eligible
1029
1121
  where claimed.session_id = $1 and claimed.idx = eligible.idx
1030
- returning claimed.*`, [
1122
+ returning claimed.*, ${clearIndexOf("$1")} as clear_index`, [
1031
1123
  sessionId,
1032
1124
  now,
1033
1125
  holder,
1034
- expiresAtMs ?? now + ttlMs,
1126
+ expiresAt,
1035
1127
  [...excludeIndexes]
1036
1128
  ]);
1037
1129
  if (rows.length > 0) return {
1038
1130
  outcome: "claimed",
1039
- events: rows.map(toStored).toSorted((a, b) => a.index - b.index)
1131
+ events: rows.map((row) => {
1132
+ const stored = toStored(row);
1133
+ const clearIndex = asNumber(row["clear_index"]);
1134
+ if (clearIndex > 0) stored.clearIndex = clearIndex;
1135
+ return stored;
1136
+ }).toSorted((a, b) => a.index - b.index)
1040
1137
  };
1041
1138
  const { rows: activeRows } = await query(`select min(claim_expires_at) as retry_at
1042
1139
  from a2_events
@@ -1044,6 +1141,7 @@ function postgres(options = {}) {
1044
1141
  and processed_at is null
1045
1142
  and failed_at is null
1046
1143
  and lane_ready
1144
+ and idx > ${clearIndexOf("$1")}
1047
1145
  and claim_expires_at > $2`, [sessionId, now]);
1048
1146
  const retryAt = activeRows[0]?.["retry_at"];
1049
1147
  return retryAt === null || retryAt === void 0 ? { outcome: "settled" } : {
@@ -1072,6 +1170,7 @@ function postgres(options = {}) {
1072
1170
  from listed
1073
1171
  where event.session_id = $2
1074
1172
  and event.idx = listed.idx
1173
+ and event.idx > ${clearIndexOf("$2")}
1075
1174
  and event.attempt_count = listed.attempt
1076
1175
  and event.processed_at is null
1077
1176
  and event.failed_at is null
@@ -1079,11 +1178,13 @@ function postgres(options = {}) {
1079
1178
  and event.claim_expires_at > $5
1080
1179
  returning event.idx
1081
1180
  )
1082
- select event.idx, event.attempt_count,
1083
- exists(select 1 from renewed where renewed.idx = event.idx)
1084
- as renewed
1085
- from a2_events event
1086
- where event.session_id = $2 and event.idx = any($3::bigint[])`, [
1181
+ select listed.idx, event.attempt_count,
1182
+ exists(select 1 from renewed where renewed.idx = listed.idx)
1183
+ as renewed,
1184
+ listed.idx <= ${clearIndexOf("$2")} as cleared
1185
+ from listed
1186
+ left join a2_events event
1187
+ on event.session_id = $2 and event.idx = listed.idx`, [
1087
1188
  expiresAt,
1088
1189
  sessionId,
1089
1190
  indexes,
@@ -1096,7 +1197,7 @@ function postgres(options = {}) {
1096
1197
  for (const row of rows) {
1097
1198
  const index = asNumber(row["idx"]);
1098
1199
  if (row["renewed"] === true) renewed.push(index);
1099
- else if (asNumber(row["attempt_count"]) > (attempts.get(index) ?? 0)) superseded.push(index);
1200
+ else if (row["cleared"] === true || row["attempt_count"] !== null && asNumber(row["attempt_count"]) > (attempts.get(index) ?? 0)) superseded.push(index);
1100
1201
  }
1101
1202
  return {
1102
1203
  renewed: renewed.toSorted((a, b) => a - b),
@@ -1106,11 +1207,23 @@ function postgres(options = {}) {
1106
1207
  },
1107
1208
  async completeAttempt({ sessionId, index, attempt, events }) {
1108
1209
  return wrap(() => withTx(async (query) => {
1109
- const { rows: parentRows } = await query(`select * from a2_events
1110
- where session_id = $1 and idx = $2
1111
- for update`, [sessionId, index]);
1112
- const parent = parentRows[0];
1113
- if (!parent) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
1210
+ const { rows: parentRows } = await query(`select boundary.clear_index, parent.*
1211
+ from (select ${clearIndexOf("$1")} as clear_index) boundary
1212
+ left join lateral (
1213
+ select * from a2_events
1214
+ where session_id = $1 and idx = $2 and idx > boundary.clear_index
1215
+ for update
1216
+ ) parent on true`, [sessionId, index]);
1217
+ if (index <= asNumber(parentRows[0]?.["clear_index"])) return { outcome: "superseded" };
1218
+ const parent = parentRows[0]?.["idx"] == null ? void 0 : parentRows[0];
1219
+ if (!parent) {
1220
+ if (await clearedAt({
1221
+ query,
1222
+ sessionId,
1223
+ index
1224
+ })) return { outcome: "superseded" };
1225
+ throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
1226
+ }
1114
1227
  const ids = events.map((event) => event.id);
1115
1228
  if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
1116
1229
  if (parent["processed_at"] !== null) {
@@ -1125,6 +1238,11 @@ function postgres(options = {}) {
1125
1238
  where event_id = any($1::text[])`, [ids]);
1126
1239
  const byId = new Map(existing.map((row) => [row["event_id"], row]));
1127
1240
  const ordered = ids.map((id) => byId.get(id));
1241
+ if (ordered.some((row) => !row) && await clearedAt({
1242
+ query,
1243
+ sessionId,
1244
+ index
1245
+ })) return { outcome: "superseded" };
1128
1246
  if (ordered.some((row) => {
1129
1247
  const cause = row ? toCause(row["cause"]) : null;
1130
1248
  return !row || row["session_id"] !== sessionId || cause?.index !== index || cause.attempt !== attempt;
@@ -1136,42 +1254,44 @@ function postgres(options = {}) {
1136
1254
  }
1137
1255
  if (asNumber(parent["attempt_count"]) !== attempt || parent["claim_holder"] === null || parent["claim_holder"] === void 0 || parent["failed_at"] !== null) return { outcome: "superseded" };
1138
1256
  await query("select pg_advisory_xact_lock(hashtext($1))", [sessionId]);
1257
+ const { rows: maxRows } = await query(`select coalesce(max(idx), 0) as max, ${clearIndexOf("$1")} as clear_index
1258
+ from a2_events where session_id = $1`, [sessionId]);
1259
+ const clearIndex = asNumber(maxRows[0]?.["clear_index"]);
1260
+ if (index <= clearIndex) return { outcome: "superseded" };
1139
1261
  if (ids.length > 0) {
1140
- const { rows: existing } = await query("select event_id from a2_events where event_id = any($1::text[])", [ids]);
1141
- if (existing.length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
1262
+ if ((await liveOwnersOf({
1263
+ query,
1264
+ sessionId,
1265
+ clearIndex,
1266
+ ids
1267
+ })).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
1142
1268
  }
1143
- const { rows: maxRows } = await query("select coalesce(max(idx), 0) as max from a2_events where session_id = $1", [sessionId]);
1144
1269
  const base = asNumber(maxRows[0]?.["max"]);
1145
1270
  const now = clock.now().getTime();
1146
1271
  const inserted = [];
1147
1272
  for (let offset = 0; offset < events.length; offset += 1) {
1148
1273
  const event = events[offset];
1149
- const { rows } = await query(`insert into a2_events
1150
- (session_id, idx, event_type, payload, event_id, created_at,
1151
- cause, lane, lane_ready, processed_at)
1152
- values (
1153
- $1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,
1154
- $8::text is null or not exists (
1155
- select 1 from a2_events
1156
- where session_id = $1 and lane = $8 and processed_at is null
1157
- ),
1158
- $9
1159
- )
1160
- returning *`, [
1161
- sessionId,
1162
- base + offset + 1,
1163
- event.type,
1164
- jsonb(event.payload),
1165
- event.id,
1166
- now,
1167
- jsonb({
1168
- index,
1169
- attempt
1170
- }),
1171
- event.lane ?? null,
1172
- event.settled ? now : null
1173
- ]);
1174
- inserted.push(toStored(rows[0]));
1274
+ const childIndex = base + offset + 1;
1275
+ const row = await insertEvent({
1276
+ query,
1277
+ row: {
1278
+ sessionId,
1279
+ index: childIndex,
1280
+ type: event.type,
1281
+ payload: event.payload,
1282
+ id: event.id,
1283
+ createdAt: now,
1284
+ cause: {
1285
+ index,
1286
+ attempt
1287
+ },
1288
+ lane: event.lane ?? null,
1289
+ processedAt: event.settled ? now : null
1290
+ },
1291
+ clearIndex,
1292
+ returning: true
1293
+ });
1294
+ inserted.push(toStored(row));
1175
1295
  }
1176
1296
  await query(`update a2_events set
1177
1297
  processed_at = $1,
@@ -1203,8 +1323,13 @@ function postgres(options = {}) {
1203
1323
  and idx = (
1204
1324
  select idx from a2_events
1205
1325
  where session_id = $1 and lane = $2 and processed_at is null
1326
+ and idx > $3
1206
1327
  order by idx limit 1
1207
- )`, [sessionId, parentLane]);
1328
+ )`, [
1329
+ sessionId,
1330
+ parentLane,
1331
+ clearIndex
1332
+ ]);
1208
1333
  return {
1209
1334
  outcome: "completed",
1210
1335
  events: inserted
@@ -1216,6 +1341,7 @@ function postgres(options = {}) {
1216
1341
  const { rows } = await query(`with current as materialized (
1217
1342
  select e.* from a2_events e
1218
1343
  where e.session_id = $1 and e.idx = $2
1344
+ and e.idx > ${clearIndexOf("$1")}
1219
1345
  for update of e
1220
1346
  ), updated as (
1221
1347
  update a2_events e set
@@ -1239,6 +1365,7 @@ function postgres(options = {}) {
1239
1365
  returning e.failure_count, e.failed_at
1240
1366
  )
1241
1367
  select case
1368
+ when $2 <= ${clearIndexOf("$1")} then 'cleared'
1242
1369
  when not exists (select 1 from current) then 'missing'
1243
1370
  when exists (
1244
1371
  select 1 from updated where failed_at is not null
@@ -1269,7 +1396,21 @@ function postgres(options = {}) {
1269
1396
  clock.now().getTime()
1270
1397
  ]);
1271
1398
  const row = rows[0];
1272
- if (!row || row["outcome"] === "missing") throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
1399
+ if (row?.["outcome"] === "cleared") return {
1400
+ outcome: "superseded",
1401
+ failureCount: 0
1402
+ };
1403
+ if (!row || row["outcome"] === "missing") {
1404
+ if (await clearedAt({
1405
+ query,
1406
+ sessionId,
1407
+ index
1408
+ })) return {
1409
+ outcome: "superseded",
1410
+ failureCount: 0
1411
+ };
1412
+ throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
1413
+ }
1273
1414
  if (row["outcome"] === "dead_lettered") {
1274
1415
  await query(`delete from a2_snapshot_pins
1275
1416
  where session_id = $1 and event_index = $2`, [sessionId, index]);
@@ -1300,17 +1441,32 @@ function postgres(options = {}) {
1300
1441
  },
1301
1442
  async putSnapshots(sessionId, reducerName, writes) {
1302
1443
  await wrap(() => withTx(async (query) => {
1303
- const normalized = [...new Map(writes.map((write) => [write.index, write])).values()].toSorted((a, b) => a.index - b.index);
1304
- if (normalized.length === 0) return;
1444
+ const requested = [...new Map(writes.map((write) => [write.index, write])).values()].toSorted((a, b) => a.index - b.index);
1445
+ if (requested.length === 0) return;
1305
1446
  await query("select pg_advisory_xact_lock(hashtext($1), hashtext($2))", [sessionId, reducerName]);
1447
+ const { rows: headRows } = await query(`select boundary.clear_index, head.up_to_index, head.state, head.updated_at
1448
+ from (select ${clearIndexOf("$1")} as clear_index) boundary
1449
+ left join lateral (
1450
+ select up_to_index, state, updated_at from a2_snapshots
1451
+ where session_id = $1 and reducer_name = $2
1452
+ for update
1453
+ ) head on true`, [sessionId, reducerName]);
1454
+ const clearIndex = asNumber(headRows[0]?.["clear_index"]);
1455
+ const normalized = requested.filter((write) => write.index >= clearIndex);
1456
+ if (normalized.length === 0) return;
1457
+ const currentHead = headRows[0]?.["up_to_index"] == null ? void 0 : headRows[0];
1306
1458
  const requestedPins = normalized.flatMap((write) => (write.pinEventIndexes ?? []).map((eventIndex) => ({
1307
1459
  eventIndex,
1308
1460
  snapshotIndex: write.index
1309
1461
  })));
1310
1462
  if (requestedPins.length > 0) {
1311
1463
  const { rows: events } = await query(`select idx, processed_at, failed_at from a2_events
1312
- where session_id = $1 and idx = any($2::bigint[])
1313
- for update`, [sessionId, [...new Set(requestedPins.map((pin) => pin.eventIndex))]]);
1464
+ where session_id = $1 and idx = any($2::bigint[]) and idx > $3
1465
+ for update`, [
1466
+ sessionId,
1467
+ [...new Set(requestedPins.map((pin) => pin.eventIndex))],
1468
+ clearIndex
1469
+ ]);
1314
1470
  const unfinished = new Set(events.filter((row) => row["processed_at"] === null && row["failed_at"] === null).map((row) => asNumber(row["idx"])));
1315
1471
  const accepted = requestedPins.filter((pin) => unfinished.has(pin.eventIndex));
1316
1472
  if (accepted.length > 0) await query(`insert into a2_snapshot_pins
@@ -1326,10 +1482,6 @@ function postgres(options = {}) {
1326
1482
  jsonb(accepted)
1327
1483
  ]);
1328
1484
  }
1329
- const { rows: headRows } = await query(`select up_to_index, state, updated_at from a2_snapshots
1330
- where session_id = $1 and reducer_name = $2
1331
- for update`, [sessionId, reducerName]);
1332
- const currentHead = headRows[0];
1333
1485
  const latestWrite = normalized.at(-1);
1334
1486
  const writesHead = !currentHead || latestWrite.index >= asNumber(currentHead["up_to_index"]);
1335
1487
  const advancesHead = !currentHead || latestWrite.index > asNumber(currentHead["up_to_index"]);
@@ -1343,7 +1495,7 @@ function postgres(options = {}) {
1343
1495
  ]);
1344
1496
  const pinned = new Set(pinRows.map((row) => asNumber(row["up_to_index"])));
1345
1497
  const historical = /* @__PURE__ */ new Map();
1346
- if (advancesHead && currentHead && pinned.has(asNumber(currentHead["up_to_index"]))) {
1498
+ if (advancesHead && currentHead && asNumber(currentHead["up_to_index"]) >= clearIndex && pinned.has(asNumber(currentHead["up_to_index"]))) {
1347
1499
  const index = asNumber(currentHead["up_to_index"]);
1348
1500
  historical.set(index, {
1349
1501
  index,
@@ -1476,6 +1628,7 @@ function postgres(options = {}) {
1476
1628
  const { rows } = await (await client()).query(`select session_id, idx, event_type, payload, event_id, created_at
1477
1629
  from a2_events
1478
1630
  where session_id = $1 and idx > $2
1631
+ and idx >= ${clearIndexOf("$1")}
1479
1632
  order by idx`, [sessionId, afterIndex]);
1480
1633
  return rows.map(toEvent);
1481
1634
  });
@@ -1489,6 +1642,144 @@ function postgres(options = {}) {
1489
1642
  });
1490
1643
  return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
1491
1644
  },
1645
+ async clear({ sessionId, ifLastIndex, cause }) {
1646
+ return wrap(() => withTx(async (query) => {
1647
+ await query("select pg_advisory_xact_lock(hashtext($1))", [sessionId]);
1648
+ const { rows: headRows } = await query(`select coalesce(max(idx), 0) as head,
1649
+ ${clearIndexOf("$1")} as clear_index
1650
+ from a2_events where session_id = $1`, [sessionId]);
1651
+ const head = asNumber(headRows[0]?.["head"]);
1652
+ const clearIndex = asNumber(headRows[0]?.["clear_index"]);
1653
+ if (cause) {
1654
+ if (cause.index <= clearIndex) throw supersededAttemptError(sessionId, cause);
1655
+ const { rows: parents } = await query(`select attempt_count, failed_at from a2_events
1656
+ where session_id = $1 and idx = $2
1657
+ for update`, [sessionId, cause.index]);
1658
+ const parent = parents[0];
1659
+ if (!parent) throw new TypeError(`no event at index ${cause.index} in session '${sessionId}'`);
1660
+ if (asNumber(parent["attempt_count"]) !== cause.attempt || parent["failed_at"] !== null) throw supersededAttemptError(sessionId, cause);
1661
+ }
1662
+ if (ifLastIndex !== void 0 && clearIndex === ifLastIndex + 1) return {
1663
+ cleared: true,
1664
+ index: head,
1665
+ clearIndex
1666
+ };
1667
+ if (ifLastIndex !== void 0 && ifLastIndex !== head) return {
1668
+ cleared: false,
1669
+ index: head,
1670
+ clearIndex
1671
+ };
1672
+ if (head === 0 || clearIndex === head) return {
1673
+ cleared: true,
1674
+ index: head,
1675
+ clearIndex
1676
+ };
1677
+ const marker = head + 1;
1678
+ const now = clock.now().getTime();
1679
+ await insertEvent({
1680
+ query,
1681
+ row: {
1682
+ sessionId,
1683
+ index: marker,
1684
+ type: CLEAR_EVENT_TYPE,
1685
+ payload: null,
1686
+ id: generateId(),
1687
+ createdAt: now,
1688
+ cause: null,
1689
+ lane: null,
1690
+ processedAt: now
1691
+ },
1692
+ clearIndex
1693
+ });
1694
+ await query(`with released as (
1695
+ update a2_events
1696
+ set lane_ready = false, claim_holder = null, claim_expires_at = null
1697
+ where session_id = $1 and idx = $3
1698
+ )
1699
+ insert into a2_clears (session_id, clear_index) values ($1, $2)
1700
+ on conflict (session_id) do update set clear_index = excluded.clear_index`, [
1701
+ sessionId,
1702
+ marker,
1703
+ cause?.index ?? null
1704
+ ]);
1705
+ return {
1706
+ cleared: true,
1707
+ index: marker,
1708
+ clearIndex: marker
1709
+ };
1710
+ }));
1711
+ },
1712
+ async reclaim(sessionId) {
1713
+ return wrap(() => withTx(async (query) => {
1714
+ const { rows } = await query(`with boundary as materialized (
1715
+ select ${clearIndexOf("$1")} as clear_index
1716
+ ), batch as materialized (
1717
+ select idx from a2_events
1718
+ where session_id = $1
1719
+ and idx < (select clear_index from boundary)
1720
+ order by idx
1721
+ limit $2
1722
+ for update skip locked
1723
+ ), deleted as (
1724
+ delete from a2_events event using batch
1725
+ where event.session_id = $1 and event.idx = batch.idx
1726
+ returning event.idx
1727
+ ), pins as (
1728
+ delete from a2_snapshot_pins pin using deleted
1729
+ where pin.session_id = $1 and pin.event_index = deleted.idx
1730
+ )
1731
+ select (select count(*) from deleted) as deleted,
1732
+ (select clear_index from boundary) as clear_index`, [sessionId, RECLAIM_BATCH]);
1733
+ const deleted = asNumber(rows[0]?.["deleted"]);
1734
+ const clearIndex = asNumber(rows[0]?.["clear_index"]);
1735
+ if (clearIndex === 0) return {
1736
+ deleted: 0,
1737
+ done: true
1738
+ };
1739
+ if (deleted > 0) return {
1740
+ deleted,
1741
+ done: false
1742
+ };
1743
+ const { rows: remaining } = await query("select 1 from a2_events where session_id = $1 and idx < $2 limit 1", [sessionId, clearIndex]);
1744
+ if (remaining.length > 0) return {
1745
+ deleted: 0,
1746
+ done: false
1747
+ };
1748
+ const { rows: swept } = await query(`with pins as materialized (
1749
+ select ctid from a2_snapshot_pins
1750
+ where session_id = $1 and up_to_index < $2
1751
+ for update skip locked
1752
+ ), history as materialized (
1753
+ select ctid from a2_snapshot_history
1754
+ where session_id = $1 and up_to_index < $2
1755
+ for update skip locked
1756
+ ), heads as materialized (
1757
+ select ctid from a2_snapshots
1758
+ where session_id = $1 and up_to_index < $2
1759
+ for update skip locked
1760
+ ), gone_pins as (
1761
+ delete from a2_snapshot_pins target using pins where target.ctid = pins.ctid
1762
+ ), gone_history as (
1763
+ delete from a2_snapshot_history target using history
1764
+ where target.ctid = history.ctid
1765
+ ), gone_heads as (
1766
+ delete from a2_snapshots target using heads where target.ctid = heads.ctid
1767
+ )
1768
+ select (select count(*) from a2_snapshot_pins
1769
+ where session_id = $1 and up_to_index < $2)
1770
+ + (select count(*) from a2_snapshot_history
1771
+ where session_id = $1 and up_to_index < $2)
1772
+ + (select count(*) from a2_snapshots
1773
+ where session_id = $1 and up_to_index < $2)
1774
+ - (select count(*) from pins)
1775
+ - (select count(*) from history)
1776
+ - (select count(*) from heads) as skipped`, [sessionId, clearIndex]);
1777
+ return {
1778
+ deleted: 0,
1779
+ done: asNumber(swept[0]?.["skipped"]) === 0
1780
+ };
1781
+ }));
1782
+ },
1492
1783
  async close() {
1493
1784
  if (closed) return;
1494
1785
  closed = true;