experimental-a2 0.20.0 → 0.22.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 (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/actor-shared-DFET5q16.js.map +1 -0
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +3 -3
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +86 -73
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  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 +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  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 +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
@@ -1,10 +1,23 @@
1
- import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-DGI4jTQ5.js";
2
- import { _ as nullProtoRecord } from "./internal-CscSY7K_.js";
3
- import { t as idempotentReplay } from "./idempotent-replay-JdgMQRRm.js";
1
+ import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-wHd5GCpZ.js";
2
+ import { y as nullProtoRecord } from "./internal-CvsrprnU.js";
3
+ import { a as idempotentReplay, i as idleLogCursor } from "./store-codec-DwrWlgQx.js";
4
4
  import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
5
5
  //#region src/store-memory.ts
6
6
  const RECLAIM_BATCH = 256;
7
7
  const headOf = (log) => log ? log.offset + log.rows.length : 0;
8
+ const freshDispatch = () => ({
9
+ firstClaimedAt: null,
10
+ lastClaimedAt: null,
11
+ attemptCount: 0,
12
+ claimHolder: null,
13
+ claimExpiresAt: null,
14
+ failureCount: 0,
15
+ lastFailedAt: null,
16
+ lastFailedAttempt: null,
17
+ lastError: null,
18
+ failedAt: null
19
+ });
20
+ const date = (value) => value ? new Date(value) : null;
8
21
  const toStored = (row) => ({
9
22
  id: row.id,
10
23
  type: row.type,
@@ -12,21 +25,7 @@ const toStored = (row) => ({
12
25
  index: row.index,
13
26
  sessionId: row.sessionId,
14
27
  createdAt: new Date(row.createdAt),
15
- cause: row.cause ? { ...row.cause } : null,
16
- lane: row.lane,
17
- processedAt: row.processedAt ? new Date(row.processedAt) : null,
18
- processedByAttempt: row.processedByAttempt,
19
- returnedEventIds: row.returnedEventIds ? [...row.returnedEventIds] : null,
20
- firstClaimedAt: row.firstClaimedAt ? new Date(row.firstClaimedAt) : null,
21
- lastClaimedAt: row.lastClaimedAt ? new Date(row.lastClaimedAt) : null,
22
- attemptCount: row.attemptCount,
23
- claimHolder: row.claimHolder,
24
- claimExpiresAt: row.claimExpiresAt ? new Date(row.claimExpiresAt) : null,
25
- failureCount: row.failureCount,
26
- lastFailedAt: row.lastFailedAt ? new Date(row.lastFailedAt) : null,
27
- lastFailedAttempt: row.lastFailedAttempt,
28
- lastError: row.lastError,
29
- failedAt: row.failedAt ? new Date(row.failedAt) : null
28
+ cause: row.cause ? { ...row.cause } : null
30
29
  });
31
30
  const toEvent = (row) => ({
32
31
  id: row.id,
@@ -36,15 +35,63 @@ const toEvent = (row) => ({
36
35
  sessionId: row.sessionId,
37
36
  createdAt: new Date(row.createdAt)
38
37
  });
38
+ const toHead = (cursor, row) => ({
39
+ ...toStored(row),
40
+ consumer: cursor.consumer,
41
+ lane: cursor.lane,
42
+ firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
43
+ lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
44
+ attemptCount: cursor.dispatch.attemptCount,
45
+ claimHolder: cursor.dispatch.claimHolder,
46
+ claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
47
+ failureCount: cursor.dispatch.failureCount,
48
+ lastFailedAt: date(cursor.dispatch.lastFailedAt),
49
+ lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
50
+ lastError: cursor.dispatch.lastError,
51
+ failedAt: date(cursor.dispatch.failedAt)
52
+ });
53
+ const toCursor = (cursor) => ({
54
+ consumer: cursor.consumer,
55
+ lane: cursor.lane,
56
+ position: cursor.position,
57
+ head: cursor.head,
58
+ firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
59
+ lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
60
+ attemptCount: cursor.dispatch.attemptCount,
61
+ claimHolder: cursor.dispatch.claimHolder,
62
+ claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
63
+ failureCount: cursor.dispatch.failureCount,
64
+ lastFailedAt: date(cursor.dispatch.lastFailedAt),
65
+ lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
66
+ lastError: cursor.dispatch.lastError,
67
+ failedAt: date(cursor.dispatch.failedAt)
68
+ });
69
+ const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
70
+ /** Consumer, then lane, with concurrent cursors last by head. */
71
+ const byCursor = (a, b) => compareText(a.consumer, b.consumer) || (a.lane === null ? b.lane === null ? (a.head ?? 0) - (b.head ?? 0) : 1 : b.lane === null ? -1 : compareText(a.lane, b.lane));
72
+ const cursorKey = (consumer, lane, index) => lane === null ? `${consumer}\u0000#${index}` : `${consumer}\u0000L${lane}`;
73
+ const takes = (types, type) => type !== "a2.cleared" && (types === null || types.includes(type));
74
+ const byHead = (a, b) => a.index - b.index || compareText(a.consumer, b.consumer);
39
75
  const snapshotKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
40
- const eventPinKey = (sessionId, index) => `${sessionId}\u0000${index}`;
76
+ const headPinKey = (sessionId, ref) => `${sessionId}\u0000${cursorKey(ref.consumer, ref.lane, ref.index)}\u0000${ref.index}`;
77
+ /** Whether a cursor belongs to a consumer the caller serves; omitted, every one. */
78
+ const servedBy = (consumers) => {
79
+ if (!consumers) return () => true;
80
+ const names = new Set(consumers.map((consumer) => consumer.name));
81
+ return (cursor) => names.has(cursor.consumer);
82
+ };
83
+ /** A cursor's current head. */
84
+ const refOf = (cursor) => ({
85
+ consumer: cursor.consumer,
86
+ lane: cursor.lane,
87
+ index: cursor.head
88
+ });
41
89
  function memory(options = {}) {
42
90
  const clock = options.clock ?? SYSTEM_CLOCK;
43
91
  const generateId = options.ids ?? RANDOM_IDS;
44
92
  const sessions = /* @__PURE__ */ new Map();
45
- const dispatch = /* @__PURE__ */ new Map();
46
- /** Global unique index on event ids, like the SQL schema's. */
47
- const byEventId = /* @__PURE__ */ new Map();
93
+ /** Session id → cursor key → cursor. */
94
+ const cursors = /* @__PURE__ */ new Map();
48
95
  /** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
49
96
  const snapshots = /* @__PURE__ */ new Map();
50
97
  const historicalSnapshots = /* @__PURE__ */ new Map();
@@ -65,7 +112,8 @@ function memory(options = {}) {
65
112
  log = {
66
113
  rows: [],
67
114
  offset: 0,
68
- clearIndex: 0
115
+ clearIndex: 0,
116
+ byEventId: /* @__PURE__ */ new Map()
69
117
  };
70
118
  sessions.set(sessionId, log);
71
119
  }
@@ -76,14 +124,24 @@ function memory(options = {}) {
76
124
  return log && index > log.offset ? log.rows[index - log.offset - 1] : void 0;
77
125
  };
78
126
  const clearIndexOf = (sessionId) => sessions.get(sessionId)?.clearIndex ?? 0;
79
- /** The row owning `id`, ignoring this session's rows below its clear. */
127
+ /** The session's row owning `id`, ignoring rows below its clear. */
80
128
  const ownerOf = (sessionId, id) => {
81
- const row = byEventId.get(id);
82
- return row?.sessionId === sessionId && row.index < clearIndexOf(sessionId) ? void 0 : row;
129
+ const log = sessions.get(sessionId);
130
+ const row = log?.byEventId.get(id);
131
+ return log && row && row.index >= log.clearIndex ? row : void 0;
132
+ };
133
+ const cursorsOf = (sessionId) => {
134
+ let map = cursors.get(sessionId);
135
+ if (!map) {
136
+ map = /* @__PURE__ */ new Map();
137
+ cursors.set(sessionId, map);
138
+ }
139
+ return map;
83
140
  };
141
+ const cursorAt = (sessionId, ref) => cursors.get(sessionId)?.get(cursorKey(ref.consumer, ref.lane, ref.index));
84
142
  const checkpointKey = (sessionId, reducerName, index) => `${snapshotKey(sessionId, reducerName)}\u0000${index}`;
85
- const releaseSnapshotPins = (sessionId, eventIndex) => {
86
- const pinKey = eventPinKey(sessionId, eventIndex);
143
+ const releaseSnapshotPins = (sessionId, ref) => {
144
+ const pinKey = headPinKey(sessionId, ref);
87
145
  const checkpoints = snapshotPins.get(pinKey);
88
146
  if (!checkpoints) return;
89
147
  for (const key of checkpoints) {
@@ -95,97 +153,148 @@ function memory(options = {}) {
95
153
  }
96
154
  snapshotPins.delete(pinKey);
97
155
  };
98
- const dispatchOf = (sessionId) => {
99
- let state = dispatch.get(sessionId);
100
- if (!state) {
101
- state = {
102
- ready: /* @__PURE__ */ new Map(),
103
- lanes: /* @__PURE__ */ new Map()
104
- };
105
- dispatch.set(sessionId, state);
106
- }
107
- return state;
108
- };
109
- const enqueue = (sessionId, row) => {
110
- if (row.processedAt !== null) return;
111
- const state = dispatchOf(sessionId);
112
- if (row.lane === null) {
113
- state.ready.set(row.index, row);
114
- return;
115
- }
116
- let queue = state.lanes.get(row.lane);
117
- if (!queue) {
118
- queue = {
119
- rows: [],
120
- head: 0
121
- };
122
- state.lanes.set(row.lane, queue);
123
- }
124
- queue.rows.push(row);
125
- if (queue.rows.length === 1) state.ready.set(row.index, row);
126
- };
127
- const settle = (sessionId, row) => {
128
- const state = dispatchOf(sessionId);
129
- state.ready.delete(row.index);
130
- if (row.lane === null) return;
131
- const queue = state.lanes.get(row.lane);
132
- if (queue.rows[queue.head] !== row) return;
133
- queue.head += 1;
134
- if (queue.head === queue.rows.length) {
135
- state.lanes.delete(row.lane);
136
- return;
156
+ /** The next event after `after` that a log cursor takes, or null. */
157
+ const nextLogHead = (sessionId, after, types) => {
158
+ const log = sessions.get(sessionId);
159
+ if (!log) return null;
160
+ for (let offset = Math.max(0, after - log.offset); offset < log.rows.length; offset += 1) {
161
+ const row = log.rows[offset];
162
+ if (takes(types, row.type)) return row.index;
137
163
  }
138
- const next = queue.rows[queue.head];
139
- if (next.failedAt === null) state.ready.set(next.index, next);
164
+ return null;
140
165
  };
141
- const find = (sessionId, index) => {
142
- const row = rowAt(sessionId, index);
143
- if (!row) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
144
- return row;
166
+ /** Moves a cursor past its head: the next head starts with fresh bookkeeping. */
167
+ const advance = (sessionId, cursor) => {
168
+ const done = cursor.head;
169
+ releaseSnapshotPins(sessionId, refOf(cursor));
170
+ cursor.position = done;
171
+ cursor.dispatch = freshDispatch();
172
+ if (cursor.kind === "event") cursor.head = null;
173
+ else if (cursor.kind === "lane") {
174
+ cursor.queued += 1;
175
+ cursor.head = cursor.queue[cursor.queued] ?? null;
176
+ } else cursor.head = nextLogHead(sessionId, done, cursor.types);
145
177
  };
146
- const hasPending = (sessionId) => {
147
- const log = sessions.get(sessionId);
148
- return (log?.rows ?? []).some((row) => row.index > log.clearIndex && row.processedAt === null);
178
+ const hasPending = (sessionId, consumers) => {
179
+ const served = servedBy(consumers);
180
+ for (const cursor of cursors.get(sessionId)?.values() ?? []) if (cursor.head !== null && cursor.dispatch.failedAt === null && served(cursor)) return true;
181
+ return false;
149
182
  };
150
- const appendResult = (sessionId, events) => ({
151
- events,
152
- hasPending: hasPending(sessionId)
153
- });
154
- /** The attempt-currency fence shared by handler appends and clears. */
183
+ /** The attempt-currency fence shared by consumer appends and clears. */
155
184
  const assertCurrentCause = (sessionId, cause) => {
156
185
  if (cause.index <= clearIndexOf(sessionId)) throw supersededAttemptError(sessionId, cause);
157
- const parent = find(sessionId, cause.index);
158
- if (parent.attemptCount !== cause.attempt || parent.failedAt !== null) throw supersededAttemptError(sessionId, cause);
186
+ const cursor = cursorAt(sessionId, cause);
187
+ if (!cursor || cursor.head !== cause.index || cursor.dispatch.attemptCount !== cause.attempt || cursor.dispatch.failedAt !== null) throw supersededAttemptError(sessionId, cause);
188
+ };
189
+ const deadLetter = (sessionId, cursor, now) => {
190
+ cursor.dispatch.failedAt = new Date(now);
191
+ releaseSnapshotPins(sessionId, refOf(cursor));
159
192
  };
160
- const claim = ({ sessionId, row, holder, now, expiresAt, maxFailures }) => {
161
- if (row.claimHolder !== null) {
162
- row.failureCount += 1;
163
- if (maxFailures !== void 0 && row.failureCount >= maxFailures) {
164
- row.attemptCount += 1;
165
- row.failedAt = new Date(now);
166
- row.claimHolder = null;
167
- row.claimExpiresAt = null;
168
- dispatchOf(sessionId).ready.delete(row.index);
169
- releaseSnapshotPins(sessionId, row.index);
170
- return toStored(row);
193
+ const claim = ({ sessionId, cursor, holder, now, expiresAt, maxFailures }) => {
194
+ const dispatch = cursor.dispatch;
195
+ const row = rowAt(sessionId, cursor.head);
196
+ if (dispatch.claimHolder !== null) {
197
+ dispatch.failureCount += 1;
198
+ if (maxFailures !== void 0 && dispatch.failureCount >= maxFailures) {
199
+ dispatch.attemptCount += 1;
200
+ dispatch.claimHolder = null;
201
+ dispatch.claimExpiresAt = null;
202
+ deadLetter(sessionId, cursor, now);
203
+ return toHead(cursor, row);
171
204
  }
172
205
  }
173
- row.attemptCount += 1;
174
- row.firstClaimedAt ??= new Date(now);
175
- row.lastClaimedAt = new Date(now);
176
- row.claimHolder = holder;
177
- row.claimExpiresAt = new Date(expiresAt);
178
- return toStored(row);
206
+ dispatch.attemptCount += 1;
207
+ dispatch.firstClaimedAt ??= new Date(now);
208
+ dispatch.lastClaimedAt = new Date(now);
209
+ dispatch.claimHolder = holder;
210
+ dispatch.claimExpiresAt = new Date(expiresAt);
211
+ return toHead(cursor, row);
179
212
  };
180
213
  const checkBatchIds = (sessionId, events) => {
181
214
  const supplied = events.filter((candidate) => candidate.id !== void 0);
182
215
  if (new Set(supplied.map((candidate) => candidate.id)).size !== supplied.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
183
- const existing = supplied.map((candidate) => ownerOf(sessionId, candidate.id)).filter((row) => row !== void 0);
184
- const foreign = existing.find((row) => row.sessionId !== sessionId);
185
- if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.id}' already exists in another session`);
186
- return existing;
216
+ return supplied.map((candidate) => ownerOf(sessionId, candidate.id)).filter((row) => row !== void 0);
217
+ };
218
+ /** Records the types a declared log consumer takes on its cursor. */
219
+ const refreshLogTypes = (sessionId, consumers) => {
220
+ const map = cursors.get(sessionId);
221
+ if (!map) return;
222
+ for (const consumer of consumers ?? []) {
223
+ if (!consumer.log) continue;
224
+ const cursor = map.get(cursorKey(consumer.name, consumer.log.lane, 0));
225
+ if (cursor?.kind === "log") cursor.types = consumer.log.types;
226
+ }
227
+ };
228
+ /** Starts the cursor work an inserted batch carries. */
229
+ const enqueue = (sessionId, rows, events, base, consumers) => {
230
+ const map = cursorsOf(sessionId);
231
+ rows.forEach((row, offset) => {
232
+ for (const target of events[offset].targets ?? []) {
233
+ const key = cursorKey(target.consumer, target.lane, row.index);
234
+ if (target.lane === null) {
235
+ map.set(key, {
236
+ consumer: target.consumer,
237
+ lane: null,
238
+ kind: "event",
239
+ position: row.index - 1,
240
+ head: row.index,
241
+ dispatch: freshDispatch(),
242
+ queue: [],
243
+ queued: 0,
244
+ completions: /* @__PURE__ */ new Map(),
245
+ types: null
246
+ });
247
+ continue;
248
+ }
249
+ let cursor = map.get(key);
250
+ if (!cursor) {
251
+ cursor = {
252
+ consumer: target.consumer,
253
+ lane: target.lane,
254
+ kind: "lane",
255
+ position: clearIndexOf(sessionId),
256
+ head: null,
257
+ dispatch: freshDispatch(),
258
+ queue: [],
259
+ queued: 0,
260
+ completions: /* @__PURE__ */ new Map(),
261
+ types: null
262
+ };
263
+ map.set(key, cursor);
264
+ }
265
+ cursor.queue.push(row.index);
266
+ cursor.head ??= row.index;
267
+ }
268
+ });
269
+ const clearIndex = clearIndexOf(sessionId);
270
+ for (const consumer of consumers ?? []) {
271
+ const log = consumer.log;
272
+ if (!log) continue;
273
+ const first = rows.find((row) => takes(log.types, row.type));
274
+ if (!first) continue;
275
+ const key = cursorKey(consumer.name, log.lane, 0);
276
+ const cursor = map.get(key);
277
+ if (!cursor) {
278
+ const position = log.backfill ? clearIndex : base;
279
+ map.set(key, {
280
+ consumer: consumer.name,
281
+ lane: log.lane,
282
+ kind: "log",
283
+ position,
284
+ head: log.backfill ? nextLogHead(sessionId, position, log.types) : first.index,
285
+ dispatch: freshDispatch(),
286
+ queue: [],
287
+ queued: 0,
288
+ completions: /* @__PURE__ */ new Map(),
289
+ types: log.types
290
+ });
291
+ } else {
292
+ cursor.types = log.types;
293
+ cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
294
+ }
295
+ }
187
296
  };
188
- const insert = (sessionId, events, now, cause) => {
297
+ const insert = (sessionId, events, now, consumers, cause) => {
189
298
  const log = logOf(sessionId);
190
299
  const base = headOf(log);
191
300
  const inserted = events.map((candidate, offset) => ({
@@ -195,28 +304,14 @@ function memory(options = {}) {
195
304
  index: base + 1 + offset,
196
305
  sessionId,
197
306
  createdAt: new Date(now),
198
- cause: cause ? { ...cause } : candidate.cause ? { ...candidate.cause } : null,
199
- lane: candidate.lane ?? null,
200
- processedAt: candidate.settled ? new Date(now) : null,
201
- processedByAttempt: null,
202
- returnedEventIds: null,
203
- firstClaimedAt: null,
204
- lastClaimedAt: null,
205
- attemptCount: 0,
206
- claimHolder: null,
207
- claimExpiresAt: null,
208
- failureCount: 0,
209
- lastFailedAt: null,
210
- lastFailedAttempt: null,
211
- lastError: null,
212
- failedAt: null
307
+ cause: cause ? { ...cause } : candidate.cause ? { ...candidate.cause } : null
213
308
  }));
214
309
  for (const row of inserted) if (ownerOf(sessionId, row.id)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${row.id}' already exists`);
215
310
  for (const row of inserted) {
216
311
  log.rows.push(row);
217
- byEventId.set(row.id, row);
218
- enqueue(sessionId, row);
312
+ log.byEventId.set(row.id, row);
219
313
  }
314
+ enqueue(sessionId, inserted, events, base, consumers);
220
315
  return inserted;
221
316
  };
222
317
  /** Every cached fold of this session below its clear, head or historical. */
@@ -256,21 +351,35 @@ function memory(options = {}) {
256
351
  ...clearIndex > 0 ? { clearIndex } : {}
257
352
  };
258
353
  };
354
+ /** The dead-lettered head a redrive names, or undefined. */
355
+ const deadHeadOf = (sessionId, consumer, index) => {
356
+ if (index <= clearIndexOf(sessionId)) return void 0;
357
+ for (const cursor of cursors.get(sessionId)?.values() ?? []) if (cursor.consumer === consumer && cursor.head === index && cursor.dispatch.failedAt !== null) return cursor;
358
+ };
259
359
  return {
260
360
  async append(sessionId, events, appendOptions) {
261
- if (events.length === 0) return appendResult(sessionId, []);
361
+ const consumers = appendOptions?.consumers;
362
+ if (events.length === 0) return {
363
+ events: [],
364
+ hasPending: hasPending(sessionId, consumers)
365
+ };
262
366
  const existing = checkBatchIds(sessionId, events);
263
367
  if (existing.length > 0) {
264
- if (existing.length === events.length) return appendResult(sessionId, idempotentReplay(events, existing.map(toStored)));
368
+ if (existing.length === events.length) return {
369
+ events: idempotentReplay(events, existing.map(toStored)),
370
+ hasPending: hasPending(sessionId, consumers)
371
+ };
265
372
  throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
266
373
  }
267
374
  const originIndex = appendOptions?.originIndex;
268
375
  if (originIndex !== void 0 && originIndex <= clearIndexOf(sessionId)) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
269
376
  for (const event of events) if (event.cause) assertCurrentCause(sessionId, event.cause);
270
- const now = clock.now();
271
- const inserted = insert(sessionId, events, now);
377
+ const inserted = insert(sessionId, events, clock.now(), consumers);
272
378
  notifyStreams(sessionId, inserted);
273
- return appendResult(sessionId, inserted.map(toStored));
379
+ return {
380
+ events: inserted.map(toStored),
381
+ hasPending: hasPending(sessionId, consumers)
382
+ };
274
383
  },
275
384
  async clear({ sessionId, ifLastIndex, cause }) {
276
385
  if (cause) assertCurrentCause(sessionId, cause);
@@ -294,11 +403,21 @@ function memory(options = {}) {
294
403
  };
295
404
  const [marker] = insert(sessionId, [{
296
405
  type: CLEAR_EVENT_TYPE,
297
- payload: null,
298
- settled: true
299
- }], clock.now());
406
+ payload: null
407
+ }], clock.now(), void 0);
300
408
  logOf(sessionId).clearIndex = marker.index;
301
- dispatch.delete(sessionId);
409
+ const map = cursors.get(sessionId);
410
+ for (const [key, cursor] of map ?? []) {
411
+ if (cursor.head !== null) releaseSnapshotPins(sessionId, refOf(cursor));
412
+ if (cursor.kind !== "log") {
413
+ map.delete(key);
414
+ continue;
415
+ }
416
+ cursor.position = marker.index;
417
+ cursor.head = null;
418
+ cursor.dispatch = freshDispatch();
419
+ cursor.completions.clear();
420
+ }
302
421
  notifyStreams(sessionId, [marker]);
303
422
  return {
304
423
  cleared: true,
@@ -314,10 +433,7 @@ function memory(options = {}) {
314
433
  };
315
434
  const removed = log.rows.splice(0, Math.min(RECLAIM_BATCH, log.clearIndex - 1 - log.offset));
316
435
  log.offset += removed.length;
317
- for (const row of removed) {
318
- if (byEventId.get(row.id) === row) byEventId.delete(row.id);
319
- releaseSnapshotPins(sessionId, row.index);
320
- }
436
+ for (const row of removed) if (log.byEventId.get(row.id) === row) log.byEventId.delete(row.id);
321
437
  const done = log.offset === log.clearIndex - 1;
322
438
  if (done) dropCheckpointsBelow(sessionId, log.clearIndex);
323
439
  return {
@@ -325,6 +441,43 @@ function memory(options = {}) {
325
441
  done
326
442
  };
327
443
  },
444
+ async wakeLog({ sessionId, consumer }) {
445
+ const { log } = consumer;
446
+ const key = cursorKey(consumer.name, log.lane, 0);
447
+ const head = headOf(sessions.get(sessionId));
448
+ let cursor = cursors.get(sessionId)?.get(key);
449
+ if (!cursor) {
450
+ const marker = clearIndexOf(sessionId);
451
+ if (head === marker) return {
452
+ cursor: idleLogCursor(consumer.name, log.lane, marker),
453
+ head
454
+ };
455
+ const position = log.backfill ? marker : head;
456
+ cursor = {
457
+ consumer: consumer.name,
458
+ lane: log.lane,
459
+ kind: "log",
460
+ position,
461
+ head: log.backfill ? nextLogHead(sessionId, position, log.types) : null,
462
+ dispatch: freshDispatch(),
463
+ queue: [],
464
+ queued: 0,
465
+ completions: /* @__PURE__ */ new Map(),
466
+ types: log.types
467
+ };
468
+ cursorsOf(sessionId).set(key, cursor);
469
+ } else {
470
+ cursor.types = log.types;
471
+ cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
472
+ }
473
+ return {
474
+ cursor: toCursor(cursor),
475
+ head
476
+ };
477
+ },
478
+ async cursors(sessionId) {
479
+ return [...cursors.get(sessionId)?.values() ?? []].filter((cursor) => cursor.kind !== "event" || cursor.head !== null).map(toCursor).toSorted(byCursor);
480
+ },
328
481
  async read(sessionId, opts) {
329
482
  const log = sessions.get(sessionId);
330
483
  if (!log) return [];
@@ -334,41 +487,44 @@ function memory(options = {}) {
334
487
  if (through <= after) return [];
335
488
  return log.rows.slice(after - log.offset, through - log.offset).map(toStored);
336
489
  },
337
- async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes, maxFailures }) {
490
+ async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, exclude, consumers, maxFailures, through }) {
491
+ refreshLogTypes(sessionId, consumers);
338
492
  const claimedAt = clock.now();
339
493
  const now = claimedAt.getTime();
340
494
  const expiresAt = new Date(expiresAtMs ?? now + ttlMs);
341
- const excluded = new Set(excludeIndexes ?? []);
495
+ const excluded = new Set((exclude ?? []).map((ref) => cursorKey(ref.consumer, ref.lane, ref.index)));
496
+ const served = servedBy(consumers);
342
497
  const eligible = [];
343
498
  let retryAt = null;
344
- for (const row of dispatchOf(sessionId).ready.values()) {
345
- if (row.failedAt !== null || excluded.has(row.index)) continue;
346
- if (row.claimExpiresAt && row.claimExpiresAt.getTime() > now) {
347
- if (!retryAt || row.claimExpiresAt < retryAt) retryAt = new Date(row.claimExpiresAt);
499
+ for (const [key, cursor] of cursors.get(sessionId) ?? []) {
500
+ if (cursor.head === null || through !== void 0 && cursor.head > through || cursor.dispatch.failedAt !== null || !served(cursor) || excluded.has(key)) continue;
501
+ const expiry = cursor.dispatch.claimExpiresAt;
502
+ if (expiry && expiry.getTime() > now) {
503
+ if (!retryAt || expiry < retryAt) retryAt = new Date(expiry);
348
504
  continue;
349
505
  }
350
- eligible.push(row);
506
+ eligible.push(cursor);
351
507
  }
352
508
  if (eligible.length === 0) return retryAt ? {
353
509
  outcome: "busy",
354
510
  retryAt
355
511
  } : { outcome: "settled" };
356
- eligible.sort((a, b) => a.index - b.index);
357
512
  const clearIndex = clearIndexOf(sessionId);
358
513
  return {
359
514
  outcome: "claimed",
360
- events: eligible.map((row) => {
361
- const claimed = claim({
515
+ events: eligible.map((cursor) => {
516
+ const head = claim({
362
517
  sessionId,
363
- row,
518
+ cursor,
364
519
  holder,
365
520
  now: claimedAt,
366
521
  expiresAt,
367
522
  maxFailures
368
523
  });
369
- if (clearIndex > 0) claimed.clearIndex = clearIndex;
370
- return claimed;
371
- })
524
+ if (clearIndex > 0) head.clearIndex = clearIndex;
525
+ return head;
526
+ }).toSorted(byHead),
527
+ head: headOf(sessions.get(sessionId))
372
528
  };
373
529
  },
374
530
  async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
@@ -377,20 +533,23 @@ function memory(options = {}) {
377
533
  const clearIndex = clearIndexOf(sessionId);
378
534
  const renewed = [];
379
535
  const superseded = [];
380
- for (const { index, attempt } of claims.toSorted((a, b) => a.index - b.index)) {
381
- if (index <= clearIndex) {
382
- superseded.push(index);
536
+ const ordered = claims.toSorted((a, b) => a.index - b.index || compareText(a.consumer, b.consumer));
537
+ for (const { attempt, ...ref } of ordered) {
538
+ if (ref.index <= clearIndex) {
539
+ superseded.push(ref);
383
540
  continue;
384
541
  }
385
- const row = rowAt(sessionId, index);
386
- if (!row) continue;
387
- if (row.attemptCount > attempt) {
388
- superseded.push(row.index);
542
+ const cursor = cursorAt(sessionId, ref);
543
+ if (!cursor) continue;
544
+ const dispatch = cursor.dispatch;
545
+ if (cursor.head !== ref.index && cursor.position < ref.index) continue;
546
+ if (cursor.head !== ref.index || dispatch.attemptCount > attempt) {
547
+ superseded.push(ref);
389
548
  continue;
390
549
  }
391
- if (row.processedAt === null && row.failedAt === null && row.claimHolder === holder && row.claimExpiresAt !== null && row.claimExpiresAt > now) {
392
- row.claimExpiresAt = new Date(expiresAt);
393
- renewed.push(row.index);
550
+ if (dispatch.failedAt === null && dispatch.claimHolder === holder && dispatch.claimExpiresAt !== null && dispatch.claimExpiresAt > now) {
551
+ dispatch.claimExpiresAt = new Date(expiresAt);
552
+ renewed.push(ref);
394
553
  }
395
554
  }
396
555
  return {
@@ -398,103 +557,124 @@ function memory(options = {}) {
398
557
  superseded
399
558
  };
400
559
  },
401
- async completeAttempt({ sessionId, index, attempt, events }) {
560
+ async completeAttempt({ sessionId, consumer, lane, index, attempt, events, consumers }) {
402
561
  if (index <= clearIndexOf(sessionId)) return { outcome: "superseded" };
403
- const parent = find(sessionId, index);
404
- const cause = {
405
- index,
406
- attempt
407
- };
408
- if (parent.processedAt !== null) {
409
- if (parent.processedByAttempt !== attempt) return { outcome: "superseded" };
562
+ const cursor = cursorAt(sessionId, {
563
+ consumer,
564
+ lane,
565
+ index
566
+ });
567
+ if (!cursor) {
568
+ if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
569
+ return { outcome: "superseded" };
570
+ }
571
+ refreshLogTypes(sessionId, consumers);
572
+ if (cursor.head !== index) {
573
+ const completion = cursor.completions.get(index);
574
+ if (!completion || completion.attempt !== attempt) return { outcome: "superseded" };
410
575
  const requestedIds = events.map((event) => event.id);
411
- if (parent.returnedEventIds === null || parent.returnedEventIds.length !== requestedIds.length || parent.returnedEventIds.some((id, offset) => id !== requestedIds[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
576
+ if (completion.returnedEventIds.length !== requestedIds.length || completion.returnedEventIds.some((id, offset) => id !== requestedIds[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
412
577
  const existing = checkBatchIds(sessionId, events);
413
- if (existing.length !== events.length || existing.some((row) => row.cause?.index !== index || row.cause.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
578
+ if (existing.length !== events.length || existing.some((row) => row.cause?.consumer !== consumer || row.cause.lane !== lane || row.cause.index !== index || row.cause.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
414
579
  return {
415
580
  outcome: "completed",
416
- events: idempotentReplay(events, existing.map(toStored))
581
+ events: events.length === 0 ? [] : idempotentReplay(events, existing.map(toStored))
417
582
  };
418
583
  }
419
- if (parent.attemptCount !== attempt || parent.failedAt !== null || parent.claimHolder === null) return { outcome: "superseded" };
584
+ const dispatch = cursor.dispatch;
585
+ if (dispatch.attemptCount !== attempt || dispatch.failedAt !== null || dispatch.claimHolder === null) return { outcome: "superseded" };
420
586
  if (checkBatchIds(sessionId, events).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains already-appended ids");
421
- const now = clock.now();
422
- const inserted = insert(sessionId, events, now, cause);
423
- parent.processedAt = new Date(now);
424
- parent.processedByAttempt = attempt;
425
- parent.returnedEventIds = events.map((event) => event.id);
426
- parent.claimHolder = null;
427
- parent.claimExpiresAt = null;
428
- settle(sessionId, parent);
429
- releaseSnapshotPins(sessionId, index);
587
+ const inserted = insert(sessionId, events, clock.now(), consumers, {
588
+ consumer,
589
+ lane,
590
+ index,
591
+ attempt
592
+ });
593
+ if (cursor.kind === "log") cursor.completions.clear();
594
+ cursor.completions.set(index, {
595
+ attempt,
596
+ returnedEventIds: events.map((event) => event.id)
597
+ });
598
+ advance(sessionId, cursor);
430
599
  notifyStreams(sessionId, inserted);
431
600
  return {
432
601
  outcome: "completed",
433
602
  events: inserted.map(toStored)
434
603
  };
435
604
  },
436
- async failAttempt({ sessionId, index, attempt, error, maxFailures, retryAfterMs }) {
605
+ async failAttempt({ sessionId, consumer, lane, index, attempt, error, maxFailures, retryAfterMs }) {
437
606
  if (index <= clearIndexOf(sessionId)) return {
438
607
  outcome: "superseded",
439
608
  failureCount: 0
440
609
  };
441
- const row = find(sessionId, index);
442
- if (row.processedAt || row.attemptCount !== attempt || row.claimHolder === null && row.lastFailedAttempt !== attempt) return {
610
+ const cursor = cursorAt(sessionId, {
611
+ consumer,
612
+ lane,
613
+ index
614
+ });
615
+ if (!cursor) {
616
+ if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
617
+ return {
618
+ outcome: "superseded",
619
+ failureCount: 0
620
+ };
621
+ }
622
+ const dispatch = cursor.dispatch;
623
+ if (cursor.head !== index || dispatch.attemptCount !== attempt || dispatch.claimHolder === null && dispatch.lastFailedAttempt !== attempt) return {
443
624
  outcome: "superseded",
444
- failureCount: row.failureCount
625
+ failureCount: cursor.head === index ? dispatch.failureCount : 0
445
626
  };
446
- if (row.lastFailedAttempt === attempt && row.claimHolder === null) return {
447
- outcome: row.failedAt ? "dead_lettered" : "failed",
448
- failureCount: row.failureCount
627
+ if (dispatch.lastFailedAttempt === attempt && dispatch.claimHolder === null) return {
628
+ outcome: dispatch.failedAt ? "dead_lettered" : "failed",
629
+ failureCount: dispatch.failureCount
449
630
  };
450
- if (row.failedAt) return {
631
+ if (dispatch.failedAt) return {
451
632
  outcome: "dead_lettered",
452
- failureCount: row.failureCount
633
+ failureCount: dispatch.failureCount
453
634
  };
454
- row.failureCount += 1;
455
- row.lastError = error;
635
+ dispatch.failureCount += 1;
636
+ dispatch.lastError = error;
456
637
  const failedAt = clock.now();
457
- row.lastFailedAt = new Date(failedAt);
458
- row.lastFailedAttempt = attempt;
459
- row.claimHolder = null;
460
- row.claimExpiresAt = retryAfterMs === void 0 || row.failureCount >= maxFailures ? null : new Date(failedAt.getTime() + retryAfterMs);
461
- if (row.failureCount >= maxFailures) {
462
- row.failedAt = new Date(failedAt);
463
- dispatchOf(sessionId).ready.delete(row.index);
464
- releaseSnapshotPins(sessionId, index);
638
+ dispatch.lastFailedAt = new Date(failedAt);
639
+ dispatch.lastFailedAttempt = attempt;
640
+ dispatch.claimHolder = null;
641
+ dispatch.claimExpiresAt = retryAfterMs === void 0 || dispatch.failureCount >= maxFailures ? null : new Date(failedAt.getTime() + retryAfterMs);
642
+ if (dispatch.failureCount >= maxFailures) {
643
+ deadLetter(sessionId, cursor, failedAt);
465
644
  return {
466
645
  outcome: "dead_lettered",
467
- failureCount: row.failureCount
646
+ failureCount: dispatch.failureCount
468
647
  };
469
648
  }
470
649
  return {
471
650
  outcome: "failed",
472
- failureCount: row.failureCount
651
+ failureCount: dispatch.failureCount
473
652
  };
474
653
  },
475
654
  async deadLetters(sessionId) {
476
- const log = sessions.get(sessionId);
477
- return (log?.rows ?? []).filter((row) => row.index > log.clearIndex && row.processedAt === null && row.failedAt !== null).map(toStored);
655
+ const clearIndex = clearIndexOf(sessionId);
656
+ const dead = [];
657
+ for (const cursor of cursors.get(sessionId)?.values() ?? []) {
658
+ if (cursor.head === null || cursor.head <= clearIndex || cursor.dispatch.failedAt === null) continue;
659
+ dead.push(toHead(cursor, rowAt(sessionId, cursor.head)));
660
+ }
661
+ return dead.toSorted(byHead);
478
662
  },
479
- async redrive({ sessionId, index, action }) {
480
- const row = index > clearIndexOf(sessionId) ? rowAt(sessionId, index) : void 0;
481
- if (!row || row.processedAt !== null || row.failedAt === null) return {
663
+ async redrive({ sessionId, consumer, index, action, consumers }) {
664
+ const cursor = deadHeadOf(sessionId, consumer, index);
665
+ if (!cursor) return {
482
666
  redriven: false,
483
- hasPending: hasPending(sessionId)
667
+ hasPending: hasPending(sessionId, consumers)
484
668
  };
485
- if (action === "skip") {
486
- row.processedAt = clock.now();
487
- settle(sessionId, row);
488
- } else {
489
- row.failedAt = null;
490
- row.failureCount = 0;
491
- const state = dispatchOf(sessionId);
492
- const queue = row.lane === null ? void 0 : state.lanes.get(row.lane);
493
- if (row.lane === null || queue?.rows[queue.head] === row) state.ready.set(row.index, row);
669
+ refreshLogTypes(sessionId, consumers);
670
+ if (action === "skip") advance(sessionId, cursor);
671
+ else {
672
+ cursor.dispatch.failedAt = null;
673
+ cursor.dispatch.failureCount = 0;
494
674
  }
495
675
  return {
496
676
  redriven: true,
497
- hasPending: hasPending(sessionId)
677
+ hasPending: hasPending(sessionId, consumers)
498
678
  };
499
679
  },
500
680
  async readState(sessionId, reducerName, stateOptions) {
@@ -509,10 +689,10 @@ function memory(options = {}) {
509
689
  for (const write of writes.toSorted((a, b) => a.index - b.index)) {
510
690
  if (write.index < clearIndex) continue;
511
691
  const checkpoint = checkpointKey(sessionId, reducerName, write.index);
512
- for (const eventIndex of write.pinEventIndexes ?? []) {
513
- const event = rowAt(sessionId, eventIndex);
514
- if (!event || event.index <= clearIndex || event.processedAt !== null || event.failedAt !== null) continue;
515
- const pinKey = eventPinKey(sessionId, eventIndex);
692
+ for (const pin of write.pins ?? []) {
693
+ const cursor = cursorAt(sessionId, pin);
694
+ if (!cursor || pin.index <= clearIndex || cursor.head !== pin.index || cursor.dispatch.failedAt !== null) continue;
695
+ const pinKey = headPinKey(sessionId, pin);
516
696
  let pinned = snapshotPins.get(pinKey);
517
697
  if (!pinned) {
518
698
  pinned = /* @__PURE__ */ new Set();
@@ -558,14 +738,14 @@ function memory(options = {}) {
558
738
  fields = /* @__PURE__ */ new Map();
559
739
  participants.set(participant, fields);
560
740
  }
561
- const expiresAtMs = clock.now().getTime() + meta.ttlMs;
741
+ const now = clock.now();
742
+ const expiresAtMs = now.getTime() + meta.ttlMs;
562
743
  const applied = nullProtoRecord();
563
744
  let appliedCount = 0;
564
745
  for (const [field, value] of Object.entries(values)) {
565
746
  const existing = fields.get(field);
566
- if (existing && existing.at > meta.at) continue;
567
- if (value === null) fields.delete(field);
568
- else fields.set(field, {
747
+ if (existing && existing.expiresAt > now && existing.at > meta.at) continue;
748
+ fields.set(field, {
569
749
  value: structuredClone(value),
570
750
  seen: meta.seen,
571
751
  at: new Date(meta.at),
@@ -598,6 +778,7 @@ function memory(options = {}) {
598
778
  fields.delete(field);
599
779
  continue;
600
780
  }
781
+ if (entry.value === null) continue;
601
782
  rows.push({
602
783
  participant,
603
784
  field,