experimental-a2 0.16.1 → 0.18.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 (406) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
  4. package/dist/actor-Bjce9_5U.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +2 -5
  6. package/dist/actor-client.d.ts.map +1 -1
  7. package/dist/actor-client.js +13 -4
  8. package/dist/actor-client.js.map +1 -1
  9. package/dist/actor-react.d.ts +5 -3
  10. package/dist/actor-react.d.ts.map +1 -1
  11. package/dist/actor-react.js +20 -10
  12. package/dist/actor-react.js.map +1 -1
  13. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  14. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  15. package/dist/actor.d.ts +1 -1
  16. package/dist/actor.js +42 -23
  17. package/dist/actor.js.map +1 -1
  18. package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
  19. package/dist/ai-BpOGm-zB.js.map +1 -0
  20. package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
  21. package/dist/ai-DrUaMyRt.d.ts.map +1 -0
  22. package/dist/ai-client.d.ts +27 -0
  23. package/dist/ai-client.d.ts.map +1 -0
  24. package/dist/ai-client.js +29 -0
  25. package/dist/ai-client.js.map +1 -0
  26. package/dist/ai-fx.browser.d.ts +1 -0
  27. package/dist/ai-fx.browser.js +6 -0
  28. package/dist/ai-fx.browser.js.map +1 -0
  29. package/dist/ai-fx.d.ts +10 -0
  30. package/dist/ai-fx.d.ts.map +1 -0
  31. package/dist/ai-fx.js +634 -0
  32. package/dist/ai-fx.js.map +1 -0
  33. package/dist/ai-harness-BuLh_EYi.js +273 -0
  34. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  35. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  36. package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
  37. package/dist/ai-pi.browser.d.ts +1 -0
  38. package/dist/ai-pi.browser.js +6 -0
  39. package/dist/ai-pi.browser.js.map +1 -0
  40. package/dist/ai-pi.d.ts +17 -0
  41. package/dist/ai-pi.d.ts.map +1 -0
  42. package/dist/ai-pi.js +904 -0
  43. package/dist/ai-pi.js.map +1 -0
  44. package/dist/ai-react.d.ts +96 -0
  45. package/dist/ai-react.d.ts.map +1 -0
  46. package/dist/ai-react.js +142 -0
  47. package/dist/ai-react.js.map +1 -0
  48. package/dist/ai-server.browser.js +2 -2
  49. package/dist/ai-server.browser.js.map +1 -1
  50. package/dist/ai-server.d.ts +109 -43
  51. package/dist/ai-server.d.ts.map +1 -1
  52. package/dist/ai-server.js +526 -283
  53. package/dist/ai-server.js.map +1 -1
  54. package/dist/ai.d.ts +2 -2
  55. package/dist/ai.js +2 -2
  56. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  57. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  58. package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
  59. package/dist/client-BReKqwZC.js.map +1 -0
  60. package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
  61. package/dist/client-h_DKFAjj.d.ts.map +1 -0
  62. package/dist/client.d.ts +4 -2
  63. package/dist/client.js +1 -1
  64. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  65. package/dist/contract-BYKASlCV.js.map +1 -0
  66. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  67. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  68. package/dist/index.d.ts +6 -5
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
  72. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
  73. package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
  74. package/dist/internal-B2T3U4Rq.js.map +1 -0
  75. package/dist/otel.d.ts +1 -1
  76. package/dist/react-BBXgHWFc.d.ts +86 -0
  77. package/dist/react-BBXgHWFc.d.ts.map +1 -0
  78. package/dist/react.d.ts +2 -122
  79. package/dist/react.js +107 -100
  80. package/dist/react.js.map +1 -1
  81. package/dist/reducer-BAXc8JyF.js +87 -0
  82. package/dist/reducer-BAXc8JyF.js.map +1 -0
  83. package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
  84. package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
  85. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  86. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  87. package/dist/scheduler-qstash.d.ts +3 -3
  88. package/dist/scheduler-qstash.d.ts.map +1 -1
  89. package/dist/scheduler-qstash.js +33 -10
  90. package/dist/scheduler-qstash.js.map +1 -1
  91. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  92. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  93. package/dist/scheduler-vercel.d.ts +4 -9
  94. package/dist/scheduler-vercel.d.ts.map +1 -1
  95. package/dist/scheduler-vercel.js +25 -44
  96. package/dist/scheduler-vercel.js.map +1 -1
  97. package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
  98. package/dist/server-Dw_k_7kO.js.map +1 -0
  99. package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
  100. package/dist/server-En7cbi3v.d.ts.map +1 -0
  101. package/dist/server.d.ts +4 -4
  102. package/dist/server.js +2 -2
  103. package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
  104. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  105. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  106. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  107. package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
  108. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  109. package/dist/store-N8PXxDAS.js.map +1 -1
  110. package/dist/store-memory.d.ts +1 -1
  111. package/dist/store-memory.d.ts.map +1 -1
  112. package/dist/store-memory.js +205 -66
  113. package/dist/store-memory.js.map +1 -1
  114. package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
  115. package/dist/store-polling-B5FcwJfo.js.map +1 -0
  116. package/dist/store-postgres.d.ts +1 -1
  117. package/dist/store-postgres.d.ts.map +1 -1
  118. package/dist/store-postgres.js +450 -108
  119. package/dist/store-postgres.js.map +1 -1
  120. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
  121. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  122. package/dist/store-redis-http.d.ts +1 -1
  123. package/dist/store-redis-http.d.ts.map +1 -1
  124. package/dist/store-redis-http.js +293 -11
  125. package/dist/store-redis-http.js.map +1 -1
  126. package/dist/store-redis-notify-BRITti_o.js +2493 -0
  127. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  128. package/dist/store-redis.d.ts +1 -1
  129. package/dist/store-redis.d.ts.map +1 -1
  130. package/dist/store-redis.js +11 -11
  131. package/dist/store-redis.js.map +1 -1
  132. package/dist/store-sqlite.d.ts +1 -1
  133. package/dist/store-sqlite.d.ts.map +1 -1
  134. package/dist/store-sqlite.js +197 -33
  135. package/dist/store-sqlite.js.map +1 -1
  136. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  137. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  138. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  139. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  140. package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
  141. package/dist/wire-rSUglYo8.js.map +1 -0
  142. package/docs/01-quickstart.mdx +50 -84
  143. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  144. package/docs/actors/01-introduction.mdx +48 -82
  145. package/docs/actors/02-concurrency.mdx +43 -70
  146. package/docs/actors/03-timers.mdx +28 -42
  147. package/docs/actors/04-routes.mdx +110 -137
  148. package/docs/actors/meta.ts +1 -1
  149. package/docs/agents/01-introduction.mdx +257 -0
  150. package/docs/agents/02-tools.mdx +247 -0
  151. package/docs/agents/03-conversations.mdx +538 -0
  152. package/docs/agents/04-harnesses.mdx +136 -0
  153. package/docs/agents/meta.ts +1 -0
  154. package/docs/client/01-react.mdx +294 -0
  155. package/docs/client/02-transports.mdx +86 -0
  156. package/docs/client/03-presence.mdx +224 -0
  157. package/docs/client/meta.ts +1 -0
  158. package/docs/contract/01-events.mdx +57 -0
  159. package/docs/contract/02-state.mdx +142 -0
  160. package/docs/contract/03-sessions.mdx +175 -0
  161. package/docs/index.mdx +112 -156
  162. package/docs/reference/01-core.mdx +139 -0
  163. package/docs/reference/02-server.mdx +855 -0
  164. package/docs/reference/03-client.mdx +301 -0
  165. package/docs/reference/04-ai.mdx +722 -0
  166. package/docs/reference/05-adapters.mdx +351 -0
  167. package/docs/reference/06-errors.mdx +138 -0
  168. package/docs/reference/meta.ts +1 -1
  169. package/docs/server/01-handlers.mdx +200 -0
  170. package/docs/server/02-timers.mdx +130 -0
  171. package/docs/server/03-cancellation.mdx +141 -0
  172. package/docs/server/04-routes.mdx +243 -0
  173. package/docs/server/05-production.mdx +322 -0
  174. package/docs/server/06-guarantees.mdx +123 -0
  175. package/examples/playground/DEPLOY.md +53 -7
  176. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  177. package/examples/playground/app/a0/[appId]/page.tsx +53 -0
  178. package/examples/playground/app/a0/a0.css +818 -0
  179. package/examples/playground/app/a0/app-list.tsx +218 -0
  180. package/examples/playground/app/a0/app-view.tsx +63 -0
  181. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  182. package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
  183. package/examples/playground/app/a0/apps.test.ts +128 -0
  184. package/examples/playground/app/a0/apps.ts +102 -0
  185. package/examples/playground/app/a0/chat.tsx +410 -0
  186. package/examples/playground/app/a0/columns.tsx +109 -0
  187. package/examples/playground/app/a0/context-menu.tsx +116 -0
  188. package/examples/playground/app/a0/cookies.ts +46 -0
  189. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  190. package/examples/playground/app/a0/crashes/route.ts +3 -0
  191. package/examples/playground/app/a0/crashes.test.ts +33 -0
  192. package/examples/playground/app/a0/crashes.ts +28 -0
  193. package/examples/playground/app/a0/devtools.tsx +164 -0
  194. package/examples/playground/app/a0/events/route.ts +6 -0
  195. package/examples/playground/app/a0/flags.ts +30 -0
  196. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  197. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  198. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  199. package/examples/playground/app/a0/layout.tsx +38 -0
  200. package/examples/playground/app/a0/list/route.ts +10 -0
  201. package/examples/playground/app/a0/model.test.ts +73 -0
  202. package/examples/playground/app/a0/model.ts +95 -0
  203. package/examples/playground/app/a0/no-photon.cjs +4 -0
  204. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  205. package/examples/playground/app/a0/page.tsx +14 -0
  206. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  207. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  208. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  209. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  210. package/examples/playground/app/a0/preview.test.ts +53 -0
  211. package/examples/playground/app/a0/preview.ts +26 -0
  212. package/examples/playground/app/a0/sandbox-log.ts +75 -0
  213. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  214. package/examples/playground/app/a0/sandbox.ts +191 -0
  215. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  216. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  217. package/examples/playground/app/a0/server.ts +213 -0
  218. package/examples/playground/app/a0/session.ts +59 -0
  219. package/examples/playground/app/a0/shell.tsx +158 -0
  220. package/examples/playground/app/a0/short/events/route.ts +9 -0
  221. package/examples/playground/app/a0/splitter.tsx +120 -0
  222. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  223. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  224. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  225. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  226. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  227. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  228. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  229. package/examples/playground/app/agent/events/route.ts +5 -2
  230. package/examples/playground/app/agent/ingress.test.ts +101 -0
  231. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  232. package/examples/playground/app/agent/model-input.tsx +115 -0
  233. package/examples/playground/app/agent/model.test.ts +73 -0
  234. package/examples/playground/app/agent/model.ts +58 -26
  235. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  236. package/examples/playground/app/agent/page.tsx +4 -4
  237. package/examples/playground/app/agent/server.ts +19 -15
  238. package/examples/playground/app/agent/session.ts +5 -6
  239. package/examples/playground/app/agent/telemetry.test.ts +189 -0
  240. package/examples/playground/app/agent/telemetry.ts +30 -0
  241. package/examples/playground/app/agent/trace.test.ts +23 -0
  242. package/examples/playground/app/agent/trace.ts +18 -0
  243. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
  244. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  245. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  246. package/examples/playground/app/canvas/model.ts +53 -53
  247. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  248. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  249. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  250. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  251. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  252. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  253. package/examples/playground/app/chat/agent/route.ts +9 -0
  254. package/examples/playground/app/chat/model.test.ts +46 -119
  255. package/examples/playground/app/chat/model.ts +82 -128
  256. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  257. package/examples/playground/app/chat/server.test.ts +112 -0
  258. package/examples/playground/app/chat/server.ts +113 -117
  259. package/examples/playground/app/components/activity-feed.tsx +30 -1
  260. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  261. package/examples/playground/app/components/demo-icons.tsx +89 -0
  262. package/examples/playground/app/components/demos.tsx +33 -0
  263. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  264. package/examples/playground/app/components/fast-link.tsx +55 -0
  265. package/examples/playground/app/components/site-header.tsx +96 -0
  266. package/examples/playground/app/counter/counter-client.tsx +3 -3
  267. package/examples/playground/app/counter/model.test.ts +3 -3
  268. package/examples/playground/app/counter/model.ts +11 -21
  269. package/examples/playground/app/counter/page.tsx +2 -15
  270. package/examples/playground/app/counter/session.ts +2 -2
  271. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  272. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  273. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  274. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  275. package/examples/playground/app/documents/model.ts +27 -27
  276. package/examples/playground/app/globals.css +420 -207
  277. package/examples/playground/app/initial-state.test.tsx +54 -0
  278. package/examples/playground/app/layout.tsx +7 -23
  279. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  280. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  281. package/examples/playground/app/orders/model.ts +49 -49
  282. package/examples/playground/app/page.tsx +13 -102
  283. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  284. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  285. package/examples/playground/app/recovery/model.ts +30 -30
  286. package/examples/playground/instrumentation.ts +5 -0
  287. package/examples/playground/next.config.ts +17 -5
  288. package/examples/playground/package.json +13 -2
  289. package/examples/playground/scripts/a0-template.ts +77 -0
  290. package/examples/playground/vercel.json +36 -0
  291. package/package.json +29 -3
  292. package/src/actor-client.ts +16 -4
  293. package/src/actor-react.ts +25 -11
  294. package/src/actor-shared.ts +21 -6
  295. package/src/actor.ts +76 -31
  296. package/src/ai-agent-actions.ts +219 -0
  297. package/src/ai-agent.ts +2517 -0
  298. package/src/ai-app-state.ts +91 -0
  299. package/src/ai-client-state.ts +6 -3
  300. package/src/ai-client.ts +62 -0
  301. package/src/ai-context-schema.ts +25 -5
  302. package/src/ai-context.ts +90 -40
  303. package/src/ai-control-server.ts +92 -72
  304. package/src/ai-coordinator.ts +23 -22
  305. package/src/ai-fx.browser.ts +5 -0
  306. package/src/ai-fx.ts +833 -0
  307. package/src/ai-harness.ts +518 -0
  308. package/src/ai-message-base.ts +209 -0
  309. package/src/ai-message-projection.ts +262 -64
  310. package/src/ai-model-metadata.ts +11 -6
  311. package/src/ai-pi.browser.ts +5 -0
  312. package/src/ai-pi.ts +1263 -0
  313. package/src/ai-progress-batches.ts +26 -0
  314. package/src/ai-projector.ts +95 -14
  315. package/src/ai-react.ts +146 -0
  316. package/src/ai-sdk-step.ts +2 -2
  317. package/src/ai-server.browser.ts +2 -2
  318. package/src/ai-server.ts +23 -1965
  319. package/src/ai-stored-state.ts +48 -163
  320. package/src/ai.ts +339 -155
  321. package/src/bootstrap-stream.ts +72 -0
  322. package/src/bounded-cache.ts +55 -0
  323. package/src/client-render-session.ts +49 -0
  324. package/src/client.ts +688 -151
  325. package/src/contract.ts +92 -42
  326. package/src/deterministic-id.ts +37 -0
  327. package/src/equal-json.ts +32 -0
  328. package/src/errors.ts +13 -1
  329. package/src/index.ts +5 -1
  330. package/src/initial-snapshot.ts +39 -0
  331. package/src/internal.ts +52 -0
  332. package/src/libfx.d.ts +34 -0
  333. package/src/postgres-notification-scope.ts +14 -3
  334. package/src/react.ts +171 -206
  335. package/src/redis-http-reply.ts +256 -0
  336. package/src/redis-http-stream.ts +73 -0
  337. package/src/redis-http-subscriptions.ts +13 -5
  338. package/src/reducer-runtime.ts +52 -0
  339. package/src/reducer.ts +93 -11
  340. package/src/scheduler-qstash.ts +49 -20
  341. package/src/scheduler-task.ts +34 -2
  342. package/src/scheduler-vercel.ts +32 -84
  343. package/src/server-fetch.ts +139 -35
  344. package/src/server.ts +1133 -255
  345. package/src/session-clear.ts +23 -0
  346. package/src/session-socket.ts +154 -49
  347. package/src/sse.ts +71 -4
  348. package/src/store-memory.ts +252 -75
  349. package/src/store-polling.ts +18 -4
  350. package/src/store-postgres.ts +541 -114
  351. package/src/store-redis-core.ts +1450 -493
  352. package/src/store-redis-http.ts +18 -2
  353. package/src/store-redis-notify.ts +262 -85
  354. package/src/store-redis.ts +19 -9
  355. package/src/store-sqlite.ts +319 -45
  356. package/src/store.ts +79 -1
  357. package/src/stream-buffer.ts +43 -0
  358. package/src/telemetry.ts +14 -1
  359. package/src/wire.ts +241 -13
  360. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  361. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  362. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  363. package/dist/ai-DgOBltJ_.js.map +0 -1
  364. package/dist/client-BYzHjkwU.js.map +0 -1
  365. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  366. package/dist/contract-CKRg_E4q.js.map +0 -1
  367. package/dist/errors-DCk6ch5n.js.map +0 -1
  368. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  369. package/dist/internal-Dq2qYxou.js.map +0 -1
  370. package/dist/react.d.ts.map +0 -1
  371. package/dist/reducer-DEMjEY_O.js +0 -29
  372. package/dist/reducer-DEMjEY_O.js.map +0 -1
  373. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  374. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  375. package/dist/server-BD5ckxZb.js.map +0 -1
  376. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  377. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  378. package/dist/store-polling-CmxUbV93.js.map +0 -1
  379. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  380. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  381. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  382. package/dist/validate-XKT4FSNn.js +0 -30
  383. package/dist/validate-XKT4FSNn.js.map +0 -1
  384. package/dist/wire-BO5wWCb1.js.map +0 -1
  385. package/docs/concepts/01-contracts.mdx +0 -142
  386. package/docs/concepts/02-handlers.mdx +0 -284
  387. package/docs/concepts/03-durability.mdx +0 -314
  388. package/docs/concepts/04-state.mdx +0 -209
  389. package/docs/guides/01-timers.mdx +0 -185
  390. package/docs/guides/02-cancellation.mdx +0 -161
  391. package/docs/guides/03-react.mdx +0 -384
  392. package/docs/guides/04-local-first.mdx +0 -88
  393. package/docs/guides/05-production.mdx +0 -521
  394. package/docs/guides/06-ai-agents.mdx +0 -1318
  395. package/docs/guides/08-application-data.mdx +0 -113
  396. package/docs/guides/09-presence.mdx +0 -266
  397. package/docs/guides/10-transports.mdx +0 -221
  398. package/docs/reference/01-api.mdx +0 -1896
  399. package/docs/reference/02-errors.mdx +0 -149
  400. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  401. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  402. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  403. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  404. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
  405. /package/docs/{concepts → contract}/meta.ts +0 -0
  406. /package/docs/{guides → server}/meta.ts +0 -0
@@ -1,1583 +0,0 @@
1
- import { t as A2Error } from "./errors-DCk6ch5n.js";
2
- import { h as nullProtoRecord, r as NOTIFY_TIMINGS } from "./internal-Dq2qYxou.js";
3
- import { i as streamActivities, n as pollingStream, r as createStreamActivity, t as defaultSleep } from "./store-polling-CmxUbV93.js";
4
- import { t as idempotentReplay } from "./idempotent-replay-DVOlyYbx.js";
5
- import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
6
- import { t as pollPresence } from "./store-presence-polling-C7-XZyW9.js";
7
- //#region src/presence-recovery.ts
8
- const key = ({ participant, field }) => JSON.stringify([participant, field]);
9
- function presenceRecovery({ read, deliver, isActive }) {
10
- const fields = /* @__PURE__ */ new Map();
11
- const reading = /* @__PURE__ */ new Set();
12
- let generation = 0;
13
- let latest;
14
- const remember = ({ participant, field, seen, at }) => {
15
- const id = key({
16
- participant,
17
- field
18
- });
19
- const previous = fields.get(id);
20
- if (!previous || previous.at.getTime() < at.getTime() || previous.at.getTime() === at.getTime() && previous.seen !== seen) fields.set(id, {
21
- participant,
22
- field,
23
- seen,
24
- at
25
- });
26
- };
27
- const snapshot = async ({ reconcile }) => {
28
- const run = generation;
29
- const before = new Map(fields);
30
- const changed = /* @__PURE__ */ new Set();
31
- reading.add(changed);
32
- try {
33
- const rows = await read();
34
- if (!isActive()) return rows;
35
- if (run !== generation) return latest;
36
- const live = /* @__PURE__ */ new Set();
37
- for (const row of rows) {
38
- const id = key(row);
39
- live.add(id);
40
- if (changed.has(id)) continue;
41
- remember(row);
42
- if (reconcile) deliver({
43
- participant: row.participant,
44
- values: { [row.field]: row.value },
45
- seen: row.seen,
46
- at: row.at
47
- });
48
- }
49
- if (reconcile) for (const [id, prior] of before) {
50
- if (live.has(id) || changed.has(id) || fields.get(id) !== prior) continue;
51
- fields.delete(id);
52
- deliver({
53
- participant: prior.participant,
54
- values: { [prior.field]: null },
55
- seen: prior.seen,
56
- at: prior.at
57
- });
58
- }
59
- return rows;
60
- } finally {
61
- reading.delete(changed);
62
- if (reading.size === 0) latest = void 0;
63
- }
64
- };
65
- return {
66
- async read() {
67
- let pending = latest;
68
- while (pending) {
69
- await pending;
70
- if (latest === pending) break;
71
- pending = latest;
72
- }
73
- return snapshot({ reconcile: false });
74
- },
75
- async reconnect() {
76
- generation += 1;
77
- latest = snapshot({ reconcile: true });
78
- await latest;
79
- },
80
- update(patch) {
81
- for (const [field, value] of Object.entries(patch.values)) {
82
- if (value === void 0) continue;
83
- const id = key({
84
- participant: patch.participant,
85
- field
86
- });
87
- const previous = fields.get(id);
88
- if (previous && previous.at.getTime() > patch.at.getTime()) continue;
89
- for (const changed of reading) changed.add(id);
90
- if (value === null) fields.delete(id);
91
- else remember({
92
- participant: patch.participant,
93
- field,
94
- seen: patch.seen,
95
- at: patch.at
96
- });
97
- }
98
- }
99
- };
100
- }
101
- //#endregion
102
- //#region src/store-redis-core.ts
103
- const READ_PAGE_SIZE = 256;
104
- const READ_CONCURRENCY = 16;
105
- /**
106
- * KEYS: store stream, counter, global id hash, metadata, pending, ready,
107
- * watched.
108
- * ARGV: sessionId, nowMs, n, then per event:
109
- * id, type, payloadJson, causeJson, lane, settled; finally notify channel.
110
- * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |
111
- * ['dup', hasPending, ...indexes] | ['partial', count] |
112
- * ['foreign', eventId] | ['no_parent', idx] | ['superseded', idx].
113
- */
114
- const APPEND_LUA = `
115
- local function lane_field(lane, suffix)
116
- return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
117
- end
118
- local function enqueue(field, idx, lane)
119
- redis.call('ZADD', KEYS[5], idx, field)
120
- if lane == '' then
121
- redis.call('ZADD', KEYS[6], idx, field)
122
- return
123
- end
124
- local tail_key = lane_field(lane, 'tail')
125
- local tail = redis.call('HGET', KEYS[4], tail_key)
126
- if tail then
127
- redis.call('HSET', KEYS[4], tail .. ':next', field)
128
- else
129
- redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
130
- redis.call('ZADD', KEYS[6], idx, field)
131
- end
132
- redis.call('HSET', KEYS[4], tail_key, field)
133
- end
134
- local n = tonumber(ARGV[3])
135
- local base = tonumber(redis.call('GET', KEYS[2]) or '0')
136
- local has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
137
- if n == 0 then return { 'empty', has_pending } end
138
- local dups = {}
139
- for i = 1, n do
140
- local id = ARGV[3 + (i - 1) * 6 + 1]
141
- local existing = redis.call('HGET', KEYS[3], id)
142
- if existing then
143
- local rec = cjson.decode(existing)
144
- if rec[1] ~= ARGV[1] then return { 'foreign', id } end
145
- dups[#dups + 1] = tostring(rec[2])
146
- end
147
- end
148
- if #dups == n then
149
- local res = { 'dup', has_pending }
150
- for i = 1, #dups do res[#res + 1] = dups[i] end
151
- return res
152
- end
153
- if #dups > 0 then return { 'partial', tostring(#dups) } end
154
- for i = 1, n do
155
- local cause = ARGV[3 + (i - 1) * 6 + 4]
156
- if cause ~= '' then
157
- local parsed = cjson.decode(cause)
158
- local pidx = tonumber(parsed.index)
159
- if pidx < 1 or pidx > base then
160
- return { 'no_parent', tostring(parsed.index) }
161
- end
162
- local field = tostring(pidx)
163
- local dispatches = tonumber(redis.call('HGET', KEYS[4], field .. ':d') or '0')
164
- if dispatches ~= tonumber(parsed.attempt) or
165
- redis.call('HEXISTS', KEYS[4], field .. ':f') == 1 then
166
- return { 'superseded', field }
167
- end
168
- end
169
- end
170
- for i = 1, n do
171
- local id = ARGV[3 + (i - 1) * 6 + 1]
172
- local typ = ARGV[3 + (i - 1) * 6 + 2]
173
- local payload = ARGV[3 + (i - 1) * 6 + 3]
174
- local cause = ARGV[3 + (i - 1) * 6 + 4]
175
- local lane = ARGV[3 + (i - 1) * 6 + 5]
176
- local settled = ARGV[3 + (i - 1) * 6 + 6]
177
- local idx = base + i
178
- redis.call('XADD', KEYS[1], tostring(idx) .. '-0',
179
- 'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
180
- 'cause', cause, 'lane', lane)
181
- redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
182
- redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
183
- if settled == '1' then
184
- redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
185
- else
186
- enqueue(tostring(idx), idx, lane)
187
- end
188
- end
189
- redis.call('SET', KEYS[2], tostring(base + n))
190
- has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
191
- if redis.call('EXISTS', KEYS[7]) == 1 then
192
- redis.pcall('PUBLISH', ARGV[4 + n * 6], tostring(base + n))
193
- end
194
- return { 'ok', tostring(base + 1), has_pending }
195
- `;
196
- /**
197
- * KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts.
198
- * ARGV: idx, attempt, error, maxFailures, nowMs.
199
- */
200
- const FAIL_ATTEMPT_LUA = `
201
- local function release_snapshot_pins()
202
- local refs = redis.call('SMEMBERS', KEYS[5])
203
- for _, ref in ipairs(refs) do
204
- local count = redis.call('HINCRBY', KEYS[6], ref, -1)
205
- if count <= 0 then
206
- redis.call('HDEL', KEYS[6], ref)
207
- local checkpoint = cjson.decode(ref)
208
- redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
209
- end
210
- end
211
- redis.call('DEL', KEYS[5])
212
- if redis.call('HLEN', KEYS[6]) == 0 then redis.call('DEL', KEYS[6]) end
213
- end
214
- local function lane_field(lane, suffix)
215
- return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
216
- end
217
- local function settle(field)
218
- redis.call('ZREM', KEYS[3], field)
219
- redis.call('ZREM', KEYS[4], field)
220
- local lane = redis.call('HGET', KEYS[2], field .. ':l') or ''
221
- if lane == '' then return end
222
- local head_key = lane_field(lane, 'head')
223
- if redis.call('HGET', KEYS[2], head_key) ~= field then return end
224
- local next_field = redis.call('HGET', KEYS[2], field .. ':next')
225
- while next_field and redis.call('HEXISTS', KEYS[2], next_field .. ':p') == 1 do
226
- next_field = redis.call('HGET', KEYS[2], next_field .. ':next')
227
- end
228
- if next_field then
229
- redis.call('HSET', KEYS[2], head_key, next_field)
230
- if redis.call('HEXISTS', KEYS[2], next_field .. ':f') == 0 then
231
- redis.call('ZADD', KEYS[4], tonumber(next_field), next_field)
232
- end
233
- else
234
- redis.call('HDEL', KEYS[2], head_key, lane_field(lane, 'tail'))
235
- end
236
- end
237
- local idx = tonumber(ARGV[1])
238
- if idx < 1 or idx > tonumber(redis.call('GET', KEYS[1]) or '0') then
239
- return { 'missing', '0' }
240
- end
241
- local field = ARGV[1]
242
- local failures = tonumber(redis.call('HGET', KEYS[2], field .. ':a') or '0')
243
- local dispatches = tonumber(redis.call('HGET', KEYS[2], field .. ':d') or '0')
244
- if redis.call('HEXISTS', KEYS[2], field .. ':p') == 1 or dispatches ~= tonumber(ARGV[2]) then
245
- return { 'superseded', tostring(failures) }
246
- end
247
- if redis.call('HEXISTS', KEYS[2], field .. ':f') == 1 then
248
- return { 'dead_lettered', tostring(failures) }
249
- end
250
- if redis.call('HEXISTS', KEYS[2], field .. ':lfa') == 1 and
251
- tonumber(redis.call('HGET', KEYS[2], field .. ':lfa')) == tonumber(ARGV[2]) and
252
- redis.call('HEXISTS', KEYS[2], field .. ':ch') == 0 then
253
- return { 'failed', tostring(failures) }
254
- end
255
- if redis.call('HEXISTS', KEYS[2], field .. ':ch') == 0 then
256
- return { 'superseded', tostring(failures) }
257
- end
258
- failures = redis.call('HINCRBY', KEYS[2], field .. ':a', 1)
259
- redis.call('HSET', KEYS[2],
260
- field .. ':e', ARGV[3],
261
- field .. ':lf', ARGV[5],
262
- field .. ':lfa', ARGV[2])
263
- redis.call('HDEL', KEYS[2], field .. ':ch', field .. ':ce')
264
- if failures >= tonumber(ARGV[4]) then
265
- redis.call('HSET', KEYS[2], field .. ':f', ARGV[5])
266
- redis.call('ZREM', KEYS[4], field)
267
- release_snapshot_pins()
268
- return { 'dead_lettered', tostring(failures) }
269
- end
270
- return { 'failed', tostring(failures) }
271
- `;
272
- /**
273
- * KEYS: store, counter, meta, pending, ready.
274
- * ARGV: holder, nowMs, expiresAtMs, excludedCount, excluded indexes.
275
- */
276
- const CLAIM_AVAILABLE_LUA = `
277
- if redis.call('ZCARD', KEYS[4]) == 0 then return { 'settled' } end
278
- local ready = redis.call('ZRANGE', KEYS[5], 0, -1)
279
- local excluded = {}
280
- for i = 1, tonumber(ARGV[4]) do excluded[ARGV[4 + i]] = true end
281
- local claimed = {}
282
- local retry_at = nil
283
- for _, field in ipairs(ready) do
284
- if not excluded[field] then
285
- local expires = tonumber(redis.call('HGET', KEYS[3], field .. ':ce') or '0')
286
- if expires > tonumber(ARGV[2]) then
287
- if not retry_at or expires < retry_at then retry_at = expires end
288
- else
289
- local entries = redis.call('XRANGE', KEYS[1], field .. '-0', field .. '-0')
290
- if #entries == 0 then return { 'missing', field } end
291
- local attempt = redis.call('HINCRBY', KEYS[3], field .. ':d', 1)
292
- redis.call('HSETNX', KEYS[3], field .. ':fc', ARGV[2])
293
- redis.call('HSET', KEYS[3],
294
- field .. ':lc', ARGV[2],
295
- field .. ':ch', ARGV[1],
296
- field .. ':ce', ARGV[3])
297
- claimed[#claimed + 1] = {
298
- tostring(attempt),
299
- redis.call('HGET', KEYS[3], field .. ':a') or '0',
300
- redis.call('HGET', KEYS[3], field .. ':e') or false,
301
- redis.call('HGET', KEYS[3], field .. ':fc') or false,
302
- ARGV[2],
303
- redis.call('HGET', KEYS[3], field .. ':lf') or false,
304
- redis.call('HGET', KEYS[3], field .. ':lfa') or false,
305
- ARGV[1],
306
- ARGV[3],
307
- entries[1]
308
- }
309
- end
310
- end
311
- end
312
- if #claimed > 0 then
313
- local result = { 'claimed', tostring(#claimed) }
314
- for _, claim in ipairs(claimed) do
315
- for _, value in ipairs(claim) do result[#result + 1] = value end
316
- end
317
- return result
318
- end
319
- if retry_at then return { 'busy', tostring(retry_at) } end
320
- return { 'settled' }
321
- `;
322
- /**
323
- * KEYS: counter, meta.
324
- * ARGV: holder, nowMs, expiresAtMs, n, then per claim: index, attempt.
325
- * Returns [renewedCount, ...renewed, ...superseded].
326
- */
327
- const RENEW_CLAIMS_LUA = `
328
- local total = tonumber(redis.call('GET', KEYS[1]) or '0')
329
- local renewed = {}
330
- local superseded = {}
331
- for i = 1, tonumber(ARGV[4]) do
332
- local field = ARGV[4 + (i - 1) * 2 + 1]
333
- local attempt = tonumber(ARGV[4 + (i - 1) * 2 + 2])
334
- local idx = tonumber(field)
335
- if idx >= 1 and idx <= total then
336
- local dispatches = tonumber(redis.call('HGET', KEYS[2], field .. ':d') or '0')
337
- if dispatches > attempt then
338
- superseded[#superseded + 1] = field
339
- else
340
- local expires = tonumber(redis.call('HGET', KEYS[2], field .. ':ce') or '0')
341
- if tonumber(ARGV[3]) > tonumber(ARGV[2]) and
342
- redis.call('HEXISTS', KEYS[2], field .. ':p') == 0 and
343
- redis.call('HEXISTS', KEYS[2], field .. ':f') == 0 and
344
- redis.call('HGET', KEYS[2], field .. ':ch') == ARGV[1] and
345
- expires > tonumber(ARGV[2]) then
346
- redis.call('HSET', KEYS[2], field .. ':ce', ARGV[3])
347
- renewed[#renewed + 1] = field
348
- end
349
- end
350
- end
351
- end
352
- local result = { tostring(#renewed) }
353
- for i = 1, #renewed do result[#result + 1] = renewed[i] end
354
- for i = 1, #superseded do result[#result + 1] = superseded[i] end
355
- return result
356
- `;
357
- /**
358
- * KEYS: store, counter, ids, meta, pending, ready, watched,
359
- * event snapshot pins, snapshot pin counts.
360
- * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:
361
- * id, type, payloadJson, lane, settled; finally notify channel.
362
- */
363
- const COMPLETE_ATTEMPT_LUA = `
364
- local function release_snapshot_pins()
365
- local refs = redis.call('SMEMBERS', KEYS[8])
366
- for _, ref in ipairs(refs) do
367
- local count = redis.call('HINCRBY', KEYS[9], ref, -1)
368
- if count <= 0 then
369
- redis.call('HDEL', KEYS[9], ref)
370
- local checkpoint = cjson.decode(ref)
371
- redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
372
- end
373
- end
374
- redis.call('DEL', KEYS[8])
375
- if redis.call('HLEN', KEYS[9]) == 0 then redis.call('DEL', KEYS[9]) end
376
- end
377
- local function lane_field(lane, suffix)
378
- return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
379
- end
380
- local function settle(field)
381
- redis.call('ZREM', KEYS[5], field)
382
- redis.call('ZREM', KEYS[6], field)
383
- local lane = redis.call('HGET', KEYS[4], field .. ':l') or ''
384
- if lane == '' then return end
385
- local head_key = lane_field(lane, 'head')
386
- if redis.call('HGET', KEYS[4], head_key) ~= field then return end
387
- local next_field = redis.call('HGET', KEYS[4], field .. ':next')
388
- while next_field and redis.call('HEXISTS', KEYS[4], next_field .. ':p') == 1 do
389
- next_field = redis.call('HGET', KEYS[4], next_field .. ':next')
390
- end
391
- if next_field then
392
- redis.call('HSET', KEYS[4], head_key, next_field)
393
- if redis.call('HEXISTS', KEYS[4], next_field .. ':f') == 0 then
394
- redis.call('ZADD', KEYS[6], tonumber(next_field), next_field)
395
- end
396
- else
397
- redis.call('HDEL', KEYS[4], head_key, lane_field(lane, 'tail'))
398
- end
399
- end
400
- local function enqueue(field, idx, lane)
401
- redis.call('ZADD', KEYS[5], idx, field)
402
- if lane == '' then
403
- redis.call('ZADD', KEYS[6], idx, field)
404
- return
405
- end
406
- local tail_key = lane_field(lane, 'tail')
407
- local tail = redis.call('HGET', KEYS[4], tail_key)
408
- if tail then
409
- redis.call('HSET', KEYS[4], tail .. ':next', field)
410
- else
411
- redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
412
- redis.call('ZADD', KEYS[6], idx, field)
413
- end
414
- redis.call('HSET', KEYS[4], tail_key, field)
415
- end
416
- local parent = ARGV[3]
417
- local attempt = tonumber(ARGV[4])
418
- local n = tonumber(ARGV[5])
419
- local total = tonumber(redis.call('GET', KEYS[2]) or '0')
420
- if tonumber(parent) < 1 or tonumber(parent) > total then return { 'missing' } end
421
- local dispatches = tonumber(redis.call('HGET', KEYS[4], parent .. ':d') or '0')
422
- local processed = redis.call('HGET', KEYS[4], parent .. ':p')
423
- local processed_attempt = tonumber(redis.call('HGET', KEYS[4], parent .. ':pa') or '0')
424
- if processed then
425
- if processed_attempt ~= attempt then return { 'superseded' } end
426
- local raw_recorded_ids = redis.call('HGET', KEYS[4], parent .. ':ri')
427
- if not raw_recorded_ids then return { 'partial' } end
428
- local recorded_ids = cjson.decode(raw_recorded_ids)
429
- if #recorded_ids ~= n then return { 'partial' } end
430
- local indexes = {}
431
- for i = 1, n do
432
- local id = ARGV[5 + (i - 1) * 5 + 1]
433
- if recorded_ids[i] ~= id then return { 'partial' } end
434
- local existing = redis.call('HGET', KEYS[3], id)
435
- if not existing then return { 'partial' } end
436
- local rec = cjson.decode(existing)
437
- if rec[1] ~= ARGV[1] then return { 'partial' } end
438
- local entries = redis.call('XRANGE', KEYS[1], tostring(rec[2]) .. '-0', tostring(rec[2]) .. '-0')
439
- if #entries == 0 then return { 'partial' } end
440
- local values = entries[1][2]
441
- local cause = nil
442
- for j = 1, #values, 2 do
443
- if values[j] == 'cause' then cause = values[j + 1] break end
444
- end
445
- if not cause or cause == '' then return { 'partial' } end
446
- local decoded = cjson.decode(cause)
447
- if tonumber(decoded['index']) ~= tonumber(parent) or
448
- tonumber(decoded['attempt']) ~= attempt then return { 'partial' } end
449
- indexes[#indexes + 1] = tostring(rec[2])
450
- end
451
- local result = { 'duplicate' }
452
- for _, idx in ipairs(indexes) do result[#result + 1] = idx end
453
- return result
454
- end
455
- if dispatches ~= attempt then return { 'superseded' } end
456
- if redis.call('HEXISTS', KEYS[4], parent .. ':ch') == 0 or
457
- redis.call('HEXISTS', KEYS[4], parent .. ':f') == 1 or
458
- tonumber(redis.call('HGET', KEYS[4], parent .. ':lfa') or '0') == attempt then
459
- return { 'superseded' }
460
- end
461
- for i = 1, n do
462
- local id = ARGV[5 + (i - 1) * 5 + 1]
463
- if redis.call('HEXISTS', KEYS[3], id) == 1 then return { 'partial' } end
464
- end
465
- local returned_ids = {}
466
- for i = 1, n do returned_ids[i] = ARGV[5 + (i - 1) * 5 + 1] end
467
- local returned_json = '[]'
468
- if n > 0 then returned_json = cjson.encode(returned_ids) end
469
- redis.call('HSET', KEYS[4],
470
- parent .. ':p', ARGV[2],
471
- parent .. ':pa', ARGV[4],
472
- parent .. ':ri', returned_json)
473
- redis.call('HDEL', KEYS[4], parent .. ':ch', parent .. ':ce')
474
- settle(parent)
475
- release_snapshot_pins()
476
- local cause = cjson.encode({ index = tonumber(parent), attempt = attempt })
477
- for i = 1, n do
478
- local id = ARGV[5 + (i - 1) * 5 + 1]
479
- local typ = ARGV[5 + (i - 1) * 5 + 2]
480
- local payload = ARGV[5 + (i - 1) * 5 + 3]
481
- local lane = ARGV[5 + (i - 1) * 5 + 4]
482
- local settled = ARGV[5 + (i - 1) * 5 + 5]
483
- local idx = total + i
484
- redis.call('XADD', KEYS[1], tostring(idx) .. '-0',
485
- 'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
486
- 'cause', cause, 'lane', lane)
487
- redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
488
- redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
489
- if settled == '1' then
490
- redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
491
- else
492
- enqueue(tostring(idx), idx, lane)
493
- end
494
- end
495
- redis.call('SET', KEYS[2], tostring(total + n))
496
- if n > 0 and redis.call('EXISTS', KEYS[7]) == 1 then
497
- redis.pcall('PUBLISH', ARGV[6 + n * 5], tostring(total + n))
498
- end
499
- return { 'ok', tostring(total + 1) }
500
- `;
501
- /**
502
- * KEYS: head, history, event counter, event meta, pin counts.
503
- * ARGV: nowMs, event pin key prefix, write count, then per write:
504
- * index, snapshot JSON, pin count, pin event indexes.
505
- */
506
- const PUT_SNAPSHOTS_LUA = `
507
- local total = tonumber(redis.call('GET', KEYS[3]) or '0')
508
- local writes = {}
509
- local offset = 4
510
- for i = 1, tonumber(ARGV[3]) do
511
- local index = tonumber(ARGV[offset])
512
- local snapshot = ARGV[offset + 1]
513
- local pin_count = tonumber(ARGV[offset + 2])
514
- local ref = cjson.encode({ KEYS[2], tostring(index) })
515
- for p = 1, pin_count do
516
- local event_index = tonumber(ARGV[offset + 2 + p])
517
- if event_index >= 1 and event_index <= total and
518
- redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':p') == 0 and
519
- redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':f') == 0 then
520
- local pin_key = ARGV[2] .. tostring(event_index)
521
- if redis.call('SADD', pin_key, ref) == 1 then
522
- redis.call('HINCRBY', KEYS[5], ref, 1)
523
- end
524
- end
525
- end
526
- writes[#writes + 1] = { index = index, snapshot = snapshot, ref = ref }
527
- offset = offset + 3 + pin_count
528
- end
529
-
530
- local head = redis.call('GET', KEYS[1])
531
- local head_index = nil
532
- if head then head_index = tonumber(cjson.decode(head)['index']) end
533
- local latest = writes[#writes]
534
- local writes_head = not head_index or latest.index >= head_index
535
- local advances = not head_index or latest.index > head_index
536
- local final_head_index = advances and latest.index or head_index
537
-
538
- if advances and head and tonumber(redis.call('HGET', KEYS[5], cjson.encode({ KEYS[2], tostring(head_index) })) or '0') > 0 then
539
- redis.call('ZREMRANGEBYSCORE', KEYS[2], head_index, head_index)
540
- redis.call('ZADD', KEYS[2], head_index, head)
541
- end
542
-
543
- for _, write in ipairs(writes) do
544
- if write.index < final_head_index then
545
- redis.call('ZREMRANGEBYSCORE', KEYS[2], write.index, write.index)
546
- if tonumber(redis.call('HGET', KEYS[5], write.ref) or '0') > 0 then
547
- redis.call('ZADD', KEYS[2], write.index, write.snapshot)
548
- end
549
- end
550
- end
551
-
552
- if writes_head then
553
- redis.call('SET', KEYS[1], latest.snapshot)
554
- redis.call('ZREMRANGEBYSCORE', KEYS[2], latest.index, latest.index)
555
- end
556
- return 1
557
- `;
558
- /**
559
- * KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:
560
- * hashField, valueJson ('' deletes), patchFieldJson; finally channel and
561
- * message prefix. Wire fragments stay JSON text to preserve arrays
562
- * and numeric precision through Lua. Field-wise LWW by the sender's
563
- * `at`: a stored entry with a strictly newer stamp survives; ties go
564
- * to the incoming write (memory-tier parity). Returns the applied
565
- * 1-based entry positions. The whole-key PEXPIRE is the coarse bound,
566
- * sound because core sends one uniform ttlMs; the per-entry `e` stamp
567
- * is the fine bound (the injected storage clock at the applied set
568
- * plus ttlMs — never the sender stamp), pruned on read against the
569
- * same clock.
570
- */
571
- const PRESENCE_SET_LUA = `
572
- local at = tonumber(ARGV[1])
573
- local applied = {}
574
- local published = {}
575
- local n = tonumber(ARGV[3])
576
- for i = 1, n do
577
- local field = ARGV[3 + (i - 1) * 3 + 1]
578
- local value = ARGV[3 + (i - 1) * 3 + 2]
579
- local existing = redis.call('HGET', KEYS[1], field)
580
- if not existing or tonumber(cjson.decode(existing)['a']) <= at then
581
- if value == '' then
582
- redis.call('HDEL', KEYS[1], field)
583
- else
584
- redis.call('HSET', KEYS[1], field, value)
585
- end
586
- applied[#applied + 1] = tostring(i)
587
- published[#published + 1] = ARGV[3 + (i - 1) * 3 + 3]
588
- end
589
- end
590
- if #applied > 0 then
591
- redis.call('PEXPIRE', KEYS[1], ARGV[2])
592
- redis.pcall('PUBLISH', ARGV[4 + n * 3], ARGV[5 + n * 3] .. table.concat(published, ',') .. '}}')
593
- end
594
- return applied
595
- `;
596
- /**
597
- * KEYS: presence hash. ARGV: nowMs (the injected clock, so tests can
598
- * time-travel expiry against a real server). Returns the live
599
- * [field, value, ...] pairs; entries at or past their fine-bound
600
- * expiry are HDELed in the same atomic step, so a stale reader can
601
- * never sweep a concurrently refreshed field.
602
- */
603
- const PRESENCE_READ_LUA = `
604
- local now = tonumber(ARGV[1])
605
- local rows = redis.call('HGETALL', KEYS[1])
606
- local live = {}
607
- for i = 1, #rows, 2 do
608
- if tonumber(cjson.decode(rows[i + 1])['e']) <= now then
609
- redis.call('HDEL', KEYS[1], rows[i])
610
- else
611
- live[#live + 1] = rows[i]
612
- live[#live + 1] = rows[i + 1]
613
- end
614
- end
615
- return live
616
- `;
617
- /**
618
- * KEYS: head, history, event counter. ARGV: snapshot frontier or ''.
619
- * Returns one atomic multiversion-cache-plus-log-frontier read.
620
- */
621
- const READ_STATE_LUA = `
622
- local head = redis.call('GET', KEYS[1])
623
- local head_index = head and cjson.decode(head)['index'] or nil
624
- if head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then
625
- return redis.error_reply('invalid snapshot head index')
626
- end
627
- local cap = ARGV[1]
628
- local snapshot = nil
629
- if head_index and (cap == '' or head_index <= tonumber(cap)) then
630
- snapshot = head
631
- else
632
- local upper = cap == '' and '+inf' or cap
633
- local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
634
- if #historical > 0 then snapshot = historical[1] end
635
- end
636
- local frontier = redis.call('GET', KEYS[3]) or '0'
637
- return { head_index or '', snapshot or '', frontier }
638
- `;
639
- const fieldMap = (fields) => {
640
- const map = {};
641
- for (let i = 0; i + 1 < fields.length; i += 2) map[fields[i]] = fields[i + 1];
642
- return map;
643
- };
644
- const STORED_EVENT_META_SUFFIXES = [
645
- "p",
646
- "pa",
647
- "ri",
648
- "ch",
649
- "ce",
650
- "f",
651
- "fc",
652
- "lc",
653
- "lf",
654
- "lfa",
655
- "d",
656
- "a",
657
- "e"
658
- ];
659
- const entryIndex = (entryId) => Number(entryId.slice(0, entryId.indexOf("-")));
660
- const toEvent = (sessionId, entry) => {
661
- const fields = fieldMap(entry[1]);
662
- return {
663
- id: fields["id"],
664
- type: fields["type"],
665
- payload: JSON.parse(fields["payload"]),
666
- index: entryIndex(entry[0]),
667
- sessionId,
668
- createdAt: new Date(Number(fields["created_at"]))
669
- };
670
- };
671
- const json = (value) => JSON.stringify(value) ?? "null";
672
- const toCause = (raw) => {
673
- if (raw === "" || raw === void 0) return null;
674
- const value = JSON.parse(raw);
675
- if (!Number.isInteger(value.index) || Number(value.index) < 1 || !Number.isInteger(value.attempt) || Number(value.attempt) < 1) throw new TypeError("stored event has an invalid cause");
676
- if (value.batchSize !== void 0 && (!Number.isInteger(value.batchSize) || Number(value.batchSize) < 1)) throw new TypeError("stored event has an invalid cause");
677
- return {
678
- index: Number(value.index),
679
- attempt: Number(value.attempt),
680
- ...value.batchSize === void 0 ? {} : { batchSize: Number(value.batchSize) }
681
- };
682
- };
683
- const toClaimAvailableResult = (sessionId, reply) => {
684
- const outcome = String(reply[0]);
685
- if (outcome === "claimed") {
686
- const count = Number(reply[1]);
687
- const events = [];
688
- for (let i = 0; i < count; i++) {
689
- const offset = 2 + i * 10;
690
- const entry = reply[offset + 9];
691
- const event = toEvent(sessionId, entry);
692
- const fields = fieldMap(entry[1]);
693
- events.push(Object.assign(event, {
694
- cause: toCause(fields["cause"]),
695
- lane: fields["lane"] === "" ? null : fields["lane"],
696
- processedAt: null,
697
- processedByAttempt: null,
698
- returnedEventIds: null,
699
- firstClaimedAt: reply[offset + 3] === null ? null : new Date(Number(reply[offset + 3])),
700
- lastClaimedAt: new Date(Number(reply[offset + 4])),
701
- attemptCount: Number(reply[offset]),
702
- claimHolder: String(reply[offset + 7]),
703
- claimExpiresAt: new Date(Number(reply[offset + 8])),
704
- failureCount: Number(reply[offset + 1]),
705
- lastFailedAt: reply[offset + 5] === null ? null : new Date(Number(reply[offset + 5])),
706
- lastFailedAttempt: reply[offset + 6] === null ? null : Number(reply[offset + 6]),
707
- lastError: reply[offset + 2] === null ? null : String(reply[offset + 2]),
708
- failedAt: null
709
- }));
710
- }
711
- return {
712
- outcome: "claimed",
713
- events
714
- };
715
- }
716
- if (outcome === "busy") return {
717
- outcome,
718
- retryAt: new Date(Number(reply[1]))
719
- };
720
- if (outcome === "settled") return { outcome };
721
- throw new TypeError(`claim referenced a missing event in session '${sessionId}'`);
722
- };
723
- const wrap = async (fn) => {
724
- try {
725
- return await fn();
726
- } catch (err) {
727
- if (err instanceof A2Error || err instanceof TypeError) throw err;
728
- throw new A2Error("STORE_UNAVAILABLE", "redis store operation failed", { cause: err });
729
- }
730
- };
731
- function createRedisStoreCore(options) {
732
- const { call, clock } = options;
733
- const generateId = options.ids;
734
- const prefix = options.keyPrefix;
735
- const storeKey = (s) => `${prefix}:${s}:log`;
736
- const countKey = (s) => `${prefix}:${s}:count`;
737
- const metaKey = (s) => `${prefix}:${s}:meta`;
738
- const pendingKey = (s) => `${prefix}:${s}:pending`;
739
- const readyKey = (s) => `${prefix}:${s}:ready`;
740
- const snapKey = (s, r) => `${prefix}:${s}:snap:${r}`;
741
- const snapHistoryKey = (s, r) => `${prefix}:${s}:snap-history:${r}`;
742
- const snapshotEventPinsKey = (s, index) => `${prefix}:${s}:snapshot-pins:${index}`;
743
- const snapshotEventPinsPrefix = (s) => `${prefix}:${s}:snapshot-pins:`;
744
- const snapshotPinCountsKey = (s) => `${prefix}:${s}:snapshot-pin-counts`;
745
- const watchedKey = (s) => `${prefix}:${s}:watched`;
746
- const presenceKey = (s) => `${prefix}:${s}:presence`;
747
- const idsKey = `${prefix}:ids`;
748
- const evalScript = (script, keys, args) => call("EVAL", script, keys.length, ...keys, ...args);
749
- const readFrontier = async (sessionId) => {
750
- const frontier = Number(await call("GET", countKey(sessionId)) ?? 0);
751
- if (!Number.isSafeInteger(frontier) || frontier < 0) throw new A2Error("STORE_UNAVAILABLE", `redis event count for session '${sessionId}' is invalid`);
752
- return frontier;
753
- };
754
- const readRangePage = async (sessionId, afterIndex, throughIndex, limit) => {
755
- if (throughIndex <= afterIndex) return [];
756
- const entries = await call("XRANGE", storeKey(sessionId), `${afterIndex + 1}-0`, `${throughIndex}-0`, "COUNT", limit);
757
- if (entries.length === 0) return [];
758
- const metaFields = entries.flatMap((entry) => {
759
- const index = entryIndex(entry[0]);
760
- return STORED_EVENT_META_SUFFIXES.map((suffix) => `${index}:${suffix}`);
761
- });
762
- const metaValues = await call("HMGET", metaKey(sessionId), ...metaFields) ?? [];
763
- const meta = {};
764
- for (let i = 0; i < metaFields.length; i += 1) {
765
- const value = metaValues[i];
766
- if (value !== null && value !== void 0) meta[metaFields[i]] = value;
767
- }
768
- return entries.map((entry) => {
769
- const event = toEvent(sessionId, entry);
770
- const idx = event.index;
771
- const fields = fieldMap(entry[1]);
772
- const processedAt = meta[`${idx}:p`];
773
- const processedByAttempt = meta[`${idx}:pa`];
774
- const returnedEventIds = meta[`${idx}:ri`];
775
- const claimHolder = meta[`${idx}:ch`];
776
- const claimExpiresAt = meta[`${idx}:ce`];
777
- const failedAt = meta[`${idx}:f`];
778
- const firstClaimedAt = meta[`${idx}:fc`];
779
- const lastClaimedAt = meta[`${idx}:lc`];
780
- const lastFailedAt = meta[`${idx}:lf`];
781
- const lastFailedAttempt = meta[`${idx}:lfa`];
782
- return Object.assign(event, {
783
- cause: toCause(fields["cause"]),
784
- lane: fields["lane"] === "" ? null : fields["lane"],
785
- processedAt: processedAt === void 0 ? null : new Date(Number(processedAt)),
786
- processedByAttempt: processedByAttempt === void 0 ? null : Number(processedByAttempt),
787
- returnedEventIds: returnedEventIds === void 0 ? null : decodeReturnedEventIds(returnedEventIds),
788
- firstClaimedAt: firstClaimedAt === void 0 ? null : new Date(Number(firstClaimedAt)),
789
- lastClaimedAt: lastClaimedAt === void 0 ? null : new Date(Number(lastClaimedAt)),
790
- attemptCount: Number(meta[`${idx}:d`] ?? 0),
791
- claimHolder: claimHolder ?? null,
792
- claimExpiresAt: claimExpiresAt === void 0 ? null : new Date(Number(claimExpiresAt)),
793
- failureCount: Number(meta[`${idx}:a`] ?? 0),
794
- lastFailedAt: lastFailedAt === void 0 ? null : new Date(Number(lastFailedAt)),
795
- lastFailedAttempt: lastFailedAttempt === void 0 ? null : Number(lastFailedAttempt),
796
- lastError: meta[`${idx}:e`] ?? null,
797
- failedAt: failedAt === void 0 ? null : new Date(Number(failedAt))
798
- });
799
- });
800
- };
801
- const readEventPages = async (sessionId, afterIndex, throughIndex) => {
802
- if (throughIndex <= afterIndex) return [];
803
- const events = [];
804
- let nextIndex = afterIndex + 1;
805
- while (nextIndex <= throughIndex) {
806
- const entries = await call("XRANGE", storeKey(sessionId), `${nextIndex}-0`, `${throughIndex}-0`, "COUNT", READ_PAGE_SIZE);
807
- if (entries.length === 0) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' ended before index ${throughIndex}`);
808
- for (let position = 0; position < entries.length; position += 1) {
809
- const index = entryIndex(entries[position][0]);
810
- if (index !== nextIndex + position || index > throughIndex) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' is not contiguous`);
811
- }
812
- events.push(...entries.map((entry) => toEvent(sessionId, entry)));
813
- nextIndex = events.at(-1).index + 1;
814
- }
815
- return events;
816
- };
817
- const readEventRange = async (sessionId, afterIndex, throughIndex) => {
818
- const events = [];
819
- for (let start = afterIndex; start < throughIndex; start += READ_PAGE_SIZE * READ_CONCURRENCY) {
820
- const ranges = Array.from({ length: Math.min(READ_CONCURRENCY, Math.ceil((throughIndex - start) / READ_PAGE_SIZE)) }, (_, position) => {
821
- const after = start + position * READ_PAGE_SIZE;
822
- return readEventPages(sessionId, after, Math.min(after + READ_PAGE_SIZE, throughIndex));
823
- });
824
- const results = await Promise.allSettled(ranges);
825
- for (const result of results) {
826
- if (result.status === "rejected") throw result.reason;
827
- events.push(...result.value);
828
- }
829
- }
830
- return events;
831
- };
832
- /** Bounded XRANGE pages restore the full-read contract on capped providers. */
833
- const readRange = async (sessionId, afterIndex, throughIndex) => {
834
- if (throughIndex !== void 0 && throughIndex <= afterIndex) return [];
835
- const currentFrontier = await readFrontier(sessionId);
836
- const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier);
837
- if (frontier <= afterIndex) return [];
838
- const rows = [];
839
- let nextIndex = afterIndex + 1;
840
- while (nextIndex <= frontier) {
841
- const page = await readRangePage(sessionId, nextIndex - 1, frontier, READ_PAGE_SIZE);
842
- if (page.length === 0) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' ended before index ${frontier}`);
843
- for (let position = 0; position < page.length; position += 1) if (page[position].index !== nextIndex + position) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' is not contiguous`);
844
- const lastIndex = page.at(-1).index;
845
- if (lastIndex < nextIndex || lastIndex > frontier) throw new A2Error("STORE_UNAVAILABLE", `redis event range for session '${sessionId}' did not advance`);
846
- rows.push(...page);
847
- nextIndex = lastIndex + 1;
848
- }
849
- return rows;
850
- };
851
- return {
852
- async append(sessionId, events) {
853
- return wrap(async () => {
854
- const suppliedIds = events.filter((e) => e.id !== void 0).map((e) => e.id);
855
- if (new Set(suppliedIds).size !== suppliedIds.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
856
- const now = clock.now().getTime();
857
- const withIds = events.map((e) => ({
858
- id: e.id ?? generateId(),
859
- type: e.type,
860
- payload: e.payload,
861
- cause: e.cause ? { ...e.cause } : null,
862
- lane: e.lane ?? null,
863
- settled: e.settled === true
864
- }));
865
- const args = [
866
- sessionId,
867
- now,
868
- withIds.length,
869
- ...withIds.flatMap((e) => [
870
- e.id,
871
- e.type,
872
- json(e.payload),
873
- e.cause ? json(e.cause) : "",
874
- e.lane ?? "",
875
- e.settled ? "1" : "0"
876
- ]),
877
- `${prefix}:${sessionId}:notify`
878
- ];
879
- const reply = await evalScript(APPEND_LUA, [
880
- storeKey(sessionId),
881
- countKey(sessionId),
882
- idsKey,
883
- metaKey(sessionId),
884
- pendingKey(sessionId),
885
- readyKey(sessionId),
886
- watchedKey(sessionId)
887
- ], args);
888
- const [verdict] = reply;
889
- if (verdict === "empty") return {
890
- events: [],
891
- hasPending: reply[1] === "1"
892
- };
893
- if (verdict === "foreign") throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${reply[1]}' already exists in another session`);
894
- if (verdict === "no_parent") throw new TypeError(`no event at index ${reply[1]} in session '${sessionId}'`);
895
- if (verdict === "superseded") throw new A2Error("SUPERSEDED_ATTEMPT", `a newer attempt owns event ${reply[1]} in session '${sessionId}'`);
896
- if (verdict === "partial") throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${reply[1]} already-appended and ${events.length - Number(reply[1])} fresh events`);
897
- if (verdict === "dup") {
898
- const hasPending = reply[1] === "1";
899
- const wanted = new Set(reply.slice(2).map(Number));
900
- const min = Math.min(...wanted);
901
- const existing = (await readRange(sessionId, min - 1)).filter((row) => wanted.has(row.index));
902
- return {
903
- events: idempotentReplay(events, existing),
904
- hasPending
905
- };
906
- }
907
- const base = Number(reply[1]) - 1;
908
- return {
909
- events: withIds.map((e, i) => ({
910
- id: e.id,
911
- type: e.type,
912
- payload: structuredClone(e.payload),
913
- index: base + 1 + i,
914
- sessionId,
915
- createdAt: new Date(now),
916
- cause: e.cause,
917
- lane: e.lane,
918
- processedAt: e.settled ? new Date(now) : null,
919
- processedByAttempt: null,
920
- returnedEventIds: null,
921
- firstClaimedAt: null,
922
- lastClaimedAt: null,
923
- attemptCount: 0,
924
- claimHolder: null,
925
- claimExpiresAt: null,
926
- failureCount: 0,
927
- lastFailedAt: null,
928
- lastFailedAttempt: null,
929
- lastError: null,
930
- failedAt: null
931
- })),
932
- hasPending: reply[2] === "1"
933
- };
934
- });
935
- },
936
- async read(sessionId, opts) {
937
- return wrap(async () => {
938
- return await readRange(sessionId, opts?.afterIndex ?? 0, opts?.throughIndex);
939
- });
940
- },
941
- async readHistory(sessionId, opts) {
942
- return wrap(async () => {
943
- const after = opts?.afterIndex ?? 0;
944
- if (opts?.throughIndex !== void 0 && opts.throughIndex <= after) return [];
945
- const frontier = await readFrontier(sessionId);
946
- return readEventRange(sessionId, after, Math.min(opts?.throughIndex ?? frontier, frontier));
947
- });
948
- },
949
- async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes }) {
950
- return wrap(async () => {
951
- const now = clock.now().getTime();
952
- const expiresAt = expiresAtMs ?? now + ttlMs;
953
- const excluded = excludeIndexes ?? [];
954
- const reply = await evalScript(CLAIM_AVAILABLE_LUA, [
955
- storeKey(sessionId),
956
- countKey(sessionId),
957
- metaKey(sessionId),
958
- pendingKey(sessionId),
959
- readyKey(sessionId)
960
- ], [
961
- holder,
962
- now,
963
- expiresAt,
964
- excluded.length,
965
- ...excluded
966
- ]);
967
- return toClaimAvailableResult(sessionId, reply);
968
- });
969
- },
970
- async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
971
- return wrap(async () => {
972
- const now = clock.now().getTime();
973
- const expiresAt = expiresAtMs ?? now + ttlMs;
974
- const reply = await evalScript(RENEW_CLAIMS_LUA, [countKey(sessionId), metaKey(sessionId)], [
975
- holder,
976
- now,
977
- expiresAt,
978
- claims.length,
979
- ...claims.flatMap((claim) => [claim.index, claim.attempt])
980
- ]);
981
- const renewedCount = Number(reply[0]);
982
- const values = reply.slice(1).map(Number);
983
- return {
984
- renewed: values.slice(0, renewedCount).toSorted((a, b) => a - b),
985
- superseded: values.slice(renewedCount).toSorted((a, b) => a - b)
986
- };
987
- });
988
- },
989
- async completeAttempt({ sessionId, index, attempt, events }) {
990
- return wrap(async () => {
991
- const ids = events.map((event) => event.id);
992
- if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
993
- const now = clock.now().getTime();
994
- const reply = await evalScript(COMPLETE_ATTEMPT_LUA, [
995
- storeKey(sessionId),
996
- countKey(sessionId),
997
- idsKey,
998
- metaKey(sessionId),
999
- pendingKey(sessionId),
1000
- readyKey(sessionId),
1001
- watchedKey(sessionId),
1002
- snapshotEventPinsKey(sessionId, index),
1003
- snapshotPinCountsKey(sessionId)
1004
- ], [
1005
- sessionId,
1006
- now,
1007
- index,
1008
- attempt,
1009
- events.length,
1010
- ...events.flatMap((event) => [
1011
- event.id,
1012
- event.type,
1013
- json(event.payload),
1014
- event.lane ?? "",
1015
- event.settled === true ? "1" : "0"
1016
- ]),
1017
- `${prefix}:${sessionId}:notify`
1018
- ]);
1019
- const outcome = reply[0];
1020
- if (outcome === "missing") throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
1021
- if (outcome === "partial") throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event ids do not match the completed attempt");
1022
- if (outcome === "superseded") return { outcome };
1023
- const cause = {
1024
- index,
1025
- attempt
1026
- };
1027
- if (outcome === "ok") {
1028
- const base = Number(reply[1]) - 1;
1029
- return {
1030
- outcome: "completed",
1031
- events: events.map((event, i) => ({
1032
- id: event.id,
1033
- type: event.type,
1034
- payload: structuredClone(event.payload),
1035
- index: base + 1 + i,
1036
- sessionId,
1037
- createdAt: new Date(now),
1038
- cause,
1039
- lane: event.lane ?? null,
1040
- processedAt: event.settled === true ? new Date(now) : null,
1041
- processedByAttempt: null,
1042
- returnedEventIds: null,
1043
- firstClaimedAt: null,
1044
- lastClaimedAt: null,
1045
- attemptCount: 0,
1046
- claimHolder: null,
1047
- claimExpiresAt: null,
1048
- failureCount: 0,
1049
- lastFailedAt: null,
1050
- lastFailedAttempt: null,
1051
- lastError: null,
1052
- failedAt: null
1053
- }))
1054
- };
1055
- }
1056
- if (outcome === "duplicate") {
1057
- if (events.length === 0) return {
1058
- outcome: "completed",
1059
- events: []
1060
- };
1061
- const wanted = new Set(reply.slice(1).map(Number));
1062
- const min = Math.min(...wanted);
1063
- const rows = await readRange(sessionId, min - 1);
1064
- const byIndex = new Map(rows.map((row) => [row.index, row]));
1065
- const existing = reply.slice(1).map((value) => byIndex.get(Number(value)));
1066
- return {
1067
- outcome: "completed",
1068
- events: idempotentReplay(events, existing)
1069
- };
1070
- }
1071
- throw new TypeError(`unexpected completion outcome '${outcome}'`);
1072
- });
1073
- },
1074
- async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
1075
- return wrap(async () => {
1076
- const reply = await evalScript(FAIL_ATTEMPT_LUA, [
1077
- countKey(sessionId),
1078
- metaKey(sessionId),
1079
- pendingKey(sessionId),
1080
- readyKey(sessionId),
1081
- snapshotEventPinsKey(sessionId, index),
1082
- snapshotPinCountsKey(sessionId)
1083
- ], [
1084
- index,
1085
- attempt,
1086
- error,
1087
- maxFailures,
1088
- clock.now().getTime()
1089
- ]);
1090
- const outcome = reply[0];
1091
- if (outcome === "missing") throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
1092
- return {
1093
- outcome,
1094
- failureCount: Number(reply[1])
1095
- };
1096
- });
1097
- },
1098
- async readState(sessionId, reducerName, stateOptions) {
1099
- return wrap(async () => {
1100
- const reply = await evalScript(READ_STATE_LUA, [
1101
- snapKey(sessionId, reducerName),
1102
- snapHistoryKey(sessionId, reducerName),
1103
- countKey(sessionId)
1104
- ], [stateOptions?.snapshotThroughIndex ?? stateOptions?.throughIndex ?? ""]);
1105
- const rawHead = reply[0];
1106
- const raw = reply[1];
1107
- const headIndex = rawHead === "" ? null : Number(rawHead);
1108
- const parsed = raw === "" ? null : JSON.parse(raw);
1109
- const currentFrontier = Number(reply[2]);
1110
- if (!Number.isSafeInteger(currentFrontier) || currentFrontier < 0 || headIndex !== null && (!Number.isSafeInteger(headIndex) || headIndex < 0) || parsed !== null && (!Number.isSafeInteger(parsed.index) || parsed.index < 0)) throw new A2Error("STORE_UNAVAILABLE", `redis state frontier for session '${sessionId}' is invalid`);
1111
- const frontier = Math.min(currentFrontier, stateOptions?.throughIndex ?? currentFrontier);
1112
- const snapshot = parsed;
1113
- const events = await readEventRange(sessionId, snapshot?.index ?? 0, frontier);
1114
- return {
1115
- headIndex,
1116
- snapshot: snapshot ? {
1117
- index: snapshot.index,
1118
- state: snapshot.state
1119
- } : null,
1120
- events
1121
- };
1122
- });
1123
- },
1124
- async putSnapshots(sessionId, reducerName, writes) {
1125
- const byIndex = /* @__PURE__ */ new Map();
1126
- for (const write of writes) {
1127
- const existing = byIndex.get(write.index);
1128
- if (existing) {
1129
- existing.state = write.state;
1130
- for (const eventIndex of write.pinEventIndexes ?? []) existing.pinEventIndexes.add(eventIndex);
1131
- } else byIndex.set(write.index, {
1132
- index: write.index,
1133
- state: write.state,
1134
- pinEventIndexes: new Set(write.pinEventIndexes ?? [])
1135
- });
1136
- }
1137
- const normalized = [...byIndex.values()].toSorted((a, b) => a.index - b.index);
1138
- if (normalized.length === 0) return;
1139
- await wrap(async () => {
1140
- const args = [
1141
- clock.now().getTime(),
1142
- snapshotEventPinsPrefix(sessionId),
1143
- normalized.length
1144
- ];
1145
- for (const write of normalized) args.push(write.index, json({
1146
- index: write.index,
1147
- state: write.state,
1148
- updatedAt: clock.now().getTime()
1149
- }), write.pinEventIndexes.size, ...write.pinEventIndexes);
1150
- await evalScript(PUT_SNAPSHOTS_LUA, [
1151
- snapKey(sessionId, reducerName),
1152
- snapHistoryKey(sessionId, reducerName),
1153
- countKey(sessionId),
1154
- metaKey(sessionId),
1155
- snapshotPinCountsKey(sessionId)
1156
- ], args);
1157
- });
1158
- },
1159
- presence: {
1160
- async set(ns, participant, values, meta) {
1161
- return wrap(async () => {
1162
- const entries = Object.entries(values);
1163
- if (entries.length === 0) return null;
1164
- const atMs = meta.at.getTime();
1165
- const expiresAtMs = clock.now().getTime() + meta.ttlMs;
1166
- const applied = await evalScript(PRESENCE_SET_LUA, [presenceKey(ns)], [
1167
- atMs,
1168
- meta.ttlMs,
1169
- entries.length,
1170
- ...entries.flatMap(([field, value]) => [
1171
- JSON.stringify([participant, field]),
1172
- value === null ? "" : json({
1173
- v: value,
1174
- s: meta.seen,
1175
- a: atMs,
1176
- e: expiresAtMs
1177
- }),
1178
- `${json(field)}:${json(value)}`
1179
- ]),
1180
- presenceKey(ns),
1181
- `${json({
1182
- src: options.presenceSource,
1183
- participant,
1184
- seen: meta.seen,
1185
- at: atMs
1186
- }).slice(0, -1)},"values":{`
1187
- ]);
1188
- if (applied.length === 0) return null;
1189
- const appliedValues = nullProtoRecord();
1190
- for (const position of applied) {
1191
- const [field, value] = entries[Number(position) - 1];
1192
- appliedValues[field] = structuredClone(value);
1193
- }
1194
- return {
1195
- participant,
1196
- values: appliedValues,
1197
- seen: meta.seen,
1198
- at: new Date(atMs)
1199
- };
1200
- });
1201
- },
1202
- async read(ns) {
1203
- return wrap(async () => {
1204
- const reply = await evalScript(PRESENCE_READ_LUA, [presenceKey(ns)], [clock.now().getTime()]);
1205
- const rows = [];
1206
- for (let i = 0; i + 1 < reply.length; i += 2) {
1207
- const [participant, field] = JSON.parse(reply[i]);
1208
- const entry = JSON.parse(reply[i + 1]);
1209
- rows.push({
1210
- participant,
1211
- field,
1212
- value: entry.v,
1213
- seen: entry.s,
1214
- at: new Date(entry.a),
1215
- expiresAt: new Date(entry.e)
1216
- });
1217
- }
1218
- return rows;
1219
- });
1220
- }
1221
- },
1222
- async readRange(sessionId, afterIndex, throughIndex) {
1223
- return wrap(() => readRange(sessionId, afterIndex, throughIndex));
1224
- },
1225
- async readEvents(sessionId, afterIndex) {
1226
- return wrap(async () => {
1227
- return (await call("XRANGE", storeKey(sessionId), `${afterIndex + 1}-0`, "+", "COUNT", READ_PAGE_SIZE)).map((entry) => toEvent(sessionId, entry));
1228
- });
1229
- },
1230
- async markWatched(sessionId, ttlMs) {
1231
- await wrap(() => call("SET", watchedKey(sessionId), "1", "PX", ttlMs));
1232
- }
1233
- };
1234
- }
1235
- //#endregion
1236
- //#region src/store-redis-notify.ts
1237
- function createRedisNotifyStore(options) {
1238
- const { client, clock, ids, keyPrefix: prefix } = options;
1239
- const notifyChannel = (sessionId) => `${prefix}:${sessionId}:notify`;
1240
- const presenceChannel = (ns) => `${prefix}:${ns}:presence`;
1241
- const instanceId = crypto.randomUUID();
1242
- const core = createRedisStoreCore({
1243
- call: async (command, ...args) => {
1244
- return (await client()).call(command, ...args);
1245
- },
1246
- clock,
1247
- ids,
1248
- keyPrefix: prefix,
1249
- presenceSource: instanceId
1250
- });
1251
- let severed = false;
1252
- let subscriber = null;
1253
- /** channel → parked-feed wake-ups, armed before each catch-up read. */
1254
- const wakers = /* @__PURE__ */ new Map();
1255
- /** channel → refcounted subscription held for each live iterator. */
1256
- const subscriptions = /* @__PURE__ */ new Map();
1257
- /**
1258
- * channel → last presence-marker refresh, throttling the SET.
1259
- * Deliberately real time (`Date.now()`), not the injected clock:
1260
- * the marker's PX expiry runs on server real time, and the throttle
1261
- * must tick with the TTL it refreshes or time-traveling tests would
1262
- * desync the two. Process-local and never stored, so the injected
1263
- * clock's every-stored-timestamp pledge is untouched.
1264
- */
1265
- const watched = /* @__PURE__ */ new Map();
1266
- const presenceSubscriptions = /* @__PURE__ */ new Map();
1267
- const pollingStreams = /* @__PURE__ */ new Set();
1268
- options.onReconnect?.((channel) => {
1269
- for (const wake of wakers.get(channel) ?? []) wake();
1270
- presenceSubscriptions.get(channel)?.recovery?.reconnect().catch(() => {});
1271
- });
1272
- const deliverRemotePresence = ({ subscription, message }) => {
1273
- let parsed;
1274
- try {
1275
- parsed = JSON.parse(message);
1276
- } catch {
1277
- return;
1278
- }
1279
- if (parsed.src === instanceId) return;
1280
- const patch = {
1281
- participant: parsed.participant,
1282
- values: parsed.values,
1283
- seen: parsed.seen,
1284
- at: new Date(parsed.at)
1285
- };
1286
- subscription.recovery?.update(patch);
1287
- for (const listener of subscription.listeners) listener(patch);
1288
- };
1289
- const getSubscriber = () => {
1290
- subscriber ??= (async () => {
1291
- const conn = (await client()).duplicate();
1292
- conn.on("message", (channel, message) => {
1293
- const set = wakers.get(channel);
1294
- if (set) for (const wake of set) wake();
1295
- const subscription = presenceSubscriptions.get(channel);
1296
- if (subscription) deliverRemotePresence({
1297
- subscription,
1298
- message
1299
- });
1300
- });
1301
- return conn;
1302
- })();
1303
- return subscriber;
1304
- };
1305
- /**
1306
- * The subscriber, gated on connection readiness — for channel
1307
- * commands only (`close` uses the ungated promise, so it can never
1308
- * hang on an unreachable server). SUBSCRIBE carries redis's
1309
- * ok-loading flag, so ioredis writes it mid-handshake, ahead of the
1310
- * ready check's INFO — which then finds the connection already in
1311
- * subscriber mode and fails the check (a spurious error event plus
1312
- * a reconnect). ECHO has no such flag: it resolves only once the
1313
- * connection is ready, so every channel command chained behind it
1314
- * lands after the check.
1315
- */
1316
- let subscriberGate = null;
1317
- const gatedSubscriber = () => {
1318
- subscriberGate ??= getSubscriber().then(async (conn) => {
1319
- await conn.call("echo", "ready");
1320
- return conn;
1321
- });
1322
- return subscriberGate;
1323
- };
1324
- const acquireChannel = async (channel) => {
1325
- const existing = subscriptions.get(channel);
1326
- if (existing) {
1327
- existing.refs += 1;
1328
- try {
1329
- await existing.ready;
1330
- } catch (err) {
1331
- existing.refs -= 1;
1332
- throw err;
1333
- }
1334
- return;
1335
- }
1336
- const lease = {
1337
- refs: 1,
1338
- ready: gatedSubscriber().then(async (conn) => {
1339
- await conn.call("subscribe", channel);
1340
- if (subscriptions.get(channel) !== lease || severed) return;
1341
- const presence = presenceSubscriptions.get(channel);
1342
- if (options.polling?.() && presence) {
1343
- presence.recovery = void 0;
1344
- const ns = channel.slice(prefix.length + 1, -9);
1345
- presence.poll = pollPresence({
1346
- read: () => core.presence.read(ns),
1347
- deliver: (patch) => {
1348
- for (const listener of presence.listeners) listener(patch);
1349
- }
1350
- });
1351
- await presence.poll.ready;
1352
- }
1353
- })
1354
- };
1355
- subscriptions.set(channel, lease);
1356
- try {
1357
- await lease.ready;
1358
- } catch (err) {
1359
- if (subscriptions.get(channel) === lease) {
1360
- subscriptions.delete(channel);
1361
- const presence = presenceSubscriptions.get(channel);
1362
- presence?.poll?.stop();
1363
- if (presence) presence.poll = void 0;
1364
- }
1365
- throw err;
1366
- }
1367
- };
1368
- const releaseChannel = (channel) => {
1369
- const lease = subscriptions.get(channel);
1370
- if (!lease) return;
1371
- lease.refs -= 1;
1372
- if (lease.refs > 0) return;
1373
- subscriptions.delete(channel);
1374
- watched.delete(channel);
1375
- gatedSubscriber().then((conn) => conn.call("unsubscribe", channel)).catch(() => {});
1376
- };
1377
- const armWaker = (channel) => {
1378
- let wake;
1379
- const wakeup = new Promise((resolve) => {
1380
- wake = resolve;
1381
- });
1382
- let set = wakers.get(channel);
1383
- if (!set) {
1384
- set = /* @__PURE__ */ new Set();
1385
- wakers.set(channel, set);
1386
- }
1387
- set.add(wake);
1388
- return {
1389
- wakeup,
1390
- wake,
1391
- disarm: () => {
1392
- set.delete(wake);
1393
- if (set.size === 0) wakers.delete(channel);
1394
- }
1395
- };
1396
- };
1397
- return {
1398
- append: core.append,
1399
- read: core.read,
1400
- readHistory: core.readHistory,
1401
- claimAvailable: core.claimAvailable,
1402
- renewClaims: core.renewClaims,
1403
- completeAttempt: core.completeAttempt,
1404
- failAttempt: core.failAttempt,
1405
- readState: core.readState,
1406
- putSnapshots: core.putSnapshots,
1407
- presence: {
1408
- async set(ns, participant, values, meta) {
1409
- const patch = await core.presence.set(ns, participant, values, meta);
1410
- if (!patch) return;
1411
- const channel = presenceChannel(ns);
1412
- const subscription = presenceSubscriptions.get(channel);
1413
- if (subscription) {
1414
- subscription.poll?.update(patch);
1415
- subscription.recovery?.update(patch);
1416
- for (const listener of subscription.listeners) listener(patch);
1417
- }
1418
- },
1419
- async read(ns) {
1420
- const channel = presenceChannel(ns);
1421
- await subscriptions.get(channel)?.ready;
1422
- return presenceSubscriptions.get(channel)?.recovery?.read() ?? core.presence.read(ns);
1423
- },
1424
- subscribe(ns, onPatch) {
1425
- const channel = presenceChannel(ns);
1426
- let subscription = presenceSubscriptions.get(channel);
1427
- if (!subscription) {
1428
- subscription = { listeners: /* @__PURE__ */ new Set() };
1429
- presenceSubscriptions.set(channel, subscription);
1430
- if (options.onReconnect && !options.polling?.()) {
1431
- const current = subscription;
1432
- subscription.recovery = presenceRecovery({
1433
- read: () => core.presence.read(ns),
1434
- deliver: (patch) => {
1435
- for (const listener of current.listeners) listener(patch);
1436
- },
1437
- isActive: () => !severed && presenceSubscriptions.get(channel) === current
1438
- });
1439
- }
1440
- }
1441
- subscription.listeners.add(onPatch);
1442
- acquireChannel(channel).catch(() => {});
1443
- let stopped = false;
1444
- return () => {
1445
- if (stopped) return;
1446
- stopped = true;
1447
- subscription.listeners.delete(onPatch);
1448
- if (subscription.listeners.size === 0) {
1449
- subscription.poll?.stop();
1450
- presenceSubscriptions.delete(channel);
1451
- }
1452
- releaseChannel(channel);
1453
- };
1454
- }
1455
- },
1456
- stream(sessionId, opts) {
1457
- const startAfter = opts?.startAfter ?? 0;
1458
- const channel = notifyChannel(sessionId);
1459
- return { [Symbol.asyncIterator]() {
1460
- let last = startAfter;
1461
- let buffer = [];
1462
- let closed = false;
1463
- let subscribed = false;
1464
- let pending = null;
1465
- let interrupt = null;
1466
- let poller;
1467
- let stopActivity;
1468
- const activity = createStreamActivity(() => {
1469
- if (poller) streamActivities.get(poller)?.activate();
1470
- });
1471
- const release = () => {
1472
- stopActivity?.();
1473
- if (poller) {
1474
- poller.return?.();
1475
- pollingStreams.delete(poller);
1476
- }
1477
- if (!subscribed) return;
1478
- subscribed = false;
1479
- releaseChannel(channel);
1480
- };
1481
- const iterator = {
1482
- async next() {
1483
- for (;;) {
1484
- if (closed) {
1485
- release();
1486
- return {
1487
- value: void 0,
1488
- done: true
1489
- };
1490
- }
1491
- const row = buffer.shift();
1492
- if (row) {
1493
- last = row.index;
1494
- return {
1495
- value: row,
1496
- done: false
1497
- };
1498
- }
1499
- if (severed) {
1500
- release();
1501
- throw new A2Error("STORE_UNAVAILABLE", "the store was closed");
1502
- }
1503
- const waker = armWaker(channel);
1504
- interrupt = waker.wake;
1505
- try {
1506
- if (!subscribed) {
1507
- subscribed = true;
1508
- await acquireChannel(channel);
1509
- if (closed || severed) continue;
1510
- }
1511
- if (options.polling?.()) {
1512
- if (!poller) {
1513
- poller = pollingStream((after) => core.readEvents(sessionId, after), { startAfter: last })[Symbol.asyncIterator]();
1514
- pollingStreams.add(poller);
1515
- stopActivity = streamActivities.get(poller)?.subscribe(activity.setIdle);
1516
- }
1517
- const result = await poller.next();
1518
- if (!result.done) presenceSubscriptions.get(presenceChannel(sessionId))?.poll?.wake();
1519
- if (severed) throw new A2Error("STORE_UNAVAILABLE", "the store was closed");
1520
- return result;
1521
- }
1522
- let marker = watched.get(channel);
1523
- if (!marker || Date.now() - marker.at >= NOTIFY_TIMINGS.safetyReadMs) {
1524
- marker = {
1525
- at: Date.now(),
1526
- ready: core.markWatched(sessionId, NOTIFY_TIMINGS.safetyReadMs * 3)
1527
- };
1528
- watched.set(channel, marker);
1529
- }
1530
- await marker.ready;
1531
- if (closed || severed) continue;
1532
- buffer = await core.readEvents(sessionId, last);
1533
- if (buffer.length === 0 && !closed && !severed) {
1534
- pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs);
1535
- await Promise.race([waker.wakeup, pending.promise]);
1536
- pending.cancel();
1537
- pending = null;
1538
- }
1539
- } catch (err) {
1540
- release();
1541
- if (closed) return {
1542
- value: void 0,
1543
- done: true
1544
- };
1545
- if (err instanceof A2Error) throw err;
1546
- throw new A2Error("STORE_UNAVAILABLE", "redis store operation failed", { cause: err });
1547
- } finally {
1548
- waker.disarm();
1549
- interrupt = null;
1550
- }
1551
- }
1552
- },
1553
- async return() {
1554
- closed = true;
1555
- pending?.cancel();
1556
- interrupt?.();
1557
- release();
1558
- return {
1559
- value: void 0,
1560
- done: true
1561
- };
1562
- }
1563
- };
1564
- streamActivities.set(iterator, activity);
1565
- return iterator;
1566
- } };
1567
- },
1568
- async close() {
1569
- severed = true;
1570
- for (const subscription of presenceSubscriptions.values()) subscription.poll?.stop();
1571
- presenceSubscriptions.clear();
1572
- for (const poller of pollingStreams) poller.return?.();
1573
- pollingStreams.clear();
1574
- for (const set of wakers.values()) for (const wake of set) wake();
1575
- if (subscriber) (await subscriber.catch(() => null))?.disconnect();
1576
- await options.closeClient();
1577
- }
1578
- };
1579
- }
1580
- //#endregion
1581
- export { createRedisNotifyStore as t };
1582
-
1583
- //# sourceMappingURL=store-redis-notify-BVLUfI3j.js.map