experimental-a2 0.20.0 → 0.21.1

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 (286) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
  4. package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
  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-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
  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-CvEnqVfo.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
  17. package/dist/ai-CzE672Q3.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 +1 -1
  21. package/dist/ai-fx.js +3 -6
  22. package/dist/ai-fx.js.map +1 -1
  23. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
  24. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-Bfk-Lycn.d.ts.map} +1 -1
  25. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  26. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  27. package/dist/ai-pi.d.ts +1 -1
  28. package/dist/ai-pi.js +3 -3
  29. package/dist/ai-pi.js.map +1 -1
  30. package/dist/ai-react.d.ts +5 -5
  31. package/dist/ai-react.js +2 -2
  32. package/dist/ai-server.d.ts +14 -17
  33. package/dist/ai-server.d.ts.map +1 -1
  34. package/dist/ai-server.js +80 -73
  35. package/dist/ai-server.js.map +1 -1
  36. package/dist/ai.d.ts +2 -2
  37. package/dist/ai.js +2 -2
  38. package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
  39. package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
  40. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  41. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  42. package/dist/client.d.ts +3 -3
  43. package/dist/client.js +1 -1
  44. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  45. package/dist/contract-CWukt01d.js.map +1 -0
  46. package/dist/index.d.ts +5 -5
  47. package/dist/index.js +3 -3
  48. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  49. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  50. package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
  51. package/dist/internal-Bc4PvfHk.js.map +1 -0
  52. package/dist/otel.d.ts +1 -1
  53. package/dist/otel.js +1 -1
  54. package/dist/otel.js.map +1 -1
  55. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  56. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  57. package/dist/react.d.ts +1 -1
  58. package/dist/react.js +1 -1
  59. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  60. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  61. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  62. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  63. package/dist/runtime-consumer-BWmht60z.js +12 -0
  64. package/dist/runtime-consumer-BWmht60z.js.map +1 -0
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +2 -2
  69. package/dist/scheduler-vercel.js +3 -3
  70. package/dist/scheduler-vercel.js.map +1 -1
  71. package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
  72. package/dist/server-BshFZ0eL.d.ts.map +1 -0
  73. package/dist/{server-qyb5aJJG.js → server-DvIB8kj5.js} +387 -113
  74. package/dist/server-DvIB8kj5.js.map +1 -0
  75. package/dist/server.browser.js +1 -1
  76. package/dist/server.browser.js.map +1 -1
  77. package/dist/server.d.ts +4 -4
  78. package/dist/server.js +2 -2
  79. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
  80. package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
  81. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  82. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  83. package/dist/store-N8PXxDAS.js.map +1 -1
  84. package/dist/store-Z2qjw50n.d.ts +523 -0
  85. package/dist/store-Z2qjw50n.d.ts.map +1 -0
  86. package/dist/store-codec-DwrWlgQx.js +65 -0
  87. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  88. package/dist/store-memory.d.ts +1 -1
  89. package/dist/store-memory.d.ts.map +1 -1
  90. package/dist/store-memory.js +393 -210
  91. package/dist/store-memory.js.map +1 -1
  92. package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
  93. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
  94. package/dist/store-postgres.d.ts +1 -1
  95. package/dist/store-postgres.d.ts.map +1 -1
  96. package/dist/store-postgres.js +1586 -566
  97. package/dist/store-postgres.js.map +1 -1
  98. package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
  99. package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
  100. package/dist/store-redis-http.d.ts +1 -1
  101. package/dist/store-redis-http.js +4 -4
  102. package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
  103. package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
  104. package/dist/store-redis.d.ts +1 -1
  105. package/dist/store-redis.js +2 -2
  106. package/dist/store-sqlite.d.ts +1 -1
  107. package/dist/store-sqlite.d.ts.map +1 -1
  108. package/dist/store-sqlite.js +517 -287
  109. package/dist/store-sqlite.js.map +1 -1
  110. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  111. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  112. package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
  113. package/dist/wire-CZGqNZRx.js.map +1 -0
  114. package/docs/01-quickstart.mdx +21 -15
  115. package/docs/02-examples.mdx +1 -1
  116. package/docs/actors/01-introduction.mdx +8 -0
  117. package/docs/actors/02-concurrency.mdx +1 -1
  118. package/docs/agents/01-introduction.mdx +1 -1
  119. package/docs/agents/02-tools.mdx +12 -11
  120. package/docs/agents/03-conversations.mdx +30 -20
  121. package/docs/agents/04-harnesses.mdx +7 -3
  122. package/docs/client/03-presence.mdx +18 -14
  123. package/docs/contract/01-events.mdx +2 -2
  124. package/docs/contract/02-state.mdx +18 -14
  125. package/docs/contract/03-sessions.mdx +32 -25
  126. package/docs/index.mdx +29 -24
  127. package/docs/reference/01-core.mdx +4 -4
  128. package/docs/reference/02-server.mdx +245 -190
  129. package/docs/reference/04-ai.mdx +20 -19
  130. package/docs/reference/05-telemetry.mdx +27 -13
  131. package/docs/reference/06-errors.mdx +24 -18
  132. package/docs/reference/meta.ts +1 -1
  133. package/docs/schedulers/01-overview.mdx +8 -8
  134. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  135. package/docs/schedulers/03-qstash.mdx +4 -4
  136. package/docs/server/01-consumers.mdx +415 -0
  137. package/docs/server/02-timers.mdx +31 -21
  138. package/docs/server/03-cancellation.mdx +52 -44
  139. package/docs/server/04-routes.mdx +4 -1
  140. package/docs/server/05-sql-projections.mdx +391 -0
  141. package/docs/server/06-guarantees.mdx +55 -48
  142. package/docs/stores/01-overview.mdx +3 -3
  143. package/docs/stores/02-redis.mdx +11 -3
  144. package/docs/stores/03-redis-http.mdx +2 -2
  145. package/docs/stores/04-postgres.mdx +65 -14
  146. package/docs/stores/05-sqlite.mdx +5 -3
  147. package/docs/upgrades/01-overview.mdx +30 -0
  148. package/docs/upgrades/79-0-21.mdx +165 -0
  149. package/docs/upgrades/80-0-20.mdx +43 -0
  150. package/docs/upgrades/81-0-19.mdx +33 -0
  151. package/docs/upgrades/82-0-18.mdx +33 -0
  152. package/docs/upgrades/83-0-17.mdx +69 -0
  153. package/docs/upgrades/meta.ts +3 -0
  154. package/examples/playground/DEPLOY.md +39 -7
  155. package/examples/playground/app/a0/chat.test.tsx +2 -2
  156. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  157. package/examples/playground/app/a0/server.ts +4 -4
  158. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  159. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  160. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  161. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  162. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  163. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  164. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  165. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  166. package/examples/playground/app/a2slack/db/a2.ts +206 -0
  167. package/examples/playground/app/a2slack/db/index.ts +73 -0
  168. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  169. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  170. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  171. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  172. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  173. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  174. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  175. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  176. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
  177. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  178. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  179. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  180. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  181. package/examples/playground/app/a2slack/events/route.ts +36 -0
  182. package/examples/playground/app/a2slack/identity.ts +44 -0
  183. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  184. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  185. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  186. package/examples/playground/app/a2slack/lag.ts +73 -0
  187. package/examples/playground/app/a2slack/layout.tsx +43 -0
  188. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  189. package/examples/playground/app/a2slack/model.test.ts +208 -0
  190. package/examples/playground/app/a2slack/model.ts +369 -0
  191. package/examples/playground/app/a2slack/models.test.ts +72 -0
  192. package/examples/playground/app/a2slack/page.tsx +10 -0
  193. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  194. package/examples/playground/app/a2slack/search/route.ts +40 -0
  195. package/examples/playground/app/a2slack/search-index.ts +108 -0
  196. package/examples/playground/app/a2slack/server.test.ts +315 -0
  197. package/examples/playground/app/a2slack/server.ts +55 -0
  198. package/examples/playground/app/a2slack/sessions.ts +50 -0
  199. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  200. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  201. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  202. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  203. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  204. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  205. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  206. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  207. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  208. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  209. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  210. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  211. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  212. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  213. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  214. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  215. package/examples/playground/app/agent/server.ts +3 -3
  216. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  217. package/examples/playground/app/chat/server.ts +45 -35
  218. package/examples/playground/app/components/demo-icons.tsx +7 -0
  219. package/examples/playground/app/components/demos.tsx +7 -0
  220. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  221. package/examples/playground/app/orders/page.tsx +1 -1
  222. package/examples/playground/app/orders/server.ts +49 -30
  223. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  224. package/examples/playground/app/recovery/page.tsx +4 -4
  225. package/examples/playground/app/recovery/server.ts +23 -32
  226. package/examples/playground/drizzle.config.ts +14 -0
  227. package/examples/playground/next.config.ts +6 -1
  228. package/examples/playground/package.json +12 -1
  229. package/examples/playground/scripts/migrate.mjs +31 -0
  230. package/examples/playground/vercel.json +9 -0
  231. package/examples/playground/vitest.drills.config.ts +32 -0
  232. package/package.json +5 -2
  233. package/sql/postgres/drizzle-schema.ts +206 -0
  234. package/sql/postgres/schema.sql +206 -67
  235. package/src/actor.ts +101 -71
  236. package/src/ai-agent.ts +149 -111
  237. package/src/ai-control-server.ts +8 -8
  238. package/src/ai-fx.ts +4 -6
  239. package/src/ai-harness.ts +10 -2
  240. package/src/ai-pi.ts +2 -2
  241. package/src/ai-server.ts +2 -2
  242. package/src/ai.ts +7 -7
  243. package/src/contract.ts +1 -1
  244. package/src/deterministic-id.ts +26 -3
  245. package/src/errors.ts +8 -5
  246. package/src/idempotent-replay.ts +0 -1
  247. package/src/index.ts +1 -1
  248. package/src/internal.ts +15 -5
  249. package/src/otel.ts +1 -1
  250. package/src/postgres-schema.ts +98 -57
  251. package/src/push-envelope.ts +20 -0
  252. package/src/runtime-consumer.ts +10 -0
  253. package/src/scheduler-task.ts +2 -2
  254. package/src/scheduler-vercel.ts +1 -1
  255. package/src/server.browser.ts +1 -1
  256. package/src/server.ts +774 -207
  257. package/src/session-socket.ts +1 -1
  258. package/src/store-codec.ts +66 -0
  259. package/src/store-memory.ts +581 -255
  260. package/src/store-postgres.ts +1984 -825
  261. package/src/store-redis-core.ts +1624 -1152
  262. package/src/store-redis-notify.ts +2 -0
  263. package/src/store-sqlite.ts +891 -486
  264. package/src/store.ts +227 -97
  265. package/src/telemetry.ts +2 -1
  266. package/src/wire.ts +1 -0
  267. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  268. package/dist/ai-mMiWul1T.js.map +0 -1
  269. package/dist/contract-BYKASlCV.js.map +0 -1
  270. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  271. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  272. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  273. package/dist/internal-CscSY7K_.js.map +0 -1
  274. package/dist/server-DYAATFuT.d.ts.map +0 -1
  275. package/dist/server-qyb5aJJG.js.map +0 -1
  276. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  277. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  278. package/dist/store-CyLUy86v.d.ts +0 -404
  279. package/dist/store-CyLUy86v.d.ts.map +0 -1
  280. package/dist/store-codec-DTG0Ftek.js +0 -10
  281. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  282. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  283. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  284. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  285. package/dist/wire-BKxH1vbP.js.map +0 -1
  286. package/docs/server/01-handlers.mdx +0 -227
@@ -1,12 +1,12 @@
1
- import { a as NonRetriableError, i as A2Error, n as isSessionCleared, o as asStoreUnavailable, r as sessionClearedError } from "./session-clear-DGI4jTQ5.js";
2
- import { n as foldEvent, s as validateSync, t as cloneInitial } from "./reducer-BAXc8JyF.js";
1
+ import { a as NonRetriableError, i as A2Error, n as isSessionCleared, o as asStoreUnavailable, r as sessionClearedError } from "./session-clear-wHd5GCpZ.js";
2
+ import { n as foldEvent, s as validateSync, t as cloneInitial } from "./reducer-CjmvZSAb.js";
3
3
  import { n as encodeState, t as decodeState } from "./reducer-runtime-DQwz_J59.js";
4
- import { _ as nullProtoRecord, c as STATE_CACHE, g as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, m as failedPreviousAttempt, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as serverInternals, y as stateTailReaders } from "./internal-CscSY7K_.js";
4
+ import { _ as nullProtoRecord, b as serverInternals, c as STATE_CACHE, g as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, m as failedPreviousAttempt, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as runtimeConsumerFactory, x as stateTailReaders, y as runtimeConsumers } from "./internal-Bc4PvfHk.js";
5
5
  import { t as boundedCache } from "./bounded-cache-CPHWF-8h.js";
6
- import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-7WJV0YWP.js";
6
+ import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-DYNyxCev.js";
7
7
  import { i as takeBuffered, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-Cx7AAPGq.js";
8
8
  import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
9
- import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-BKxH1vbP.js";
9
+ import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-CZGqNZRx.js";
10
10
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
11
11
  //#region src/push-envelope.ts
12
12
  /**
@@ -34,6 +34,10 @@ function parsePresenceSibling(value) {
34
34
  if (at !== void 0) out.at = at;
35
35
  return out;
36
36
  }
37
+ /** The longest event id a push may carry: ids are indexed, and Postgres's index rows have a limit. */
38
+ const MAX_PUSHED_ID = 256;
39
+ /** A UUIDv5, the shape of the ids A2 derives (deterministic-id.ts). */
40
+ const DERIVED_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
37
41
  /** The events half of the envelope, shared by the POST body and socket
38
42
  * `push` frames so INVALID_PAYLOAD shapes have one implementation. */
39
43
  function parsePushEvents(events) {
@@ -43,6 +47,8 @@ function parsePushEvents(events) {
43
47
  const { type, payload, id } = event;
44
48
  if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
45
49
  if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
50
+ if (id !== void 0 && (id.length === 0 || id.length > MAX_PUSHED_ID)) throw invalid(`events[${i}].id must be 1-${MAX_PUSHED_ID} characters when present`);
51
+ if (id !== void 0 && DERIVED_ID.test(id)) throw invalid(`events[${i}].id is shaped like the ids A2 derives for consumers (a UUIDv5); use crypto.randomUUID() or idempotentId()`);
46
52
  const out = {
47
53
  type,
48
54
  payload
@@ -982,7 +988,7 @@ function createServerFetch(server) {
982
988
  //#endregion
983
989
  //#region src/deterministic-id.ts
984
990
  /**
985
- * Deterministic default ids for handler `session.append` calls. A handler
991
+ * Deterministic default ids for consumer `session.append` calls. A consumer
986
992
  * re-run after a crash calls `session.append` again; giving
987
993
  * each call a deterministic id — derived from the triggering event's id
988
994
  * and the caller's stable name — makes the re-run a no-op via the store's
@@ -1008,20 +1014,27 @@ async function deterministicIdFromInput(input) {
1008
1014
  async function deterministicId(parts) {
1009
1015
  return deterministicIdFromInput(parts.join("\0"));
1010
1016
  }
1011
- async function deterministicEventId(triggerEventId, name, itemOrdinal) {
1017
+ /**
1018
+ * `consumer` keys the id on the consumer that appends, so two consumers of
1019
+ * one event never collide. A2's own consumers pass none: their types are
1020
+ * disjoint, so the event alone keys them.
1021
+ */
1022
+ async function deterministicEventId(triggerEventId, name, itemOrdinal, consumer) {
1012
1023
  if (name.length === 0) throw new TypeError("append name must not be empty");
1013
1024
  return deterministicId([
1014
1025
  APPEND_NAMESPACE,
1015
1026
  triggerEventId,
1016
1027
  name,
1017
- itemOrdinal
1028
+ itemOrdinal,
1029
+ ...consumer === void 0 ? [] : [consumer]
1018
1030
  ]);
1019
1031
  }
1020
- async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
1032
+ async function deterministicReturnedEventId(triggerEventId, itemOrdinal, consumer) {
1021
1033
  return deterministicId([
1022
1034
  RETURN_NAMESPACE,
1023
1035
  triggerEventId,
1024
- itemOrdinal
1036
+ itemOrdinal,
1037
+ ...consumer === void 0 ? [] : [consumer]
1025
1038
  ]);
1026
1039
  }
1027
1040
  /**
@@ -1030,17 +1043,18 @@ async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
1030
1043
  * after a clear schedules a distinct task while sessions never cleared keep
1031
1044
  * the ids they always had.
1032
1045
  */
1033
- async function deterministicScheduleId(contract, sessionId, triggerEventId, name, clearIndex = 0) {
1046
+ async function deterministicScheduleId(contract, sessionId, triggerEventId, name, clearIndex = 0, consumer) {
1034
1047
  return deterministicIdFromInput(JSON.stringify([
1035
1048
  SCHEDULE_NAMESPACE,
1036
1049
  contract,
1037
1050
  sessionId,
1038
1051
  triggerEventId,
1039
1052
  name,
1040
- ...clearIndex > 0 ? [clearIndex] : []
1053
+ ...clearIndex > 0 ? [clearIndex] : [],
1054
+ ...consumer === void 0 ? [] : [{ consumer }]
1041
1055
  ]));
1042
1056
  }
1043
- async function deterministicScheduleClearId({ contract, sessionId, triggerEventId, clearIndex, name, ifLastIndex }) {
1057
+ async function deterministicScheduleClearId({ contract, sessionId, triggerEventId, clearIndex, name, ifLastIndex, consumer }) {
1044
1058
  return deterministicIdFromInput(JSON.stringify([
1045
1059
  SCHEDULE_CLEAR_NAMESPACE,
1046
1060
  contract,
@@ -1048,7 +1062,8 @@ async function deterministicScheduleClearId({ contract, sessionId, triggerEventI
1048
1062
  triggerEventId,
1049
1063
  clearIndex,
1050
1064
  name,
1051
- ifLastIndex
1065
+ ifLastIndex,
1066
+ ...consumer === void 0 ? [] : [{ consumer }]
1052
1067
  ]));
1053
1068
  }
1054
1069
  async function deterministicScheduledEventId(scheduleId, itemOrdinal) {
@@ -1101,13 +1116,76 @@ async function deliverSchedulerClear(server, task) {
1101
1116
  if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled clears`);
1102
1117
  await internals.schedulerClear(task.sessionId, task.ifLastIndex);
1103
1118
  }
1104
- function handler(entry) {
1105
- return typeof entry === "function" ? { handler: entry } : entry;
1119
+ const CONSUMER_NAME = /^[a-z0-9][a-z0-9._:-]{0,127}$/i;
1120
+ /** Define a consumer: a named reader of each session's log. */
1121
+ function consumer(options) {
1122
+ return defineConsumer(options, false);
1123
+ }
1124
+ /**
1125
+ * `consumer()`, and for A2's own runtimes (`runtime`) a name under `a2.` and
1126
+ * child ids without the name: their types are disjoint, so the event alone
1127
+ * keys them, and regrouping A2's consumers never changes an id.
1128
+ */
1129
+ function defineConsumer(options, runtime) {
1130
+ const contract = options?.contract;
1131
+ if (!contract || typeof contract.name !== "string") throw new TypeError("consumer expects options with a contract");
1132
+ const consumerName = options.name;
1133
+ if (typeof consumerName !== "string" || !CONSUMER_NAME.test(consumerName)) throw new TypeError("consumer name must be 1-128 letters, digits, and . _ : - characters");
1134
+ if (consumerName.startsWith("a2.") !== runtime) throw new TypeError(runtime ? `A2's own consumers are named under 'a2.'` : `consumer names starting with 'a2.' are reserved`);
1135
+ const types = options.types === void 0 ? null : [...options.types];
1136
+ if (types?.length === 0) throw new TypeError(`consumer '${consumerName}' lists no types; leave types out to take every type`);
1137
+ for (const type of types ?? []) if (!Object.hasOwn(contract.events, type)) throw new TypeError(`contract '${contract.name}' has no event type '${String(type)}'`);
1138
+ const lane = options.lane ?? null;
1139
+ if (lane !== null && typeof lane !== "function" && (typeof lane !== "string" || lane.length === 0)) throw new TypeError(`lane for consumer '${consumerName}' must be a non-empty string or a function`);
1140
+ const backfill = options.backfill ?? false;
1141
+ if (backfill && typeof lane !== "string") throw new TypeError(`consumer '${consumerName}' backfills, so its lane must be a string`);
1142
+ if (backfill && options.abortOn !== void 0) throw new TypeError(`consumer '${consumerName}' backfills, so it can't use abortOn`);
1143
+ return { run(fn) {
1144
+ if (typeof fn !== "function") throw new TypeError(`consumer '${consumerName}' needs a run function`);
1145
+ const defined = {
1146
+ name: consumerName,
1147
+ contract: { name: contract.name },
1148
+ spec: {
1149
+ types,
1150
+ lane,
1151
+ backfill,
1152
+ abortOn: options.abortOn ?? null,
1153
+ run: fn
1154
+ }
1155
+ };
1156
+ if (runtime) runtimeConsumers.add(defined);
1157
+ return defined;
1158
+ } };
1106
1159
  }
1160
+ runtimeConsumerFactory.define = (options) => defineConsumer(options, true);
1161
+ /** A registered consumer as the store needs to know it. */
1162
+ const storeConsumerOf = (registered) => registered.log ? {
1163
+ name: registered.name,
1164
+ log: registered.log
1165
+ } : { name: registered.name };
1107
1166
  const MAX_FAILURES = 10;
1167
+ /** How long `consume` waits before looking again at a head it can't run. */
1168
+ const CONSUME_WAIT_MIN_MS = 25;
1169
+ const CONSUME_WAIT_MAX_MS = 1e3;
1170
+ /**
1171
+ * `task`, or a rejection with `signal`'s reason once it aborts. The task runs
1172
+ * on either way, and its own failure is caught, so nothing goes unhandled.
1173
+ */
1174
+ const untilAborted = (task, signal) => {
1175
+ if (!signal) return task;
1176
+ task.catch(() => {});
1177
+ if (signal.aborted) return Promise.reject(signal.reason);
1178
+ return new Promise((resolve, reject) => {
1179
+ const abort = () => reject(signal.reason);
1180
+ signal.addEventListener("abort", abort, { once: true });
1181
+ task.then(resolve, reject).finally(() => {
1182
+ signal.removeEventListener("abort", abort);
1183
+ });
1184
+ });
1185
+ };
1108
1186
  /** How long an event waits after its `failureCount`th failed attempt. */
1109
1187
  const failureRetryDelayMs = (failureCount) => Math.min(DRAIN_TIMINGS.failureRetryBaseMs * 2 ** Math.max(failureCount - 1, 0), DRAIN_TIMINGS.failureRetryMaxMs);
1110
- /** Abort a lost attempt's handler and release its drain slot. */
1188
+ /** Abort a lost attempt's consumer and release its drain slot. */
1111
1189
  function endSuperseded(entry, message) {
1112
1190
  entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", message));
1113
1191
  entry.fence({
@@ -1242,21 +1320,35 @@ function foldPresenceRows(rows) {
1242
1320
  };
1243
1321
  return map;
1244
1322
  }
1245
- const addSnapshotWrite = (writes, index, state, pinEventIndex) => {
1323
+ /** One cursor head, as a map key. */
1324
+ const headKey = (ref) => JSON.stringify([
1325
+ ref.consumer,
1326
+ ref.lane,
1327
+ ref.index
1328
+ ]);
1329
+ const headRefOf = (head) => ({
1330
+ consumer: head.consumer,
1331
+ lane: head.lane,
1332
+ index: head.index
1333
+ });
1334
+ const addSnapshotWrite = (writes, index, state, pin) => {
1246
1335
  let write = writes.get(index);
1247
1336
  if (!write) {
1248
1337
  write = {
1249
1338
  state: cloneInitial(state),
1250
- pinEventIndexes: /* @__PURE__ */ new Set()
1339
+ pins: /* @__PURE__ */ new Map()
1251
1340
  };
1252
1341
  writes.set(index, write);
1253
1342
  }
1254
- if (pinEventIndex !== void 0) write.pinEventIndexes.add(pinEventIndex);
1343
+ if (pin !== void 0) write.pins.set(headKey(pin), pin);
1255
1344
  };
1345
+ /** Whether a registered consumer runs events of this type. */
1346
+ const takes = (registered, type) => registered.types === null || registered.types.has(type);
1256
1347
  /** Implement a contract: bind its vocabulary to storage and reactions. */
1257
1348
  function createServer(options) {
1258
1349
  const serverContract = options?.contract;
1259
1350
  if (serverContract === null || typeof serverContract !== "object" || typeof serverContract.name !== "string" || serverContract.events === null) throw new TypeError("createServer expects options with a contract (see a2.contract)");
1351
+ if (Object.hasOwn(options, "handlers")) throw new TypeError("createServer no longer takes handlers: write each as a consumer in `consumers` (see https://www.a2.dev/upgrades/0-21)");
1260
1352
  const name = serverContract.name;
1261
1353
  const defs = serverContract.events;
1262
1354
  const presenceDefs = serverContract.presence;
@@ -1292,15 +1384,9 @@ function createServer(options) {
1292
1384
  const resolveStore = retryableLazy(makeStore).get;
1293
1385
  const telemetry = options.telemetry ?? CONSOLE_TELEMETRY;
1294
1386
  const scheduler = options.scheduler;
1295
- const handlers = /* @__PURE__ */ new Map();
1296
- for (const [type, entry] of Object.entries(options.handlers ?? {})) {
1297
- if (entry === void 0) continue;
1298
- if (!Object.hasOwn(defs, type)) throw new TypeError(`contract '${name}' has no event type '${type}' — cannot register a handler for it`);
1299
- const callback = typeof entry === "function" ? entry : entry?.handler;
1300
- if (typeof callback !== "function") throw new TypeError(`handler for '${type}' must be a function`);
1387
+ const abortMatcherOf = (spec) => {
1301
1388
  let abortOn = null;
1302
- const spec = typeof entry === "function" ? void 0 : entry.abortOn;
1303
- if (spec !== void 0) {
1389
+ if (spec !== void 0 && spec !== null) {
1304
1390
  abortOn = /* @__PURE__ */ new Map();
1305
1391
  const pairs = Array.isArray(spec) ? spec.map((abortType) => [abortType, true]) : Object.entries(spec);
1306
1392
  for (const [abortType, matcher] of pairs) {
@@ -1310,14 +1396,70 @@ function createServer(options) {
1310
1396
  }
1311
1397
  if (abortOn.size === 0) abortOn = null;
1312
1398
  }
1313
- const lane = typeof entry === "function" ? null : entry.lane ?? null;
1314
- if (lane !== null && typeof lane !== "function" && (typeof lane !== "string" || lane.length === 0)) throw new TypeError(`lane for '${type}' must be a non-empty string or a function`);
1315
- handlers.set(type, {
1316
- handler: callback,
1317
- abortOn,
1318
- lane
1319
- });
1399
+ return abortOn;
1400
+ };
1401
+ const laneContext = (sessionId, event) => ({
1402
+ sessionId,
1403
+ event: {
1404
+ type: event.type,
1405
+ payload: structuredClone(event.payload),
1406
+ ...event.id === void 0 ? {} : { id: event.id }
1407
+ }
1408
+ });
1409
+ const registeredConsumers = /* @__PURE__ */ new Map();
1410
+ /** A consumer definition as the server runs it. */
1411
+ const registrationOf = (defined) => {
1412
+ const { types, lane, backfill, abortOn, run } = defined.spec;
1413
+ const matcher = abortMatcherOf(abortOn);
1414
+ return {
1415
+ name: defined.name,
1416
+ types: types === null ? null : new Set(types),
1417
+ laneOf: typeof lane === "string" ? null : lane === null ? () => null : (sessionId, event) => {
1418
+ const resolved = lane(laneContext(sessionId, event));
1419
+ if (resolved !== null && (typeof resolved !== "string" || resolved.length === 0)) throw new TypeError(`lane for consumer '${defined.name}' must resolve to a non-empty string or null`);
1420
+ return resolved;
1421
+ },
1422
+ log: typeof lane === "string" ? {
1423
+ lane,
1424
+ types: types === null ? null : [...types],
1425
+ backfill
1426
+ } : void 0,
1427
+ abortOn: () => matcher,
1428
+ run,
1429
+ anonymousIds: runtimeConsumers.has(defined)
1430
+ };
1431
+ };
1432
+ /** The definitions the server runs, so `consume` can tell its own from another. */
1433
+ const definitions = /* @__PURE__ */ new Map();
1434
+ /** A consumer written inline is `consumer()`'s options and `run`, on this contract. */
1435
+ const definitionOf = (entry) => {
1436
+ if (entry === null || typeof entry !== "object" || "spec" in entry || !("run" in entry)) return entry;
1437
+ const { run, ...rest } = entry;
1438
+ return consumer({
1439
+ ...rest,
1440
+ contract: serverContract
1441
+ }).run(run);
1442
+ };
1443
+ for (const entry of options.consumers ?? []) {
1444
+ const defined = definitionOf(entry);
1445
+ if (defined?.contract?.name !== name || !defined.spec) throw new TypeError(`createServer for '${name}' got a consumer of another contract`);
1446
+ if (registeredConsumers.has(defined.name)) throw new TypeError(`two consumers are named '${defined.name}'`);
1447
+ registeredConsumers.set(defined.name, registrationOf(defined));
1448
+ definitions.set(defined.name, defined);
1449
+ }
1450
+ const anonymousTypes = /* @__PURE__ */ new Map();
1451
+ for (const registered of registeredConsumers.values()) {
1452
+ if (!registered.anonymousIds) continue;
1453
+ for (const type of registered.types ?? Object.keys(defs)) {
1454
+ const other = anonymousTypes.get(type);
1455
+ if (other !== void 0) throw new TypeError(`A2's consumers '${other}' and '${registered.name}' both take '${type}'`);
1456
+ anonymousTypes.set(type, registered.name);
1457
+ }
1320
1458
  }
1459
+ /** The consumers this server serves, as the store needs to know them. */
1460
+ const storeConsumers = [...registeredConsumers.values()].map(storeConsumerOf);
1461
+ /** Whether an event starts cursor work for any consumer. */
1462
+ const startsWork = (event) => (event.targets?.length ?? 0) > 0 || [...registeredConsumers.values()].some((registered) => registered.log && takes(registered, event.type));
1321
1463
  const prefix = `${name}${NS}`;
1322
1464
  const nsId = (sessionId) => `${prefix}${sessionId}`;
1323
1465
  const stripNs = (stored) => stored.slice(prefix.length);
@@ -1414,20 +1556,15 @@ function createServer(options) {
1414
1556
  };
1415
1557
  if (e.id !== void 0) validated.id = e.id;
1416
1558
  if (!resolveDispatchMetadata) return validated;
1417
- const registration = handlers.get(e.type);
1418
- if (!registration) validated.settled = true;
1419
- else if (registration.lane !== null) {
1420
- const lane = typeof registration.lane === "function" ? registration.lane({
1421
- sessionId,
1422
- event: {
1423
- type: e.type,
1424
- payload: structuredClone(result.value),
1425
- ...e.id === void 0 ? {} : { id: e.id }
1426
- }
1427
- }) : registration.lane;
1428
- if (typeof lane !== "string" || lane.length === 0) throw new TypeError(`lane for '${e.type}' must resolve to a non-empty string`);
1429
- validated.lane = lane;
1559
+ const targets = [];
1560
+ for (const registered of registeredConsumers.values()) {
1561
+ if (!registered.laneOf || !takes(registered, e.type)) continue;
1562
+ targets.push({
1563
+ consumer: registered.name,
1564
+ lane: registered.laneOf(sessionId, validated)
1565
+ });
1430
1566
  }
1567
+ if (targets.length > 0) validated.targets = targets;
1431
1568
  return validated;
1432
1569
  });
1433
1570
  };
@@ -1455,9 +1592,12 @@ function createServer(options) {
1455
1592
  let rows;
1456
1593
  let shouldHealSession;
1457
1594
  try {
1458
- const result = await store.append(nsId(sessionId), validated, originIndex === void 0 ? void 0 : { originIndex });
1595
+ const result = await store.append(nsId(sessionId), validated, {
1596
+ consumers: storeConsumers,
1597
+ ...originIndex === void 0 ? {} : { originIndex }
1598
+ });
1459
1599
  rows = result.events;
1460
- shouldHealSession = result.hasPending && !(source === "handler" && validated.every((event) => event.settled === true));
1600
+ shouldHealSession = result.hasPending && !(source === "consumer" && !validated.some(startsWork));
1461
1601
  } catch (err) {
1462
1602
  throw asStoreUnavailable(err);
1463
1603
  }
@@ -1566,7 +1706,7 @@ function createServer(options) {
1566
1706
  state: write.state
1567
1707
  })
1568
1708
  };
1569
- if (write.pinEventIndexes.size > 0) snapshot.pinEventIndexes = [...write.pinEventIndexes];
1709
+ if (write.pins.size > 0) snapshot.pins = [...write.pins.values()];
1570
1710
  return snapshot;
1571
1711
  });
1572
1712
  if (snapshots.length > 0) persistSnapshots({
@@ -1635,7 +1775,7 @@ function createServer(options) {
1635
1775
  /**
1636
1776
  * Serves a plan from the cached state when every read is at or past the
1637
1777
  * cached index. A read bounded at the cached index by an event whose id
1638
- * matches the cached one (a handler's own trigger) needs no store request;
1778
+ * matches the cached one (a consumer's own trigger) needs no store request;
1639
1779
  * any other read fetches the tail from the cached event onward and checks
1640
1780
  * that event's id. Returns false when the store must answer instead.
1641
1781
  */
@@ -1665,14 +1805,14 @@ function createServer(options) {
1665
1805
  clearIndex: cached.clearIndex,
1666
1806
  snapshot: "memory",
1667
1807
  visit: (entry, state, index) => {
1668
- if (entry.pinEventIndex !== void 0) addSnapshotWrite(writes, index, state, entry.pinEventIndex);
1808
+ if (entry.pin !== void 0) addSnapshotWrite(writes, index, state, entry.pin);
1669
1809
  }
1670
1810
  });
1671
1811
  const added = Math.max(0, folded.position - 1);
1672
1812
  const rewritesHead = folded.index - cached.persistedIndex >= STATE_CACHE.snapshotEvery;
1673
1813
  if (rewritesHead && !writes.has(folded.index)) writes.set(folded.index, {
1674
1814
  state: folded.state,
1675
- pinEventIndexes: /* @__PURE__ */ new Set()
1815
+ pins: /* @__PURE__ */ new Map()
1676
1816
  });
1677
1817
  const snapshots = writeSnapshots({
1678
1818
  store,
@@ -1746,11 +1886,11 @@ function createServer(options) {
1746
1886
  snapshot: snapshotOutcome,
1747
1887
  visit: (entry, entryState, entryIndex, count) => {
1748
1888
  if (entryIndex === 0) return;
1749
- if (count === 0 && entry.pinEventIndex === void 0) return;
1889
+ if (count === 0 && entry.pin === void 0) return;
1750
1890
  const advancesHead = stateRead.headIndex === null || entryIndex > stateRead.headIndex;
1751
1891
  const repairsHead = snapshotOutcome === "rejected" && entryIndex === stateRead.headIndex;
1752
- if (!advancesHead && !repairsHead && entry.pinEventIndex === void 0) return;
1753
- addSnapshotWrite(writes, entryIndex, entryState, entry.pinEventIndex);
1892
+ if (!advancesHead && !repairsHead && entry.pin === void 0) return;
1893
+ addSnapshotWrite(writes, entryIndex, entryState, entry.pin);
1754
1894
  }
1755
1895
  });
1756
1896
  const snapshots = writeSnapshots({
@@ -1832,6 +1972,8 @@ function createServer(options) {
1832
1972
  };
1833
1973
  const makeSchedule = (sessionId, trigger) => {
1834
1974
  const triggerEventId = trigger?.id ?? null;
1975
+ /** Timers a consumer sets key on its name, except A2's own. */
1976
+ const triggerConsumer = trigger && !registeredConsumers.get(trigger.consumer)?.anonymousIds ? trigger.consumer : void 0;
1835
1977
  const triggerCreatedAt = trigger?.createdAt.getTime();
1836
1978
  const clearIndex = trigger?.clearIndex ?? 0;
1837
1979
  const schedule = async (scheduleName, timing, ...events) => {
@@ -1849,7 +1991,7 @@ function createServer(options) {
1849
1991
  id: event.id
1850
1992
  });
1851
1993
  const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
1852
- const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex);
1994
+ const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex, triggerConsumer);
1853
1995
  const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
1854
1996
  id: event.id ?? await deterministicScheduledEventId(scheduleId, item),
1855
1997
  type: event.type,
@@ -1886,7 +2028,8 @@ function createServer(options) {
1886
2028
  triggerEventId,
1887
2029
  clearIndex,
1888
2030
  name: scheduleName,
1889
- ifLastIndex
2031
+ ifLastIndex,
2032
+ ...triggerConsumer === void 0 ? {} : { consumer: triggerConsumer }
1890
2033
  });
1891
2034
  try {
1892
2035
  await scheduler.schedule({
@@ -2332,6 +2475,7 @@ function createServer(options) {
2332
2475
  }
2333
2476
  return rows.flatMap((row) => row.failedAt === null ? [] : [{
2334
2477
  ...toPublic(row),
2478
+ consumer: row.consumer,
2335
2479
  lane: row.lane,
2336
2480
  attemptCount: row.attemptCount,
2337
2481
  failureCount: row.failureCount,
@@ -2339,14 +2483,18 @@ function createServer(options) {
2339
2483
  failedAt: row.failedAt
2340
2484
  }]);
2341
2485
  };
2342
- const redriveEvent = async (sessionId, index, redriveOptions) => {
2486
+ const redriveEvent = async (sessionId, target, redriveOptions) => {
2487
+ const index = target?.index;
2488
+ const redriven = target?.consumer;
2343
2489
  assertRedriveIndex(index);
2490
+ if (typeof redriven !== "string" || redriven.length === 0) throw new TypeError("redrive requires a consumer name");
2344
2491
  const action = redriveOptions?.action;
2345
2492
  if (action !== "retry" && action !== "skip") throw new TypeError("redrive requires action 'retry' or 'skip'");
2346
2493
  return telemetry.span("a2.redrive", {
2347
2494
  "a2.contract": name,
2348
2495
  "a2.session_id": sessionId,
2349
2496
  "a2.redrive.index": index,
2497
+ "a2.redrive.consumer": redriven,
2350
2498
  "a2.redrive.action": action
2351
2499
  }, async (span) => {
2352
2500
  const store = await redrivingStore("redrive");
@@ -2354,8 +2502,10 @@ function createServer(options) {
2354
2502
  try {
2355
2503
  result = await store.redrive({
2356
2504
  sessionId: nsId(sessionId),
2505
+ consumer: redriven,
2357
2506
  index,
2358
- action
2507
+ action,
2508
+ consumers: storeConsumers
2359
2509
  });
2360
2510
  } catch (err) {
2361
2511
  throw asStoreUnavailable(err);
@@ -2375,9 +2525,100 @@ function createServer(options) {
2375
2525
  return { redriven: result.redriven };
2376
2526
  });
2377
2527
  };
2528
+ /**
2529
+ * `session.consume`: wakes the consumer's cursor, then drains only that
2530
+ * consumer until its head lies past `through`. A head another invocation
2531
+ * holds, or one waiting out a failure, is waited for; a dead-lettered head
2532
+ * throws.
2533
+ */
2534
+ /** Per session, which consumer's running head is waiting in `consume` on which: a cycle never ends. */
2535
+ const consumeWaits = /* @__PURE__ */ new Map();
2536
+ const consumeSession = (sessionId, defined, consumeOptions = {}, trigger = null) => {
2537
+ assertSessionId(sessionId);
2538
+ if (defined?.contract?.name !== name || !defined.spec) throw new TypeError(`session.consume for '${name}' got a consumer of another contract`);
2539
+ const own = definitions.get(defined.name);
2540
+ if (own !== void 0 && own !== defined) throw new TypeError(`this server runs a different consumer named '${defined.name}'`);
2541
+ if (defined.name === trigger?.consumer) throw new TypeError(`consumer '${defined.name}' can't consume itself: its own head is running`);
2542
+ const registered = own ? registeredConsumers.get(defined.name) : registrationOf(defined);
2543
+ const log = registered.log;
2544
+ if (!log) throw new TypeError(`consumer '${defined.name}' needs a string lane to be consumed: only then does it have one position`);
2545
+ const { through: requested, signal } = consumeOptions;
2546
+ if (requested !== void 0) assertConsumeIndex(requested);
2547
+ const waiter = trigger?.consumer;
2548
+ const waits = waiter === void 0 ? void 0 : consumeWaits.get(sessionId) ?? consumeWaits.set(sessionId, /* @__PURE__ */ new Map()).get(sessionId);
2549
+ let cycle = false;
2550
+ if (waiter !== void 0 && waits) {
2551
+ for (let next = defined.name; next !== void 0 && !cycle; next = waits.get(next)) cycle = next === waiter;
2552
+ if (!cycle) waits.set(waiter, defined.name);
2553
+ }
2554
+ const storeConsumer = {
2555
+ name: registered.name,
2556
+ log
2557
+ };
2558
+ const consumed = telemetry.span("a2.consume", {
2559
+ "a2.contract": name,
2560
+ "a2.session_id": sessionId,
2561
+ "a2.consume.consumer": registered.name
2562
+ }, async (span) => {
2563
+ const store = await resolveStore();
2564
+ const ns = nsId(sessionId);
2565
+ let woken = await store.wakeLog({
2566
+ sessionId: ns,
2567
+ consumer: storeConsumer
2568
+ });
2569
+ const through = Math.min(requested ?? woken.head, woken.head);
2570
+ span.setAttribute("a2.consume.through", through);
2571
+ const done = () => woken.cursor.head === null || woken.cursor.head > through;
2572
+ if (cycle && !done()) throw new TypeError(`consumer '${waiter}' can't consume '${defined.name}' through ${through}: '${defined.name}' is waiting on it in session '${sessionId}'`);
2573
+ let drains = 0;
2574
+ let wait = CONSUME_WAIT_MIN_MS;
2575
+ while (!done()) {
2576
+ signal?.throwIfAborted();
2577
+ const { cursor } = woken;
2578
+ if (cursor.failedAt !== null) throw new A2Error("CONSUMER_DEAD_LETTERED", `consumer '${registered.name}' is dead-lettered at event ${cursor.head} in session '${sessionId}': ${cursor.lastError ?? "every attempt ended before it finished"}`);
2579
+ drains += 1;
2580
+ const drained = drainSession(sessionId, {
2581
+ only: registered,
2582
+ through
2583
+ });
2584
+ try {
2585
+ await untilAborted(drained, signal);
2586
+ } catch (error) {
2587
+ if (signal?.aborted) drained.then(() => scheduleDrain(sessionId), () => {});
2588
+ throw error;
2589
+ }
2590
+ const before = cursor;
2591
+ woken = await store.wakeLog({
2592
+ sessionId: ns,
2593
+ consumer: storeConsumer
2594
+ });
2595
+ const moved = woken.cursor.head !== before.head || woken.cursor.attemptCount !== before.attemptCount;
2596
+ if (done() || moved) {
2597
+ wait = CONSUME_WAIT_MIN_MS;
2598
+ continue;
2599
+ }
2600
+ const sleep = defaultSleep(wait);
2601
+ const abort = () => sleep.cancel();
2602
+ signal?.addEventListener("abort", abort, { once: true });
2603
+ await sleep.promise;
2604
+ signal?.removeEventListener("abort", abort);
2605
+ wait = Math.min(wait * 2, CONSUME_WAIT_MAX_MS);
2606
+ }
2607
+ if (drains > 0 || own && woken.cursor.head !== null) scheduleDrain(sessionId);
2608
+ const index = woken.cursor.head === null ? woken.head : woken.cursor.head - 1;
2609
+ span.setAttribute("a2.consume.index", index);
2610
+ span.setAttribute("a2.consume.drains", drains);
2611
+ return { index };
2612
+ });
2613
+ return waiter === void 0 || cycle ? consumed : consumed.finally(() => {
2614
+ waits?.delete(waiter);
2615
+ if (waits?.size === 0) consumeWaits.delete(sessionId);
2616
+ });
2617
+ };
2378
2618
  const makeSession = (id, append, trigger, attempt) => ({
2379
2619
  id,
2380
2620
  append,
2621
+ consume: (defined, consumeOptions) => consumeSession(id, defined, consumeOptions, trigger),
2381
2622
  schedule: makeSchedule(id, trigger),
2382
2623
  clear: (clearOptions) => clearSession(id, clearOptions, attempt),
2383
2624
  history: async (bounds) => (await readHistory(id, bounds)).filter((row) => !isSessionCleared(row)).map(toPublic),
@@ -2388,7 +2629,7 @@ function createServer(options) {
2388
2629
  const bounds = {};
2389
2630
  if (through !== void 0 && through !== "latest") {
2390
2631
  bounds.throughIndex = through;
2391
- if (trigger) bounds.pinEventIndex = trigger.index;
2632
+ if (trigger) bounds.pin = headRefOf(trigger);
2392
2633
  if (through === trigger?.index && !attempt?.signal.aborted) bounds.throughEventId = trigger.id;
2393
2634
  }
2394
2635
  if (trigger) bounds.triggerIndex = trigger.index;
@@ -2404,32 +2645,35 @@ function createServer(options) {
2404
2645
  presence: () => readPresenceMap(id)
2405
2646
  } : {}
2406
2647
  });
2648
+ /** The consumer name a trigger's child ids key on, if any. */
2649
+ const idConsumerOf = (trigger) => registeredConsumers.get(trigger.consumer)?.anonymousIds ? void 0 : trigger.consumer;
2407
2650
  const makeCtx = (trigger, attempt, signal, endAttempt) => {
2408
2651
  const publicSessionId = stripNs(trigger.sessionId);
2652
+ const idConsumer = idConsumerOf(trigger);
2653
+ const cause = {
2654
+ consumer: trigger.consumer,
2655
+ lane: trigger.lane,
2656
+ index: trigger.index,
2657
+ attempt
2658
+ };
2409
2659
  const append = async (appendName, ...events) => {
2410
2660
  assertAppendName(appendName);
2411
2661
  const generatedIds = events.map((event) => event.id === void 0);
2412
2662
  const withIds = await Promise.all(events.map(async (event, item) => {
2413
2663
  if (event.id !== void 0) return event;
2414
- const id = await deterministicEventId(trigger.id, appendName, item);
2664
+ const id = await deterministicEventId(trigger.id, appendName, item, idConsumer);
2415
2665
  return {
2416
2666
  ...event,
2417
2667
  id
2418
2668
  };
2419
2669
  }));
2420
- return appendCore(publicSessionId, withIds, "handler", "inline", (_rows, watchdogDueAt) => scheduleDrain(publicSessionId, watchdogDueAt), {
2421
- index: trigger.index,
2422
- attempt
2423
- }, generatedIds);
2670
+ return appendCore(publicSessionId, withIds, "consumer", "inline", (_rows, watchdogDueAt) => scheduleDrain(publicSessionId, watchdogDueAt), cause, generatedIds);
2424
2671
  };
2425
2672
  const ctx = {
2426
2673
  event: toPublic(trigger),
2427
2674
  attempt,
2428
2675
  session: makeSession(publicSessionId, append, trigger, {
2429
- cause: {
2430
- index: trigger.index,
2431
- attempt
2432
- },
2676
+ cause,
2433
2677
  end: endAttempt,
2434
2678
  signal
2435
2679
  }),
@@ -2507,21 +2751,21 @@ function createServer(options) {
2507
2751
  type: "completed",
2508
2752
  value
2509
2753
  }), (error) => ({
2510
- type: "handler-failed",
2754
+ type: "run-failed",
2511
2755
  error
2512
2756
  })), hub.watch.then(() => hub.error === null ? { type: "monitor-closed" } : {
2513
2757
  type: "monitor-failed",
2514
2758
  error: hub.error
2515
2759
  })]);
2516
2760
  if (outcome.type === "completed") return outcome.value;
2517
- if (outcome.type === "handler-failed") throw outcome.error;
2761
+ if (outcome.type === "run-failed") throw outcome.error;
2518
2762
  if (outcome.type === "monitor-failed") {
2519
2763
  for (const active of hub.subscriptions) active.controller.abort(outcome.error);
2520
2764
  await operation.catch(() => {});
2521
2765
  throw outcome.error;
2522
2766
  }
2523
2767
  await operation.catch(() => {});
2524
- throw new Error("abort monitor closed while handlers were active");
2768
+ throw new Error("abort monitor closed while consumers were active");
2525
2769
  },
2526
2770
  close: async () => {
2527
2771
  hub.subscriptions.delete(subscription);
@@ -2541,28 +2785,32 @@ function createServer(options) {
2541
2785
  if (result === void 0) return [];
2542
2786
  const events = Array.isArray(result) ? [...result] : [result];
2543
2787
  if (events.length === 0) return [];
2788
+ const idConsumer = idConsumerOf(trigger);
2544
2789
  const withIds = await Promise.all(events.map(async (event, item) => {
2545
2790
  const withId = { ...event };
2546
- withId.id = event.id ?? await deterministicReturnedEventId(trigger.id, item);
2791
+ withId.id = event.id ?? await deterministicReturnedEventId(trigger.id, item, idConsumer);
2547
2792
  return withId;
2548
2793
  }));
2549
2794
  return validateEvents(sessionId, withIds).map((event) => Object.assign(event, {
2550
2795
  id: event.id,
2551
2796
  cause: {
2797
+ consumer: trigger.consumer,
2798
+ lane: trigger.lane,
2552
2799
  index: trigger.index,
2553
2800
  attempt: trigger.attemptCount
2554
2801
  }
2555
2802
  }));
2556
2803
  };
2557
- const drainSession = (sessionId, { watchdogDueAt, signal, handOffFailure = false } = {}) => telemetry.span("a2.drain", {
2804
+ const drainSession = (sessionId, { watchdogDueAt, signal, handOffFailure = false, only, through } = {}) => telemetry.span("a2.drain", {
2558
2805
  "a2.contract": name,
2559
2806
  "a2.session_id": sessionId
2560
2807
  }, async (span) => {
2561
2808
  const store = await resolveStore();
2562
2809
  const ns = nsId(sessionId);
2563
2810
  const holder = crypto.randomUUID();
2811
+ /** Running heads, keyed by `headKey`: one event can run for several consumers. */
2564
2812
  const active = /* @__PURE__ */ new Map();
2565
- const excluded = /* @__PURE__ */ new Set();
2813
+ const excluded = /* @__PURE__ */ new Map();
2566
2814
  let processed = 0;
2567
2815
  let sawFailure = false;
2568
2816
  let schedulerArm;
@@ -2574,10 +2822,10 @@ function createServer(options) {
2574
2822
  const lapseDue = () => {
2575
2823
  lapseTimer = null;
2576
2824
  const nowMs = Date.now();
2577
- for (const [index, entry] of active) {
2825
+ for (const entry of active.values()) {
2578
2826
  if (entry.lapsed || entry.expiresAtMs > nowMs) continue;
2579
2827
  entry.lapsed = true;
2580
- entry.supersede.abort(new A2Error("CLAIM_EXPIRED", `the claim on event ${index} in session '${sessionId}' lapsed without renewal`));
2828
+ entry.supersede.abort(new A2Error("CLAIM_EXPIRED", `the claim on event ${entry.ref.index} for consumer '${entry.ref.consumer}' in session '${sessionId}' lapsed without renewal`));
2581
2829
  }
2582
2830
  scheduleLapse();
2583
2831
  };
@@ -2615,8 +2863,8 @@ function createServer(options) {
2615
2863
  if (stopRenewing || renewal || active.size === 0) return renewal ?? Promise.resolve();
2616
2864
  const window = nextClaimWindow();
2617
2865
  requestWatchdog(window.watchdogAtMs);
2618
- const claims = [...active.entries()].map(([index, entry]) => ({
2619
- index,
2866
+ const claims = [...active.values()].map((entry) => ({
2867
+ ...entry.ref,
2620
2868
  attempt: entry.attempt
2621
2869
  }));
2622
2870
  const operation = store.renewClaims({
@@ -2626,14 +2874,14 @@ function createServer(options) {
2626
2874
  ttlMs: window.ttlMs,
2627
2875
  ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {}
2628
2876
  }).then((result) => {
2629
- for (const index of result.renewed) {
2630
- const entry = active.get(index);
2877
+ for (const ref of result.renewed) {
2878
+ const entry = active.get(headKey(ref));
2631
2879
  if (entry && !entry.lapsed) entry.expiresAtMs = window.expiresAtMs;
2632
2880
  }
2633
- for (const index of result.superseded) {
2634
- const entry = active.get(index);
2635
- if (!entry) continue;
2636
- endSuperseded(entry, `a newer attempt owns event ${index} in session '${sessionId}'`);
2881
+ for (const ref of result.superseded) {
2882
+ const entry = active.get(headKey(ref));
2883
+ if (!entry || entry.completing) continue;
2884
+ endSuperseded(entry, `a newer attempt owns event ${ref.index} for consumer '${ref.consumer}' in session '${sessionId}'`);
2637
2885
  }
2638
2886
  if (window.deadlineCapped) stopRenewing = true;
2639
2887
  scheduleLapse();
@@ -2647,41 +2895,46 @@ function createServer(options) {
2647
2895
  renewal = tracked;
2648
2896
  return tracked;
2649
2897
  };
2650
- /** Records an event the store dead-lettered on takeover; it never runs. */
2898
+ /** Records a head the store dead-lettered on takeover; it never runs. */
2651
2899
  const reportExhausted = (event) => telemetry.span("a2.event", {
2652
2900
  "a2.contract": name,
2653
2901
  "a2.session_id": sessionId,
2654
2902
  "a2.event.type": event.type,
2655
2903
  "a2.event.index": event.index,
2656
2904
  "a2.event.id": event.id,
2905
+ "a2.event.consumer": event.consumer,
2657
2906
  "a2.event.attempt": event.attemptCount,
2658
- "a2.event.handled": handlers.has(event.type),
2907
+ "a2.event.handled": registeredConsumers.has(event.consumer),
2659
2908
  ...event.lane === null ? {} : { "a2.event.lane": event.lane }
2660
2909
  }, async (eventSpan) => {
2661
2910
  eventSpan.setAttribute("a2.event.outcome", "dead_lettered");
2662
2911
  });
2663
- const runEvent = async (event, supersedeSignal, endAttempt) => {
2664
- const registration = handlers.get(event.type);
2912
+ const runEvent = async (event, supersedeSignal, endAttempt, beginCompleting, sessionHead) => {
2913
+ const registration = only?.name === event.consumer ? only : registeredConsumers.get(event.consumer);
2914
+ const abortOn = registration?.abortOn(event.type) ?? null;
2665
2915
  return telemetry.span("a2.event", {
2666
2916
  "a2.contract": name,
2667
2917
  "a2.session_id": sessionId,
2668
2918
  "a2.event.type": event.type,
2669
2919
  "a2.event.index": event.index,
2670
2920
  "a2.event.id": event.id,
2921
+ "a2.event.consumer": event.consumer,
2671
2922
  "a2.event.attempt": event.attemptCount,
2672
2923
  "a2.event.handled": registration !== void 0,
2924
+ "a2.event.age_ms": Math.max(0, Date.now() - event.createdAt.getTime()),
2925
+ ...sessionHead === void 0 ? {} : { "a2.event.lag": Math.max(0, sessionHead - event.index) },
2673
2926
  ...event.lane === null ? {} : { "a2.event.lane": event.lane }
2674
2927
  }, async (eventSpan) => {
2675
2928
  let subscription;
2676
- let handlerSignal = supersedeSignal;
2929
+ let runSignal = supersedeSignal;
2677
2930
  let completion;
2678
2931
  let completing = false;
2679
2932
  try {
2680
- if (registration?.abortOn) {
2681
- subscription = await subscribeAbort(store, ns, event, registration.abortOn, event.attemptCount);
2682
- handlerSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
2933
+ if (abortOn) {
2934
+ subscription = await subscribeAbort(store, ns, event, abortOn, event.attemptCount);
2935
+ runSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
2683
2936
  }
2684
- const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal, endAttempt));
2937
+ const operation = registration?.run(makeCtx(event, event.attemptCount, runSignal, endAttempt));
2685
2938
  const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
2686
2939
  if (subscription) {
2687
2940
  await subscription.close();
@@ -2690,6 +2943,7 @@ function createServer(options) {
2690
2943
  const returned = await returnedEvents(sessionId, event, result);
2691
2944
  await renewal;
2692
2945
  completing = true;
2946
+ beginCompleting();
2693
2947
  completion = await telemetry.span("a2.commit", {
2694
2948
  "a2.contract": name,
2695
2949
  "a2.session_id": sessionId,
@@ -2699,9 +2953,12 @@ function createServer(options) {
2699
2953
  }, async (commitSpan) => {
2700
2954
  const committed = await store.completeAttempt({
2701
2955
  sessionId: ns,
2956
+ consumer: event.consumer,
2957
+ lane: event.lane,
2702
2958
  index: event.index,
2703
2959
  attempt: event.attemptCount,
2704
- events: returned
2960
+ events: returned,
2961
+ consumers: storeConsumers
2705
2962
  });
2706
2963
  commitSpan.setAttribute("a2.commit.outcome", committed.outcome);
2707
2964
  return committed;
@@ -2727,6 +2984,8 @@ function createServer(options) {
2727
2984
  }, async (commitSpan) => {
2728
2985
  const result = await store.failAttempt({
2729
2986
  sessionId: ns,
2987
+ consumer: event.consumer,
2988
+ lane: event.lane,
2730
2989
  index: event.index,
2731
2990
  attempt: event.attemptCount,
2732
2991
  error: describeError(err),
@@ -2751,7 +3010,7 @@ function createServer(options) {
2751
3010
  opensLane: false,
2752
3011
  appendsEvents: false
2753
3012
  };
2754
- if (handlerSignal.aborted) eventSpan.setAttribute("a2.event.aborted", true);
3013
+ if (runSignal.aborted) eventSpan.setAttribute("a2.event.aborted", true);
2755
3014
  return {
2756
3015
  status: "processed",
2757
3016
  opensLane: event.lane !== null,
@@ -2760,9 +3019,11 @@ function createServer(options) {
2760
3019
  });
2761
3020
  };
2762
3021
  let claimAgain = false;
2763
- const start = (event, expiresAtMs) => {
2764
- const previous = active.get(event.index);
2765
- if (previous) endSuperseded(previous, `a newer attempt owns event ${event.index} in session '${sessionId}'`);
3022
+ const start = (event, expiresAtMs, sessionHead) => {
3023
+ const ref = headRefOf(event);
3024
+ const key = headKey(ref);
3025
+ const previous = active.get(key);
3026
+ if (previous) endSuperseded(previous, `a newer attempt owns event ${event.index} for consumer '${event.consumer}' in session '${sessionId}'`);
2766
3027
  const supersede = new AbortController();
2767
3028
  let fence;
2768
3029
  const fenced = new Promise((resolve) => {
@@ -2771,27 +3032,31 @@ function createServer(options) {
2771
3032
  const endAttempt = () => endSuperseded({
2772
3033
  supersede,
2773
3034
  fence
2774
- }, `event ${event.index} in session '${sessionId}' was cleared`);
3035
+ }, `event ${event.index} for consumer '${event.consumer}' in session '${sessionId}' was cleared`);
2775
3036
  let entry;
2776
- const execution = Promise.race([runEvent(event, supersede.signal, endAttempt), fenced]).then((eventOutcome) => {
2777
- if (active.get(event.index) === entry) active.delete(event.index);
3037
+ const execution = Promise.race([runEvent(event, supersede.signal, endAttempt, () => {
3038
+ entry.completing = true;
3039
+ }, sessionHead), fenced]).then((eventOutcome) => {
3040
+ if (active.get(key) === entry) active.delete(key);
2778
3041
  if (eventOutcome.status === "processed") processed += 1;
2779
3042
  if (eventOutcome.status === "failed") {
2780
- excluded.add(event.index);
3043
+ excluded.set(key, ref);
2781
3044
  sawFailure = true;
2782
3045
  }
2783
3046
  claimAgain ||= eventOutcome.opensLane || eventOutcome.appendsEvents;
2784
3047
  return eventOutcome;
2785
3048
  });
2786
3049
  entry = {
3050
+ ref,
2787
3051
  attempt: event.attemptCount,
2788
3052
  execution,
2789
3053
  supersede,
2790
3054
  fence,
2791
3055
  expiresAtMs,
2792
- lapsed: false
3056
+ lapsed: false,
3057
+ completing: false
2793
3058
  };
2794
- active.set(event.index, entry);
3059
+ active.set(key, entry);
2795
3060
  scheduleLapse();
2796
3061
  if (!heartbeat && !stopRenewing) {
2797
3062
  heartbeat = setInterval(() => void renew(), DRAIN_TIMINGS.claimHeartbeatMs);
@@ -2820,7 +3085,9 @@ function createServer(options) {
2820
3085
  ttlMs: window.ttlMs,
2821
3086
  maxFailures: MAX_FAILURES,
2822
3087
  ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
2823
- ...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
3088
+ consumers: only ? [storeConsumerOf(only)] : storeConsumers,
3089
+ ...through === void 0 ? {} : { through },
3090
+ ...excluded.size === 0 ? {} : { exclude: [...excluded.values()] }
2824
3091
  });
2825
3092
  claimSpan.setAttribute("a2.claim.outcome", result.outcome);
2826
3093
  claimSpan.setAttribute("a2.claim.count", result.outcome === "claimed" ? result.events.length : 0);
@@ -2831,7 +3098,7 @@ function createServer(options) {
2831
3098
  for (const event of claim.events) if (event.failedAt !== null) await reportExhausted(event);
2832
3099
  if (runnable.length > 0) requestWatchdog(window.watchdogAtMs);
2833
3100
  if (window.deadlineCapped) stopRenewing = true;
2834
- for (const event of runnable) start(event, window.expiresAtMs);
3101
+ for (const event of runnable) start(event, window.expiresAtMs, claim.head);
2835
3102
  shouldClaim = false;
2836
3103
  }
2837
3104
  }
@@ -2860,6 +3127,10 @@ function createServer(options) {
2860
3127
  }
2861
3128
  if (claim === void 0) continue;
2862
3129
  if (signal && signal.version !== signalVersion) continue;
3130
+ if (claimAgain) {
3131
+ claimAgain = false;
3132
+ continue;
3133
+ }
2863
3134
  if (claim.outcome === "busy") {
2864
3135
  requestWatchdog(claim.retryAt.getTime() + DRAIN_TIMINGS.watchdogGraceMs);
2865
3136
  outcome = "busy";
@@ -2899,7 +3170,7 @@ function createServer(options) {
2899
3170
  const append = Object.assign(appendInline, { dispatch: appendDispatch });
2900
3171
  return Object.assign(makeSession(id, append, null), {
2901
3172
  deadLetters: () => listDeadLetters(id),
2902
- redrive: (index, redriveOptions) => redriveEvent(id, index, redriveOptions)
3173
+ redrive: (target, redriveOptions) => redriveEvent(id, target, redriveOptions)
2903
3174
  });
2904
3175
  };
2905
3176
  const historyPage = async (sessionId, bounds) => {
@@ -2968,6 +3239,9 @@ function assertStreamIndex(value) {
2968
3239
  function assertStateIndex(value) {
2969
3240
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
2970
3241
  }
3242
+ function assertConsumeIndex(value) {
3243
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("consume through must be a non-negative safe integer");
3244
+ }
2971
3245
  function assertRedriveIndex(value) {
2972
3246
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("redrive requires a positive safe-integer index");
2973
3247
  }
@@ -2975,7 +3249,7 @@ function assertClearIndex(value) {
2975
3249
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("ifLastIndex must be a non-negative safe integer");
2976
3250
  }
2977
3251
  function assertAppendName(name) {
2978
- if (typeof name !== "string" || name.length === 0) throw new TypeError("handler append name must be a non-empty string");
3252
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("consumer append name must be a non-empty string");
2979
3253
  }
2980
3254
  function assertScheduleName(name) {
2981
3255
  if (typeof name !== "string" || name.length === 0) throw new TypeError("schedule name must be a non-empty string");
@@ -3061,6 +3335,6 @@ const measuredBytes = (reducer, state, index, snapshots) => JSON.stringify(snaps
3061
3335
  }))?.length ?? 0;
3062
3336
  const stateCacheKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
3063
3337
  //#endregion
3064
- export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c, handler as i, deliverSchedulerAppend as n, setServerFetchHooks as o, deliverSchedulerClear as r, sseResponse as s, createServer as t };
3338
+ export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c, deliverSchedulerClear as i, createServer as n, setServerFetchHooks as o, deliverSchedulerAppend as r, sseResponse as s, consumer as t };
3065
3339
 
3066
- //# sourceMappingURL=server-qyb5aJJG.js.map
3340
+ //# sourceMappingURL=server-DvIB8kj5.js.map