experimental-a2 0.17.0 → 0.18.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 (221) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +2 -2
  10. package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
  11. package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
  12. package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
  13. package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
  14. package/dist/ai-client.d.ts +2 -2
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.browser.d.ts +1 -0
  17. package/dist/ai-fx.browser.js +6 -0
  18. package/dist/ai-fx.browser.js.map +1 -0
  19. package/dist/ai-fx.d.ts +10 -0
  20. package/dist/ai-fx.d.ts.map +1 -0
  21. package/dist/ai-fx.js +692 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
  24. package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
  25. package/dist/ai-harness-BuLh_EYi.js +273 -0
  26. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  27. package/dist/ai-pi.browser.d.ts +1 -0
  28. package/dist/ai-pi.browser.js +6 -0
  29. package/dist/ai-pi.browser.js.map +1 -0
  30. package/dist/ai-pi.d.ts +17 -0
  31. package/dist/ai-pi.d.ts.map +1 -0
  32. package/dist/ai-pi.js +950 -0
  33. package/dist/ai-pi.js.map +1 -0
  34. package/dist/ai-react.d.ts +5 -5
  35. package/dist/ai-react.js +2 -2
  36. package/dist/ai-server.d.ts +47 -25
  37. package/dist/ai-server.d.ts.map +1 -1
  38. package/dist/ai-server.js +113 -40
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  41. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  42. package/dist/ai.d.ts +1 -1
  43. package/dist/ai.js +1 -1
  44. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  45. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  46. package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
  47. package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
  48. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  49. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  50. package/dist/client.d.ts +3 -3
  51. package/dist/client.js +1 -1
  52. package/dist/index.d.ts +5 -5
  53. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  54. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  55. package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
  56. package/dist/internal-CscSY7K_.js.map +1 -0
  57. package/dist/otel.d.ts +1 -1
  58. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  59. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  60. package/dist/react.d.ts +1 -1
  61. package/dist/react.js +1 -1
  62. package/dist/reducer-BAXc8JyF.js.map +1 -1
  63. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  64. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-qstash.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +7 -7
  69. package/dist/scheduler-vercel.d.ts.map +1 -1
  70. package/dist/scheduler-vercel.js +9 -10
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
  73. package/dist/server-D-Y4pqwu.js.map +1 -0
  74. package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
  75. package/dist/server-DYAATFuT.d.ts.map +1 -0
  76. package/dist/server.d.ts +4 -4
  77. package/dist/server.js +1 -1
  78. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  79. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  80. package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
  81. package/dist/store-CyLUy86v.d.ts.map +1 -0
  82. package/dist/store-N8PXxDAS.js.map +1 -1
  83. package/dist/store-memory.d.ts +1 -1
  84. package/dist/store-memory.js +56 -11
  85. package/dist/store-memory.js.map +1 -1
  86. package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
  87. package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  88. package/dist/store-postgres.d.ts +1 -1
  89. package/dist/store-postgres.d.ts.map +1 -1
  90. package/dist/store-postgres.js +106 -25
  91. package/dist/store-postgres.js.map +1 -1
  92. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
  93. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  94. package/dist/store-redis-http.d.ts +1 -1
  95. package/dist/store-redis-http.js +3 -3
  96. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
  97. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  98. package/dist/store-redis.d.ts +1 -1
  99. package/dist/store-redis.js +1 -1
  100. package/dist/store-sqlite.d.ts +1 -1
  101. package/dist/store-sqlite.js +66 -21
  102. package/dist/store-sqlite.js.map +1 -1
  103. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  104. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  105. package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
  106. package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
  107. package/docs/01-quickstart.mdx +49 -83
  108. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  109. package/docs/actors/01-introduction.mdx +48 -86
  110. package/docs/actors/02-concurrency.mdx +43 -70
  111. package/docs/actors/03-timers.mdx +28 -42
  112. package/docs/actors/04-routes.mdx +110 -150
  113. package/docs/actors/meta.ts +1 -1
  114. package/docs/agents/01-introduction.mdx +257 -0
  115. package/docs/agents/02-tools.mdx +247 -0
  116. package/docs/agents/03-conversations.mdx +538 -0
  117. package/docs/agents/04-harnesses.mdx +136 -0
  118. package/docs/agents/meta.ts +1 -0
  119. package/docs/client/01-react.mdx +294 -0
  120. package/docs/client/02-transports.mdx +86 -0
  121. package/docs/client/03-presence.mdx +224 -0
  122. package/docs/client/meta.ts +1 -0
  123. package/docs/contract/01-events.mdx +57 -0
  124. package/docs/contract/02-state.mdx +142 -0
  125. package/docs/contract/03-sessions.mdx +175 -0
  126. package/docs/index.mdx +109 -145
  127. package/docs/reference/01-core.mdx +139 -0
  128. package/docs/reference/02-server.mdx +860 -0
  129. package/docs/reference/03-client.mdx +301 -0
  130. package/docs/reference/04-ai.mdx +731 -0
  131. package/docs/reference/05-adapters.mdx +351 -0
  132. package/docs/reference/06-errors.mdx +138 -0
  133. package/docs/reference/meta.ts +1 -1
  134. package/docs/server/01-handlers.mdx +200 -0
  135. package/docs/server/02-timers.mdx +130 -0
  136. package/docs/server/03-cancellation.mdx +142 -0
  137. package/docs/server/04-routes.mdx +243 -0
  138. package/docs/server/05-production.mdx +323 -0
  139. package/docs/server/06-guarantees.mdx +132 -0
  140. package/examples/playground/DEPLOY.md +3 -5
  141. package/examples/playground/app/a0/[appId]/page.tsx +12 -13
  142. package/examples/playground/app/a0/app-list.tsx +9 -2
  143. package/examples/playground/app/a0/app-view.tsx +9 -1
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  145. package/examples/playground/app/a0/apps.test.ts +12 -0
  146. package/examples/playground/app/a0/apps.ts +22 -7
  147. package/examples/playground/app/a0/chat.tsx +47 -1
  148. package/examples/playground/app/a0/devtools.tsx +2 -8
  149. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  150. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/model.test.ts +18 -1
  153. package/examples/playground/app/a0/model.ts +47 -10
  154. package/examples/playground/app/a0/no-photon.cjs +4 -0
  155. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  156. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  157. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  158. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  159. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  160. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  161. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  162. package/examples/playground/app/a0/server.ts +98 -54
  163. package/examples/playground/app/a0/session.ts +17 -5
  164. package/examples/playground/app/a0/shell.tsx +11 -3
  165. package/examples/playground/app/agent/ingress.test.ts +6 -3
  166. package/examples/playground/app/agent/server.ts +9 -6
  167. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  168. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  169. package/examples/playground/app/chat/server.test.ts +30 -26
  170. package/examples/playground/app/chat/server.ts +46 -46
  171. package/examples/playground/next.config.ts +17 -5
  172. package/examples/playground/package.json +4 -1
  173. package/examples/playground/vercel.json +18 -0
  174. package/package.json +27 -3
  175. package/src/ai-agent.ts +271 -90
  176. package/src/ai-fx.browser.ts +5 -0
  177. package/src/ai-fx.ts +912 -0
  178. package/src/ai-harness.ts +518 -0
  179. package/src/ai-model-metadata.ts +11 -6
  180. package/src/ai-pi.browser.ts +5 -0
  181. package/src/ai-pi.ts +1322 -0
  182. package/src/ai-sdk-step.ts +2 -2
  183. package/src/ai-server.ts +3 -1
  184. package/src/ai-tool-input.ts +45 -0
  185. package/src/internal.ts +13 -0
  186. package/src/libfx.d.ts +34 -0
  187. package/src/reducer.ts +1 -1
  188. package/src/scheduler-qstash.ts +5 -5
  189. package/src/scheduler-vercel.ts +14 -15
  190. package/src/server.ts +233 -29
  191. package/src/store-memory.ts +92 -16
  192. package/src/store-postgres.ts +156 -25
  193. package/src/store-redis-core.ts +322 -110
  194. package/src/store-redis-notify.ts +2 -0
  195. package/src/store-sqlite.ts +121 -28
  196. package/src/store.ts +55 -3
  197. package/src/telemetry.ts +1 -0
  198. package/dist/internal-CbVvEwad.js.map +0 -1
  199. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  200. package/dist/server-Rj64fNvX.js.map +0 -1
  201. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  202. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  203. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  204. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  205. package/docs/concepts/01-contracts.mdx +0 -237
  206. package/docs/concepts/02-handlers.mdx +0 -284
  207. package/docs/concepts/03-durability.mdx +0 -314
  208. package/docs/concepts/04-state.mdx +0 -209
  209. package/docs/guides/01-timers.mdx +0 -193
  210. package/docs/guides/02-cancellation.mdx +0 -161
  211. package/docs/guides/03-react.mdx +0 -324
  212. package/docs/guides/04-local-first.mdx +0 -47
  213. package/docs/guides/05-production.mdx +0 -534
  214. package/docs/guides/06-ai-agents.mdx +0 -1408
  215. package/docs/guides/08-application-data.mdx +0 -113
  216. package/docs/guides/09-presence.mdx +0 -262
  217. package/docs/guides/10-transports.mdx +0 -221
  218. package/docs/reference/01-api.mdx +0 -2358
  219. package/docs/reference/02-errors.mdx +0 -150
  220. /package/docs/{concepts → contract}/meta.ts +0 -0
  221. /package/docs/{guides → server}/meta.ts +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-clear-Dshpuhff.d.ts","names":[],"sources":["../src/session-clear.ts"],"mappings":";;;cAOa;;KAGD,iBAAiB;EAC3B,aAAa;EACb"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"store-Gpkq7WBo.d.ts","names":[],"sources":["../src/store.ts"],"mappings":";;;KAaY;EACV;EACA;EACA;;EAEA;EACA;EACA,WAAW;;;KAID;EACV;EACA;;EAEA;;;;;;;KAQU,cAAc;;EAExB,OAAO;;EAEP;;EAEA,aAAa;;EAEb;;EAEA;;EAEA,gBAAgB;;EAEhB,eAAe;;EAEf;;EAEA;;EAEA,gBAAgB;;EAEhB;;EAEA,cAAc;;EAEd;;EAEA;;EAEA,UAAU;;EAEV;;;;;;;;;KAUU;EACV;EACA;EACA;EACA;EACA,IAAI;EACJ,WAAW;;;KAID;;EAEV;;EAEA;IAAY;IAAe;;;EAE3B,QAAQ;;EAER;;KAGU;EACV;EACA;;EAEA;;KAGU;EACV;EACA;EACA;;;KAGU;EACN;EAAoB,QAAQ;;EAC5B;EAAiB,SAAS;;EAC1B;;;KAGM;EACR;EAAsB,QAAQ;;EAAoB;;;KAG1C;;EAEV;;EAEA;;;KAIU;EACV;EACA;;;KAIU;EACV;EACA;;EAEA;;EAEA,QAAQ;;EAER;;EAEA;;;KAIU,gBAAgB;EAAgB;;;KAGhC;EACV,QAAQ;;EAER;;KAGU;EACV;EACA;;EAEA,QAAQ;;KAGE;EACV;;EAEA;;EAEA;;KAGU;EAAuB;EAAiB;;;;;;;KAOxC;EACV,OAAO;;;KAIG;UAQK;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4Bf,OACE,mBACA,QAAQ,eACR;IAAY;MACX,QAAQ;;;;;;;EAQX,OAAO,SAAS,oBAAoB,QAAQ;;;;;;EAO5C,SAAS,oBAAoB,QAAQ;;EAGrC,KACE,mBACA;IAAS;IAAqB;MAC7B,QAAQ;;EAGX,aACE,mBACA;IAAS;IAAqB;MAC7B,QAAQ;;;;;;;;EASX,eAAe;IACb;IACA;IACA;IACA;IACA;MACE,QAAQ;;;;;;;;;;EAWZ,YAAY;IACV;IACA;IACA;MAAmB;MAAe;;IAClC;IACA;MACE,QAAQ;;;;;;EAOZ,gBAAgB;IACd;IACA;IACA;IACA,QAAQ;MACN,QAAQ;;;;;;EAOZ,YAAY;IACV;IACA;IACA;IACA;IACA;MACE,QAAQ;;;;;;;;EASZ,UACE,mBACA,qBACA;IAAY;IAAuB;MAClC,QAAQ;;;;;;;;EASX,YACE,mBAAmB,yBACnB,sBACC,QAAQ;;;;;;;EAQX,aACE,mBACA,qBACA,oBAAoB,uBACnB;;;;;;;EAQH;;;;;;;;;IASE,IACE,YACA,qBACA,QAAQ,gCACR;MAAQ;MAAc,IAAI;MAAM;QAC/B;;IAGH,KAAK,aAAa,QAAQ;;;;;;IAO1B,WAAW,YAAY,UAAU,OAAO;;;;;;;;;EAU1C,OACE,mBACA;IAAS;MACR,cAAc"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"store-redis-notify-tqWNnMRu.js","names":[],"sources":["../src/presence-recovery.ts","../src/store-redis-core.ts","../src/store-redis-notify.ts"],"sourcesContent":["import type { PresencePatch } from './contract.ts'\nimport type { PresenceRow } from './store.ts'\n\ntype Field = Pick<PresenceRow, 'participant' | 'field' | 'seen' | 'at'>\nconst key = ({\n participant,\n field,\n}: Pick<Field, 'participant' | 'field'>): string =>\n JSON.stringify([participant, field])\n\nexport function presenceRecovery({\n read,\n deliver,\n isActive,\n}: {\n read: () => Promise<PresenceRow[]>\n deliver: (patch: PresencePatch) => void\n isActive: () => boolean\n}): {\n read(): Promise<PresenceRow[]>\n reconnect(): Promise<void>\n update(patch: PresencePatch): void\n} {\n const fields = new Map<string, Field>()\n const reading = new Set<Set<string>>()\n let generation = 0\n let latest: Promise<PresenceRow[]> | undefined\n\n const remember = ({ participant, field, seen, at }: Field): void => {\n const id = key({ participant, field })\n const previous = fields.get(id)\n if (\n !previous ||\n previous.at.getTime() < at.getTime() ||\n (previous.at.getTime() === at.getTime() && previous.seen !== seen)\n )\n fields.set(id, { participant, field, seen, at })\n }\n const snapshot = async ({\n reconcile,\n }: {\n reconcile: boolean\n }): Promise<PresenceRow[]> => {\n const run = generation\n const before = new Map(fields)\n const changed = new Set<string>()\n reading.add(changed)\n try {\n const rows = await read()\n if (!isActive()) return rows\n if (run !== generation) return latest!\n const live = new Set<string>()\n for (const row of rows) {\n const id = key(row)\n live.add(id)\n if (changed.has(id)) continue\n remember(row)\n if (reconcile)\n deliver({\n participant: row.participant,\n values: { [row.field]: row.value },\n seen: row.seen,\n at: row.at,\n })\n }\n if (reconcile) {\n for (const [id, prior] of before) {\n if (live.has(id) || changed.has(id) || fields.get(id) !== prior)\n continue\n fields.delete(id)\n deliver({\n participant: prior.participant,\n values: { [prior.field]: null },\n seen: prior.seen,\n at: prior.at,\n })\n }\n }\n return rows\n } finally {\n reading.delete(changed)\n if (reading.size === 0) latest = undefined\n }\n }\n\n return {\n async read() {\n let pending = latest\n while (pending) {\n // oxlint-disable-next-line no-await-in-loop -- follow replacement reconnects before starting a fresh read\n await pending\n if (latest === pending) break\n pending = latest\n }\n return snapshot({ reconcile: false })\n },\n async reconnect() {\n generation += 1\n latest = snapshot({ reconcile: true })\n await latest\n },\n update(patch) {\n for (const [field, value] of Object.entries(patch.values)) {\n if (value === undefined) continue\n const id = key({ participant: patch.participant, field })\n const previous = fields.get(id)\n if (previous && previous.at.getTime() > patch.at.getTime()) continue\n for (const changed of reading) changed.add(id)\n if (value === null) fields.delete(id)\n else\n remember({\n participant: patch.participant,\n field,\n seen: patch.seen,\n at: patch.at,\n })\n }\n },\n }\n}\n","/**\n * The shared Redis store core — every operation except `stream` and\n * `close`, expressed over one injected `call` function so both\n * transports reuse it: `store-redis` (Redis protocol via `ioredis`) and\n * `store-redis-http` (provider REST APIs).\n *\n * The stream IS the store: each event is a stream entry whose ID is its\n * dense a2 index (`${index}-0`), so `XRANGE` is the catch-up read.\n * Mutable drain/failure bookkeeping lives in side hashes of fixed index\n * ranges (stream entries are immutable); every write path is one atomic\n * Lua script.\n * Global event-id ownership is one key per id, so no key grows with\n * the number of sessions.\n * The conformance suite in test/conformance is the executable\n * contract, run against a real spawned `redis-server`.\n *\n * Timestamps and claim expiry come from the injected clock so tests\n * can time-travel against a real server. Cluster mode is out: the\n * atomic scripts span keys.\n */\n\nimport type { PresencePatch } from './contract.ts'\nimport { A2Error, supersededAttemptError } from './errors.ts'\nimport { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport { decodeReturnedEventIds } from './store-codec.ts'\nimport type { SerializedEvent } from './stream-buffer.ts'\nimport type {\n A2Store,\n Clock,\n CompleteAttemptResult,\n Event,\n EventCause,\n IdSource,\n StoreClaimAvailableResult,\n PresenceRow,\n StoredEvent,\n} from './store.ts'\n\n/** Issues any Redis command; the seam both transports implement. */\nexport type RedisCall = (\n command: string,\n ...args: Array<string | number>\n) => Promise<unknown>\n\nexport type RedisStoreCoreOptions = {\n call: RedisCall\n /**\n * Issues a command whose bulk replies arrive as raw bytes. Live feed\n * catch-up pages use it to decode only the fields a row needs.\n */\n callBytes?: RedisCall\n clock: Clock\n ids: IdSource\n keyPrefix: string\n presenceSource: string\n readStream?: RedisStreamRead\n}\n\n/** Stream entries from `from` onward, in nonempty ordered batches. */\nexport type RedisStreamRead = (options: {\n key: string\n from: string\n signal: AbortSignal\n}) => AsyncGenerator<StreamEntry[], void>\n\nexport type RedisStoreCore = Omit<A2Store, 'stream' | 'presence'> & {\n /**\n * The storage half of the presence capability, shared by both\n * transports. `set` returns the applied patch for local loopback,\n * or null when every field lost the LWW race. The script publishes\n * the applied fields for remote subscribers.\n */\n presence: {\n set(\n ns: string,\n participant: string,\n values: Record<string, unknown | null>,\n meta: { seen: number; at: Date; ttlMs: number },\n ): Promise<PresencePatch | null>\n read(ns: string): Promise<PresenceRow[]>\n }\n /** XRANGE + meta merge — the full-row catch-up read. */\n readRange(\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]>\n /** One bounded XRANGE page of public events for live feeds. */\n readEvents(sessionId: string, afterIndex: number): Promise<Event[]>\n /**\n * One bounded XRANGE page of catch-up rows for a live feed without a\n * streaming reader, payloads left as stored JSON. The reply is held whole,\n * so a page retains up to `FEED_PAGE_SIZE` entries.\n */\n readFeedPage(\n sessionId: string,\n afterIndex: number,\n ): Promise<SerializedEvent[]>\n /** Catch-up events after `afterIndex`, in nonempty ordered batches. */\n streamEvents?: (options: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }) => AsyncGenerator<SerializedEvent[], void>\n /**\n * Events from `fromIndex` through `throughIndex` or the newest entry in\n * one newest-first page, or null when the range doesn't fit in one.\n */\n readTail(\n sessionId: string,\n fromIndex: number,\n throughIndex?: number,\n ): Promise<Event[] | null>\n}\n\nexport const READ_PAGE_SIZE = 256\nexport const FEED_PAGE_SIZE = 1000\nconst READ_CONCURRENCY = 16\n\n// ── Lua scripts (each runs atomically) ───────────────────────────────\n\nconst META_CHUNK_SIZE = 1000\n\n/**\n * Upstash bills every call a script makes, so scripts read the counter and\n * split with one MGET, read metadata with one HMGET per hash (`read_fields`),\n * and buffer hash writes (`put`) into one HSET per hash (`flush`). Buffered\n * writes are invisible to reads until flushed, so a script must not read a\n * field it has buffered. Per-event metadata lives in hashes of\n * META_CHUNK_SIZE consecutive indexes, so no key grows with a session's\n * history. Indexes at or below the split predate chunking and stay in the\n * base hash, which also holds every session's lane pointers. An unset split\n * means the current event count, so any script that adds events must write\n * `split_write` pairs in its write phase. Including scripts bind COUNTER,\n * META (the base hash), and SPLIT first.\n */\nconst META_LUA = `\nlocal split, split_stored = nil, false\n-- The latest clear marker lives beside the counter, {prefix}:{session}:clear,\n-- and is read in the same MGET, so a session never cleared pays nothing.\nlocal CLEAR = string.sub(COUNTER, 1, -6) .. 'clear'\nlocal boundary = 0\nlocal function use_split(stored, count, cleared)\n split_stored = stored ~= false\n split = tonumber(stored or count or '0')\n boundary = tonumber(cleared or '0')\nend\nlocal function mkey(field)\n if split == nil then\n local values = redis.call('MGET', SPLIT, COUNTER, CLEAR)\n use_split(values[1], values[2], values[3])\n end\n local idx = tonumber(field)\n if idx <= split then return META end\n return META .. ':' .. tostring(math.floor((idx - 1) / ${META_CHUNK_SIZE}))\nend\nlocal function split_write(args)\n if not split_stored then\n args[#args + 1] = SPLIT\n args[#args + 1] = tostring(split)\n end\nend\nlocal function sliced(command, key, args)\n local results = {}\n for first = 1, #args, 1000 do\n local last = math.min(first + 999, #args)\n local reply\n if key then\n reply = redis.call(command, key, unpack(args, first, last))\n else\n reply = redis.call(command, unpack(args, first, last))\n end\n if type(reply) == 'table' then\n for _, value in ipairs(reply) do results[#results + 1] = value end\n end\n end\n return results\nend\nlocal function read_fields(fields, suffixes)\n local groups, order = {}, {}\n for _, field in ipairs(fields) do\n local key = mkey(field)\n local group = groups[key]\n if not group then\n group = { fields = {}, names = {} }\n groups[key] = group\n order[#order + 1] = key\n end\n group.fields[#group.fields + 1] = field\n for _, suffix in ipairs(suffixes) do\n group.names[#group.names + 1] = field .. ':' .. suffix\n end\n end\n local rows = {}\n for _, key in ipairs(order) do\n local group = groups[key]\n local values = sliced('HMGET', key, group.names)\n for position, field in ipairs(group.fields) do\n local row = {}\n for column = 1, #suffixes do\n row[column] = values[(position - 1) * #suffixes + column]\n end\n rows[field] = row\n end\n end\n return rows\nend\nlocal writes, write_order = {}, {}\nlocal function put(key, field, value)\n local fields = writes[key]\n if not fields then\n fields = {}\n writes[key] = fields\n write_order[#write_order + 1] = key\n end\n fields[#fields + 1] = field\n fields[#fields + 1] = value\nend\nlocal function flush()\n for _, key in ipairs(write_order) do sliced('HSET', key, writes[key]) end\n writes, write_order = {}, {}\nend\n`\n\n/**\n * Lane-aware enqueueing for scripts that add events. Including scripts bind\n * PENDING and READY. Lane tails are read once per lane and tracked in\n * memory, so a batch in one lane links its events without re-reading. A\n * lane emptied earlier in the script (`clear_lane`) is known to have no tail,\n * and its pointers are deleted at `flush_queue` only if nothing re-filled it.\n */\nconst ENQUEUE_LUA = `\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal queued, ready_now, lane_tails, lane_order = {}, {}, {}, {}\nlocal function enqueue(field, lane)\n local score = tonumber(field)\n queued[#queued + 1] = score\n queued[#queued + 1] = field\n if lane == '' then\n ready_now[#ready_now + 1] = score\n ready_now[#ready_now + 1] = field\n return\n end\n put(mkey(field), field .. ':l', lane)\n local tail = lane_tails[lane]\n if tail == nil then\n tail = redis.call('HGET', META, lane_field(lane, 'tail'))\n if tail and tonumber(tail) <= boundary then tail = false end\n lane_order[#lane_order + 1] = lane\n end\n if tail then\n put(mkey(tail), tail .. ':next', field)\n else\n put(META, lane_field(lane, 'head'), field)\n ready_now[#ready_now + 1] = score\n ready_now[#ready_now + 1] = field\n end\n lane_tails[lane] = field\nend\nlocal function clear_lane(lane)\n if lane_tails[lane] == nil then lane_order[#lane_order + 1] = lane end\n lane_tails[lane] = false\nend\nlocal function flush_queue()\n for _, lane in ipairs(lane_order) do\n if lane_tails[lane] then\n put(META, lane_field(lane, 'tail'), lane_tails[lane])\n else\n redis.call('HDEL', META, lane_field(lane, 'head'), lane_field(lane, 'tail'))\n end\n end\n sliced('ZADD', PENDING, queued)\n sliced('ZADD', READY, ready_now)\nend\n`\n\n/**\n * Event-id ownership for scripts that add events. Each script reads the\n * counter, split, clear boundary, and per-id keys with one MGET and applies\n * the first three. Returns the owner record, or false, per event, and the\n * counter. A record this session made below its latest clear is free.\n */\nconst ID_OWNERS_LUA = `\nlocal function read_owners(key_offset, n, session)\n local keys = { COUNTER, SPLIT, CLEAR }\n for i = 1, n do keys[#keys + 1] = KEYS[key_offset + i] end\n local values = sliced('MGET', nil, keys)\n use_split(values[2], values[1], values[3])\n local owners = {}\n for i = 1, n do\n local owner = values[3 + i]\n if owner then\n local rec = cjson.decode(owner)\n if rec[1] == session and tonumber(rec[2]) < boundary then owner = false end\n end\n owners[i] = owner\n end\n return owners, values[1]\nend\n`\n\nconst WAKE_PAYLOAD_LIMIT = 32_768\n\n/**\n * Wake-up messages carry the events they announce, so a subscribed reader\n * that is caught up applies them without reading the stream:\n * `{\"f\":frontier,\"t\":createdAt,\"e\":[[index,id,type,payload],...]}`. Payloads\n * are the stored JSON text, inlined verbatim. A batch over\n * WAKE_PAYLOAD_LIMIT bytes publishes `{\"f\":frontier}` and readers catch up\n * with XRANGE.\n */\nconst WAKE_LUA = `\nlocal wake_entries, wake_bytes = {}, 0\nlocal function wake_add(field, id, typ, payload)\n if wake_bytes + #payload > ${WAKE_PAYLOAD_LIMIT} then\n wake_bytes = ${WAKE_PAYLOAD_LIMIT} + 1\n return\n end\n local entry = '[' .. field .. ',' .. cjson.encode(id) .. ',' ..\n cjson.encode(typ) .. ',' .. payload .. ']'\n wake_bytes = wake_bytes + #entry + 1\n if wake_bytes <= ${WAKE_PAYLOAD_LIMIT} then\n wake_entries[#wake_entries + 1] = entry\n end\nend\nlocal function wake(channel, frontier, created_at)\n local message = '{\"f\":' .. frontier .. '}'\n if wake_bytes <= ${WAKE_PAYLOAD_LIMIT} then\n message = '{\"f\":' .. frontier .. ',\"t\":' .. created_at .. ',\"e\":[' ..\n table.concat(wake_entries, ',') .. ']}'\n end\n redis.pcall('PUBLISH', channel, message)\nend\n`\n\nconst RELEASE_PINS_LUA = `\nlocal function release_snapshot_pins(pins, counts)\n local refs = redis.call('SMEMBERS', pins)\n if #refs == 0 then return end\n for _, ref in ipairs(refs) do\n local count = redis.call('HINCRBY', counts, ref, -1)\n if count <= 0 then\n redis.call('HDEL', counts, ref)\n local checkpoint = cjson.decode(ref)\n redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])\n end\n end\n redis.call('DEL', pins)\nend\n`\n\n/**\n * KEYS: store stream, counter, metadata, pending, ready, metadata split,\n * then one id ownership key per event.\n * ARGV: sessionId, nowMs, n, then per event:\n * id, type, payloadJson, causeJson, lane, settled; then notify channel and\n * origin index or ''.\n * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |\n * ['dup', hasPending, ...indexes] | ['partial', count] |\n * ['foreign', eventId] | ['no_parent', idx] | ['superseded', idx] |\n * ['cleared', originIndex].\n */\nconst APPEND_LUA = `\nlocal COUNTER, META, SPLIT, PENDING, READY = KEYS[2], KEYS[3], KEYS[6], KEYS[4], KEYS[5]\n${META_LUA}\n${ENQUEUE_LUA}\n${ID_OWNERS_LUA}\n${WAKE_LUA}\nlocal function has_pending()\n return redis.call('ZCARD', PENDING) > 0 and '1' or '0'\nend\nlocal n = tonumber(ARGV[3])\nif n == 0 then return { 'empty', has_pending() } end\nlocal owners, count = read_owners(6, n, ARGV[1])\nlocal base = tonumber(count or '0')\nlocal dups = {}\nfor i = 1, n do\n if owners[i] then\n local rec = cjson.decode(owners[i])\n if rec[1] ~= ARGV[1] then return { 'foreign', ARGV[3 + (i - 1) * 6 + 1] } end\n dups[#dups + 1] = tostring(rec[2])\n end\nend\nif #dups == n then\n local res = { 'dup', has_pending() }\n for i = 1, #dups do res[#res + 1] = dups[i] end\n return res\nend\nif #dups > 0 then return { 'partial', tostring(#dups) } end\nlocal origin = ARGV[5 + n * 6]\nif origin and origin ~= '' and tonumber(origin) <= boundary then\n return { 'cleared', origin }\nend\nlocal causes, parents = {}, {}\nfor i = 1, n do\n local cause = ARGV[3 + (i - 1) * 6 + 4]\n if cause ~= '' then\n local parsed = cjson.decode(cause)\n local pidx = tonumber(parsed.index)\n causes[#causes + 1] = parsed\n if pidx >= 1 and pidx <= base then parents[#parents + 1] = tostring(pidx) end\n end\nend\nlocal parent_rows = read_fields(parents, { 'd', 'f', 'p' })\nlocal parent_pending = false\nfor _, parsed in ipairs(causes) do\n local pidx = tonumber(parsed.index)\n if pidx <= boundary then return { 'superseded', tostring(pidx) } end\n if pidx < 1 or pidx > base then\n return { 'no_parent', tostring(parsed.index) }\n end\n local row = parent_rows[tostring(pidx)]\n if tonumber(row[1] or '0') ~= tonumber(parsed.attempt) or row[2] then\n return { 'superseded', tostring(pidx) }\n end\n -- A claimed, unprocessed parent is still in the pending set.\n if not row[3] then parent_pending = true end\nend\nlocal owner_writes = {}\nfor i = 1, n do\n local o = 3 + (i - 1) * 6\n local field = tostring(base + i)\n redis.call('XADD', KEYS[1], field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', ARGV[2], 'cause', ARGV[o + 4], 'lane', ARGV[o + 5])\n owner_writes[#owner_writes + 1] = KEYS[6 + i]\n owner_writes[#owner_writes + 1] = cjson.encode({ ARGV[1], base + i })\n wake_add(field, ARGV[o + 1], ARGV[o + 2], ARGV[o + 3])\n if ARGV[o + 6] == '1' then\n put(mkey(field), field .. ':p', ARGV[2])\n else\n enqueue(field, ARGV[o + 5])\n end\nend\nowner_writes[#owner_writes + 1] = COUNTER\nowner_writes[#owner_writes + 1] = tostring(base + n)\nsplit_write(owner_writes)\nsliced('MSET', nil, owner_writes)\nflush_queue()\nflush()\nlocal pending_after = (#queued > 0 or parent_pending) and '1' or has_pending()\nwake(ARGV[4 + n * 6], tostring(base + n), ARGV[2])\nreturn { 'ok', tostring(base + 1), pending_after }\n`\n\n/**\n * KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts,\n * metadata split.\n * ARGV: idx, attempt, error, maxFailures, nowMs.\n */\nconst FAIL_ATTEMPT_LUA = `\nlocal COUNTER, META, SPLIT = KEYS[1], KEYS[2], KEYS[7]\n${META_LUA}\n${RELEASE_PINS_LUA}\nlocal idx = tonumber(ARGV[1])\nlocal counts = redis.call('MGET', COUNTER, SPLIT, CLEAR)\nlocal total = tonumber(counts[1] or '0')\nuse_split(counts[2], counts[1], counts[3])\nif idx <= boundary then return { 'cleared', '0' } end\nif idx < 1 or idx > total then return { 'missing', '0' } end\nlocal field = ARGV[1]\nlocal attempt = tonumber(ARGV[2])\nlocal row = read_fields({ field }, { 'a', 'd', 'p', 'f', 'lfa', 'ch' })[field]\nlocal failures = tonumber(row[1] or '0')\nif row[3] or tonumber(row[2] or '0') ~= attempt then\n return { 'superseded', tostring(failures) }\nend\nif row[4] then return { 'dead_lettered', tostring(failures) } end\nif row[5] and tonumber(row[5]) == attempt and not row[6] then\n return { 'failed', tostring(failures) }\nend\nif not row[6] then return { 'superseded', tostring(failures) } end\nfailures = failures + 1\nlocal dead = failures >= tonumber(ARGV[4])\nlocal meta_key = mkey(field)\nput(meta_key, field .. ':a', tostring(failures))\nput(meta_key, field .. ':e', ARGV[3])\nput(meta_key, field .. ':lf', ARGV[5])\nput(meta_key, field .. ':lfa', ARGV[2])\nif dead then put(meta_key, field .. ':f', ARGV[5]) end\nflush()\nredis.call('HDEL', meta_key, field .. ':ch', field .. ':ce')\nif dead then\n redis.call('ZREM', KEYS[4], field)\n release_snapshot_pins(KEYS[5], KEYS[6])\n return { 'dead_lettered', tostring(failures) }\nend\nreturn { 'failed', tostring(failures) }\n`\n\n/**\n * KEYS: store, counter, meta, pending, ready, metadata split.\n * ARGV: holder, nowMs, expiresAtMs, excludedCount, excluded indexes.\n * The ready set is a subset of the pending set, so an empty ready set is\n * enough to report 'settled'. A claimed result ends with the clear boundary.\n */\nconst CLAIM_AVAILABLE_LUA = `\nlocal COUNTER, META, SPLIT = KEYS[2], KEYS[3], KEYS[6]\n${META_LUA}\nlocal ready = redis.call('ZRANGE', KEYS[5], 0, -1)\nlocal excluded = {}\nfor i = 1, tonumber(ARGV[4]) do excluded[ARGV[4 + i]] = true end\nlocal candidates = {}\nfor _, field in ipairs(ready) do\n if not excluded[field] then candidates[#candidates + 1] = field end\nend\nlocal rows = read_fields(candidates, { 'ce', 'd', 'fc', 'a', 'e', 'lf', 'lfa' })\nlocal claimed = {}\nlocal retry_at = nil\nfor _, field in ipairs(candidates) do\n local row = rows[field]\n local expires = tonumber(row[1] or '0')\n if expires > tonumber(ARGV[2]) then\n if not retry_at or expires < retry_at then retry_at = expires end\n else\n local entries = redis.call('XRANGE', KEYS[1], field .. '-0', field .. '-0')\n if #entries == 0 then return { 'missing', field } end\n local attempt = tostring(tonumber(row[2] or '0') + 1)\n local first_claimed = row[3] or ARGV[2]\n local meta_key = mkey(field)\n put(meta_key, field .. ':d', attempt)\n put(meta_key, field .. ':fc', first_claimed)\n put(meta_key, field .. ':lc', ARGV[2])\n put(meta_key, field .. ':ch', ARGV[1])\n put(meta_key, field .. ':ce', ARGV[3])\n claimed[#claimed + 1] = {\n attempt,\n row[4] or '0',\n row[5] or false,\n first_claimed,\n ARGV[2],\n row[6] or false,\n row[7] or false,\n ARGV[1],\n ARGV[3],\n entries[1]\n }\n end\nend\nflush()\nif #claimed > 0 then\n local result = { 'claimed', tostring(#claimed) }\n for _, claim in ipairs(claimed) do\n for _, value in ipairs(claim) do result[#result + 1] = value end\n end\n result[#result + 1] = tostring(boundary)\n return result\nend\nif retry_at then return { 'busy', tostring(retry_at) } end\nreturn { 'settled' }\n`\n\n/**\n * KEYS: counter, meta, metadata split.\n * ARGV: holder, nowMs, expiresAtMs, n, then per claim: index, attempt.\n * Returns [renewedCount, ...renewed, ...superseded].\n */\nconst RENEW_CLAIMS_LUA = `\nlocal COUNTER, META, SPLIT = KEYS[1], KEYS[2], KEYS[3]\n${META_LUA}\nlocal counts = redis.call('MGET', COUNTER, SPLIT, CLEAR)\nlocal total = tonumber(counts[1] or '0')\nuse_split(counts[2], counts[1], counts[3])\nlocal claims, fields, cleared = {}, {}, {}\nfor i = 1, tonumber(ARGV[4]) do\n local field = ARGV[4 + (i - 1) * 2 + 1]\n local idx = tonumber(field)\n if idx <= boundary then\n cleared[#cleared + 1] = field\n elseif idx >= 1 and idx <= total then\n claims[#claims + 1] = { field = field, attempt = tonumber(ARGV[4 + (i - 1) * 2 + 2]) }\n fields[#fields + 1] = field\n end\nend\nlocal rows = read_fields(fields, { 'd', 'ce', 'p', 'f', 'ch' })\nlocal renewed = {}\nlocal superseded = {}\nfor _, claim in ipairs(claims) do\n local row = rows[claim.field]\n if tonumber(row[1] or '0') > claim.attempt then\n superseded[#superseded + 1] = claim.field\n elseif tonumber(ARGV[3]) > tonumber(ARGV[2]) and\n not row[3] and not row[4] and row[5] == ARGV[1] and\n tonumber(row[2] or '0') > tonumber(ARGV[2]) then\n put(mkey(claim.field), claim.field .. ':ce', ARGV[3])\n renewed[#renewed + 1] = claim.field\n end\nend\nflush()\nfor _, field in ipairs(cleared) do superseded[#superseded + 1] = field end\nlocal result = { tostring(#renewed) }\nfor i = 1, #renewed do result[#result + 1] = renewed[i] end\nfor i = 1, #superseded do result[#result + 1] = superseded[i] end\nreturn result\n`\n\n/**\n * KEYS: store, counter, meta, pending, ready, event snapshot pins, snapshot\n * pin counts, metadata split, then one id ownership key per child.\n * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:\n * id, type, payloadJson, lane, settled; finally notify channel.\n */\nconst COMPLETE_ATTEMPT_LUA = `\nlocal COUNTER, META, SPLIT, PENDING, READY = KEYS[2], KEYS[3], KEYS[8], KEYS[4], KEYS[5]\n${META_LUA}\n${ENQUEUE_LUA}\n${ID_OWNERS_LUA}\n${RELEASE_PINS_LUA}\n${WAKE_LUA}\nlocal function settle(field, lane, next_field)\n redis.call('ZREM', PENDING, field)\n redis.call('ZREM', READY, field)\n if lane == '' then return end\n local head_key = lane_field(lane, 'head')\n if redis.call('HGET', META, head_key) ~= field then return end\n while next_field do\n local row = read_fields({ next_field }, { 'p', 'f', 'next' })[next_field]\n if not row[1] then\n redis.call('HSET', META, head_key, next_field)\n if not row[2] then\n redis.call('ZADD', READY, tonumber(next_field), next_field)\n end\n return\n end\n next_field = row[3]\n end\n clear_lane(lane)\nend\nlocal parent = ARGV[3]\nlocal attempt = tonumber(ARGV[4])\nlocal n = tonumber(ARGV[5])\nlocal owners, count = read_owners(8, n, ARGV[1])\nlocal total = tonumber(count or '0')\nif tonumber(parent) <= boundary then return { 'superseded' } end\nif tonumber(parent) < 1 or tonumber(parent) > total then return { 'missing' } end\nlocal row = read_fields({ parent }, { 'd', 'p', 'pa', 'ri', 'ch', 'f', 'lfa', 'l', 'next' })[parent]\nif row[2] then\n if tonumber(row[3] or '0') ~= attempt then return { 'superseded' } end\n if not row[4] then return { 'partial' } end\n local recorded_ids = cjson.decode(row[4])\n if #recorded_ids ~= n then return { 'partial' } end\n for i = 1, n do\n if recorded_ids[i] ~= ARGV[5 + (i - 1) * 5 + 1] then return { 'partial' } end\n end\n local indexes = {}\n for i = 1, n do\n if not owners[i] then return { 'partial' } end\n local rec = cjson.decode(owners[i])\n if rec[1] ~= ARGV[1] then return { 'partial' } end\n local entries = redis.call('XRANGE', KEYS[1], tostring(rec[2]) .. '-0', tostring(rec[2]) .. '-0')\n if #entries == 0 then return { 'partial' } end\n local values = entries[1][2]\n local cause = nil\n for j = 1, #values, 2 do\n if values[j] == 'cause' then cause = values[j + 1] break end\n end\n if not cause or cause == '' then return { 'partial' } end\n local decoded = cjson.decode(cause)\n if tonumber(decoded['index']) ~= tonumber(parent) or\n tonumber(decoded['attempt']) ~= attempt then return { 'partial' } end\n indexes[#indexes + 1] = tostring(rec[2])\n end\n local result = { 'duplicate' }\n for _, idx in ipairs(indexes) do result[#result + 1] = idx end\n return result\nend\nif tonumber(row[1] or '0') ~= attempt then return { 'superseded' } end\nif not row[5] or row[6] or tonumber(row[7] or '0') == attempt then\n return { 'superseded' }\nend\nfor i = 1, n do\n if owners[i] then return { 'partial' } end\nend\nlocal returned_ids = {}\nfor i = 1, n do returned_ids[i] = ARGV[5 + (i - 1) * 5 + 1] end\nlocal returned_json = '[]'\nif n > 0 then returned_json = cjson.encode(returned_ids) end\nlocal parent_key = mkey(parent)\nput(parent_key, parent .. ':p', ARGV[2])\nput(parent_key, parent .. ':pa', ARGV[4])\nput(parent_key, parent .. ':ri', returned_json)\nredis.call('HDEL', parent_key, parent .. ':ch', parent .. ':ce')\nsettle(parent, row[8] or '', row[9])\nrelease_snapshot_pins(KEYS[6], KEYS[7])\nlocal cause = cjson.encode({ index = tonumber(parent), attempt = attempt })\nlocal owner_writes = {}\nfor i = 1, n do\n local o = 5 + (i - 1) * 5\n local field = tostring(total + i)\n redis.call('XADD', KEYS[1], field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', ARGV[2], 'cause', cause, 'lane', ARGV[o + 4])\n owner_writes[#owner_writes + 1] = KEYS[8 + i]\n owner_writes[#owner_writes + 1] = cjson.encode({ ARGV[1], total + i })\n wake_add(field, ARGV[o + 1], ARGV[o + 2], ARGV[o + 3])\n if ARGV[o + 5] == '1' then\n put(mkey(field), field .. ':p', ARGV[2])\n else\n enqueue(field, ARGV[o + 4])\n end\nend\nif n > 0 then\n owner_writes[#owner_writes + 1] = COUNTER\n owner_writes[#owner_writes + 1] = tostring(total + n)\n split_write(owner_writes)\n sliced('MSET', nil, owner_writes)\nend\nflush_queue()\nflush()\nif n > 0 then wake(ARGV[6 + n * 5], tostring(total + n), ARGV[2]) end\nreturn { 'ok', tostring(total + 1) }\n`\n\n/**\n * KEYS: head, history, event counter, event meta, pin counts, metadata split.\n * ARGV: nowMs, event pin key prefix, write count, then per write:\n * index, snapshot JSON, pin count, pin event indexes; finally the reducer\n * name. Writes from before the latest clear are dropped, and a head from\n * before it is replaced as if absent.\n */\nconst PUT_SNAPSHOTS_LUA = `\nlocal COUNTER, META, SPLIT = KEYS[3], KEYS[4], KEYS[6]\n${META_LUA}\nlocal counts = redis.call('MGET', COUNTER, SPLIT, CLEAR)\nlocal total = tonumber(counts[1] or '0')\nuse_split(counts[2], counts[1], counts[3])\nlocal pinned = {}\nlocal offset = 4\nfor i = 1, tonumber(ARGV[3]) do\n local pin_count = tonumber(ARGV[offset + 2])\n for p = 1, pin_count do\n local event_index = tonumber(ARGV[offset + 2 + p])\n if event_index > boundary and event_index <= total then\n pinned[#pinned + 1] = tostring(event_index)\n end\n end\n offset = offset + 3 + pin_count\nend\nlocal status = read_fields(pinned, { 'p', 'f' })\nlocal snapshots = {}\noffset = 4\nfor i = 1, tonumber(ARGV[3]) do\n local index = tonumber(ARGV[offset])\n local snapshot = ARGV[offset + 1]\n local pin_count = tonumber(ARGV[offset + 2])\n local ref = cjson.encode({ KEYS[2], tostring(index) })\n for p = 1, pin_count do\n local event_index = tonumber(ARGV[offset + 2 + p])\n local row = status[tostring(event_index)]\n if index >= boundary and event_index > boundary and event_index <= total and\n not row[1] and not row[2] then\n local pin_key = ARGV[2] .. tostring(event_index)\n if redis.call('SADD', pin_key, ref) == 1 then\n redis.call('HINCRBY', KEYS[5], ref, 1)\n end\n end\n end\n if index >= boundary then\n snapshots[#snapshots + 1] = { index = index, snapshot = snapshot, ref = ref }\n end\n offset = offset + 3 + pin_count\nend\nif #snapshots == 0 then return 1 end\n\nlocal head = redis.call('GET', KEYS[1])\nlocal head_index = nil\nlocal registered = false\nif head then\n local record = cjson.decode(head)\n head_index = tonumber(record['index'])\n registered = record['r'] == 1\nend\nif head_index and head_index < boundary then\n head, head_index, registered = nil, nil, false\n redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', '(' .. boundary)\nend\n-- Deletion finds each reducer's keys through this set instead of a scan.\n-- Heads written since carry the mark, so a reducer registers once.\nif not registered then\n redis.call('SADD', string.sub(COUNTER, 1, -6) .. 'snap-reducers', ARGV[#ARGV])\nend\nlocal latest = snapshots[#snapshots]\nlocal writes_head = not head_index or latest.index >= head_index\nlocal advances = not head_index or latest.index > head_index\nlocal final_head_index = advances and latest.index or head_index\n\nif advances and head and tonumber(redis.call('HGET', KEYS[5], cjson.encode({ KEYS[2], tostring(head_index) })) or '0') > 0 then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], head_index, head_index)\n redis.call('ZADD', KEYS[2], head_index, head)\nend\n\nfor _, write in ipairs(snapshots) do\n if write.index < final_head_index then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], write.index, write.index)\n if tonumber(redis.call('HGET', KEYS[5], write.ref) or '0') > 0 then\n redis.call('ZADD', KEYS[2], write.index, write.snapshot)\n end\n end\nend\n\nif writes_head then\n redis.call('SET', KEYS[1], latest.snapshot)\n redis.call('ZREMRANGEBYSCORE', KEYS[2], latest.index, latest.index)\nend\nreturn 1\n`\n\n/**\n * KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:\n * hashField, valueJson ('' deletes), patchFieldJson; finally channel and\n * message prefix. Wire fragments stay JSON text to preserve arrays\n * and numeric precision through Lua. Field-wise LWW by the sender's\n * `at`: a stored entry with a strictly newer stamp survives; ties go\n * to the incoming write (memory-tier parity). Returns the applied\n * 1-based entry positions. The whole-key PEXPIRE is the coarse bound,\n * sound because core sends one uniform ttlMs; the per-entry `e` stamp\n * is the fine bound (the injected storage clock at the applied set\n * plus ttlMs — never the sender stamp), pruned on read against the\n * same clock.\n */\nconst PRESENCE_SET_LUA = `\nlocal at = tonumber(ARGV[1])\nlocal applied = {}\nlocal published = {}\nlocal n = tonumber(ARGV[3])\nfor i = 1, n do\n local field = ARGV[3 + (i - 1) * 3 + 1]\n local value = ARGV[3 + (i - 1) * 3 + 2]\n local existing = redis.call('HGET', KEYS[1], field)\n if not existing or tonumber(cjson.decode(existing)['a']) <= at then\n if value == '' then\n redis.call('HDEL', KEYS[1], field)\n else\n redis.call('HSET', KEYS[1], field, value)\n end\n applied[#applied + 1] = tostring(i)\n published[#published + 1] = ARGV[3 + (i - 1) * 3 + 3]\n end\nend\nif #applied > 0 then\n redis.call('PEXPIRE', KEYS[1], ARGV[2])\n redis.pcall('PUBLISH', ARGV[4 + n * 3], ARGV[5 + n * 3] .. table.concat(published, ',') .. '}}')\nend\nreturn applied\n`\n\n/**\n * KEYS: presence hash. ARGV: nowMs (the injected clock, so tests can\n * time-travel expiry against a real server). Returns the live\n * [field, value, ...] pairs; entries at or past their fine-bound\n * expiry are HDELed in the same atomic step, so a stale reader can\n * never sweep a concurrently refreshed field.\n */\nconst PRESENCE_READ_LUA = `\nlocal now = tonumber(ARGV[1])\nlocal rows = redis.call('HGETALL', KEYS[1])\nlocal live = {}\nfor i = 1, #rows, 2 do\n if tonumber(cjson.decode(rows[i + 1])['e']) <= now then\n redis.call('HDEL', KEYS[1], rows[i])\n else\n live[#live + 1] = rows[i]\n live[#live + 1] = rows[i + 1]\n end\nend\nreturn live\n`\n\n/**\n * KEYS: head, history, event counter. ARGV: snapshot frontier or ''.\n * Returns one atomic multiversion-cache-plus-log-frontier read, with the\n * clear boundary; checkpoints from before it are ignored.\n */\nconst READ_STATE_LUA = `\nlocal counts = redis.call('MGET', KEYS[3], string.sub(KEYS[3], 1, -6) .. 'clear')\nlocal boundary = tonumber(counts[2] or '0')\nlocal head = redis.call('GET', KEYS[1])\nlocal head_index = head and cjson.decode(head)['index'] or nil\nif head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then\n return redis.error_reply('invalid snapshot head index')\nend\nif head_index and head_index < boundary then\n head = nil\n head_index = nil\nend\nlocal cap = ARGV[1]\nlocal snapshot = nil\nif head_index and (cap == '' or head_index <= tonumber(cap)) then\n snapshot = head\nelse\n local upper = cap == '' and '+inf' or cap\n local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, boundary, 'LIMIT', 0, 1)\n if #historical > 0 then snapshot = historical[1] end\nend\nreturn { head_index or '', snapshot or '', counts[1] or '0', tostring(boundary) }\n`\n\n/**\n * Removes every registered reducer's checkpoints below the latest clear, and\n * the pin counts that name them. Snapshot writes register each reducer in\n * {prefix}:{session}:snap-reducers, so no key scan is needed.\n */\nconst SWEEP_CHECKPOINTS_LUA = `\nlocal function sweep_checkpoints(session, boundary)\n for _, reducer in ipairs(redis.call('SMEMBERS', session .. 'snap-reducers')) do\n local head_key = session .. 'snap:' .. reducer\n local head = redis.call('GET', head_key)\n if head and tonumber(cjson.decode(head)['index']) < boundary then\n redis.call('DEL', head_key)\n end\n redis.call('ZREMRANGEBYSCORE', session .. 'snap-history:' .. reducer, '-inf', '(' .. boundary)\n end\n local counts_key = session .. 'snapshot-pin-counts'\n local counts = redis.call('HGETALL', counts_key)\n local stale = {}\n for i = 1, #counts, 2 do\n if tonumber(cjson.decode(counts[i])[2]) < boundary then stale[#stale + 1] = counts[i] end\n end\n if #stale > 0 then redis.call('HDEL', counts_key, unpack(stale)) end\nend\n`\n\n/**\n * KEYS: store stream, counter, metadata, metadata split, marker id ownership\n * key, pending, ready.\n * ARGV: sessionId, nowMs, ifLastIndex or '', cause index or '', cause\n * attempt, marker id, notify channel.\n * Returns ['ok', cleared, head, boundary] | ['no_parent', idx] |\n * ['superseded', idx] | ['foreign']. The old lifetime's log moves aside in one\n * RENAME, so every read of the log starts at the marker, and its queues are\n * unlinked, so claims and pending checks see only the new lifetime; deletion\n * drains the moved log.\n */\nconst CLEAR_LUA = `\nlocal COUNTER, META, SPLIT = KEYS[2], KEYS[3], KEYS[4]\n${META_LUA}\n${WAKE_LUA}\nlocal counts = redis.call('MGET', COUNTER, SPLIT, CLEAR, KEYS[5])\nlocal head = tonumber(counts[1] or '0')\nuse_split(counts[2], counts[1], counts[3])\nif ARGV[4] ~= '' then\n local field = ARGV[4]\n local pidx = tonumber(field)\n if pidx <= boundary then return { 'superseded', field } end\n if pidx < 1 or pidx > head then return { 'no_parent', field } end\n local row = read_fields({ field }, { 'd', 'f' })[field]\n if tonumber(row[1] or '0') ~= tonumber(ARGV[5]) or row[2] then\n return { 'superseded', field }\n end\nend\nlocal expected = ARGV[3]\nif expected ~= '' and boundary == tonumber(expected) + 1 then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif expected ~= '' and tonumber(expected) ~= head then\n return { 'ok', '0', tostring(head), tostring(boundary) }\nend\nif head == 0 or boundary == head then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif counts[4] then return { 'foreign' } end\nlocal field = tostring(head + 1)\nlocal session = string.sub(COUNTER, 1, -6)\nlocal moved = session .. 'cleared:' .. field\nredis.call('RENAME', KEYS[1], moved)\nredis.call('UNLINK', KEYS[6], KEYS[7])\nredis.call('SADD', session .. 'cleared', moved)\nredis.call('XADD', KEYS[1], field .. '-0',\n 'id', ARGV[6], 'type', '${CLEAR_EVENT_TYPE}', 'payload', 'null',\n 'created_at', ARGV[2], 'cause', '', 'lane', '')\nlocal writes = { KEYS[5], cjson.encode({ ARGV[1], head + 1 }), COUNTER, field, CLEAR, field }\nsplit_write(writes)\nredis.call('MSET', unpack(writes))\nput(mkey(field), field .. ':p', ARGV[2])\nflush()\nwake_add(field, ARGV[6], '${CLEAR_EVENT_TYPE}', 'null')\nwake(ARGV[7], field, ARGV[2])\nreturn { 'ok', '1', field, field }\n`\n\n/**\n * KEYS: counter. ARGV: page size.\n * The next deletion batch: a moved log and up to a page of its [index, id]\n * pairs, without payloads. Empty moved logs are dropped; with none left it\n * sweeps the old checkpoints and returns nothing, so a retry finishes an\n * interrupted deletion.\n */\nconst RECLAIM_BATCH_LUA = `\n${SWEEP_CHECKPOINTS_LUA}\nlocal session = string.sub(KEYS[1], 1, -6)\nlocal boundary = tonumber(redis.call('GET', session .. 'clear') or '0')\nif boundary == 0 then return {} end\nfor _, log in ipairs(redis.call('SMEMBERS', session .. 'cleared')) do\n local entries = redis.call('XRANGE', log, '-', '+', 'COUNT', ARGV[1])\n if #entries > 0 then\n local batch = { log }\n for _, entry in ipairs(entries) do\n local fields = entry[2]\n for i = 1, #fields, 2 do\n if fields[i] == 'id' then\n batch[#batch + 1] = string.match(entry[1], '^(%d+)')\n batch[#batch + 1] = fields[i + 1]\n break\n end\n end\n end\n return batch\n end\n redis.call('DEL', log)\n redis.call('SREM', session .. 'cleared', log)\nend\nsweep_checkpoints(session, boundary)\nreturn {}\n`\n\n/**\n * KEYS: moved log, counter, metadata, metadata split, then one id ownership\n * key per entry.\n * ARGV: sessionId, event pin key prefix, n, then per entry: index, id.\n * Deletes the listed entries below the latest clear and everything that\n * names them. An id record survives when it names another event, so an id\n * reused in the new lifetime keeps its owner. Returns [deleted, done].\n */\nconst RECLAIM_LUA = `\nlocal COUNTER, META, SPLIT = KEYS[2], KEYS[3], KEYS[4]\n${META_LUA}\n${SWEEP_CHECKPOINTS_LUA}\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal session = string.sub(COUNTER, 1, -6)\nlocal counts = redis.call('MGET', COUNTER, SPLIT, CLEAR)\nuse_split(counts[2], counts[1], counts[3])\nlocal fields, id_keys, indexes, stream_ids, pin_keys, deleted = {}, {}, {}, {}, {}, {}\n-- A concurrent reclaim may list the same batch; only XDEL says who removed it.\nlocal removed = 0\nfor i = 1, tonumber(ARGV[3]) do\n local field = ARGV[3 + (i - 1) * 2 + 1]\n if tonumber(field) < boundary then\n fields[#fields + 1] = field\n id_keys[#fields] = KEYS[4 + i]\n indexes[#fields] = tonumber(field)\n stream_ids[#fields] = field .. '-0'\n pin_keys[#fields] = ARGV[2] .. field\n deleted[field] = true\n end\nend\nif #fields > 0 then\n local lanes, seen, pointers = read_fields(fields, { 'l' }), {}, {}\n for _, field in ipairs(fields) do\n local lane = lanes[field][1]\n if lane and lane ~= '' and not seen[lane] then\n seen[lane] = true\n pointers[#pointers + 1] = lane_field(lane, 'head')\n pointers[#pointers + 1] = lane_field(lane, 'tail')\n end\n end\n if #pointers > 0 then\n local values = redis.call('HMGET', META, unpack(pointers))\n local stale = {}\n for i = 1, #pointers do\n if values[i] and deleted[values[i]] then stale[#stale + 1] = pointers[i] end\n end\n if #stale > 0 then redis.call('HDEL', META, unpack(stale)) end\n end\n local suffixes = { 'l', 'next', 'p', 'pa', 'ri', 'ch', 'ce', 'f', 'fc', 'lc', 'lf', 'lfa', 'd', 'a', 'e' }\n local columns, order = {}, {}\n for _, field in ipairs(fields) do\n local key = mkey(field)\n if not columns[key] then\n columns[key] = {}\n order[#order + 1] = key\n end\n for _, suffix in ipairs(suffixes) do\n columns[key][#columns[key] + 1] = field .. ':' .. suffix\n end\n end\n for _, key in ipairs(order) do sliced('HDEL', key, columns[key]) end\n sliced('DEL', nil, pin_keys)\n local owners, stale_ids = sliced('MGET', nil, id_keys), {}\n for i = 1, #fields do\n if owners[i] then\n local rec = cjson.decode(owners[i])\n if rec[1] == ARGV[1] and tonumber(rec[2]) == indexes[i] then\n stale_ids[#stale_ids + 1] = id_keys[i]\n end\n end\n end\n if #stale_ids > 0 then sliced('DEL', nil, stale_ids) end\n removed = redis.call('XDEL', KEYS[1], unpack(stream_ids))\nend\nlocal done = 0\nif redis.call('XLEN', KEYS[1]) == 0 then\n redis.call('DEL', KEYS[1])\n redis.call('SREM', session .. 'cleared', KEYS[1])\n if redis.call('SCARD', session .. 'cleared') == 0 then\n sweep_checkpoints(session, boundary)\n done = 1\n end\nend\nreturn { removed, done }\n`\n\n// ── wire helpers ─────────────────────────────────────────────────────\n\nexport type StreamEntry = [id: string, fields: string[]]\n\nconst fieldMap = (fields: string[]): Record<string, string> => {\n const map: Record<string, string> = {}\n for (let i = 0; i + 1 < fields.length; i += 2) {\n map[fields[i]!] = fields[i + 1]!\n }\n return map\n}\n\nconst STORED_EVENT_META_SUFFIXES = [\n 'p',\n 'pa',\n 'ri',\n 'ch',\n 'ce',\n 'f',\n 'fc',\n 'lc',\n 'lf',\n 'lfa',\n 'd',\n 'a',\n 'e',\n] as const\n\nconst entryIndex = (entryId: string): number =>\n Number(entryId.slice(0, entryId.indexOf('-')))\n\nconst toEvent = (sessionId: string, entry: StreamEntry): Event => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payload: JSON.parse(fields['payload']!),\n index: entryIndex(entry[0]),\n sessionId,\n createdAt: new Date(Number(fields['created_at'])),\n }\n}\n\ntype ByteStreamEntry = [Uint8Array, Uint8Array[]]\n\nconst fieldBytes = (name: string): Uint8Array => new TextEncoder().encode(name)\nconst ID_FIELD = fieldBytes('id')\nconst TYPE_FIELD = fieldBytes('type')\nconst PAYLOAD_FIELD = fieldBytes('payload')\nconst CREATED_AT_FIELD = fieldBytes('created_at')\n// A leading U+FEFF is part of a stored id or type, never a byte-order mark.\nconst utf8 = new TextDecoder('utf-8', { ignoreBOM: true })\n\nconst isField = (name: Uint8Array, field: Uint8Array): boolean => {\n if (name.length !== field.length) return false\n for (let at = 0; at < field.length; at++)\n if (name[at] !== field[at]) return false\n return true\n}\n\nconst bytesToSerializedEvent = (\n sessionId: string,\n entry: ByteStreamEntry,\n): SerializedEvent => {\n const values = entry[1]\n let id: string | undefined\n let type: string | undefined\n let payloadJson: string | undefined\n let createdAt: string | undefined\n for (let at = 0; at + 1 < values.length; at += 2) {\n const name = values[at]!\n if (isField(name, ID_FIELD)) id = utf8.decode(values[at + 1])\n else if (isField(name, TYPE_FIELD)) type = utf8.decode(values[at + 1])\n else if (isField(name, PAYLOAD_FIELD))\n payloadJson = utf8.decode(values[at + 1])\n else if (isField(name, CREATED_AT_FIELD))\n createdAt = utf8.decode(values[at + 1])\n }\n return {\n id: id!,\n type: type!,\n payloadJson: payloadJson!,\n index: entryIndex(utf8.decode(entry[0])),\n sessionId,\n createdAtMs: Number(createdAt),\n }\n}\n\nconst toSerializedEvent = (\n sessionId: string,\n entry: StreamEntry,\n): SerializedEvent => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payloadJson: fields['payload']!,\n index: entryIndex(entry[0]),\n sessionId,\n createdAtMs: Number(fields['created_at']),\n }\n}\n\nconst json = (value: unknown): string => JSON.stringify(value) ?? 'null'\n\nconst toCause = (raw: string | undefined): EventCause | null => {\n if (raw === '' || raw === undefined) return null\n const value = JSON.parse(raw) as {\n index?: unknown\n attempt?: unknown\n batchSize?: unknown\n }\n if (\n !Number.isInteger(value.index) ||\n Number(value.index) < 1 ||\n !Number.isInteger(value.attempt) ||\n Number(value.attempt) < 1\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n if (\n value.batchSize !== undefined &&\n (!Number.isInteger(value.batchSize) || Number(value.batchSize) < 1)\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n return {\n index: Number(value.index),\n attempt: Number(value.attempt),\n ...(value.batchSize === undefined\n ? {}\n : { batchSize: Number(value.batchSize) }),\n }\n}\n\nconst toClaimAvailableResult = (\n sessionId: string,\n reply: unknown[],\n): StoreClaimAvailableResult => {\n const outcome = String(reply[0])\n if (outcome === 'claimed') {\n const count = Number(reply[1])\n const events: StoredEvent[] = []\n for (let i = 0; i < count; i++) {\n const offset = 2 + i * 10\n const entry = reply[offset + 9] as StreamEntry\n const event = toEvent(sessionId, entry)\n const fields = fieldMap(entry[1])\n events.push(\n Object.assign(event, {\n cause: toCause(fields['cause']),\n lane: fields['lane'] === '' ? null : fields['lane']!,\n processedAt: null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt:\n reply[offset + 3] === null\n ? null\n : new Date(Number(reply[offset + 3])),\n lastClaimedAt: new Date(Number(reply[offset + 4])),\n attemptCount: Number(reply[offset]),\n claimHolder: String(reply[offset + 7]),\n claimExpiresAt: new Date(Number(reply[offset + 8])),\n failureCount: Number(reply[offset + 1]),\n lastFailedAt:\n reply[offset + 5] === null\n ? null\n : new Date(Number(reply[offset + 5])),\n lastFailedAttempt:\n reply[offset + 6] === null ? null : Number(reply[offset + 6]),\n lastError:\n reply[offset + 2] === null ? null : String(reply[offset + 2]),\n failedAt: null,\n }),\n )\n }\n const clearIndex = Number(reply[2 + count * 10] ?? 0)\n if (clearIndex > 0)\n for (const event of events) event.clearIndex = clearIndex\n return {\n outcome: 'claimed',\n events,\n }\n }\n if (outcome === 'busy') {\n return { outcome, retryAt: new Date(Number(reply[1])) }\n }\n if (outcome === 'settled') return { outcome }\n throw new TypeError(\n `claim referenced a missing event in session '${sessionId}'`,\n )\n}\n\nconst toIndex = ({\n raw,\n sessionId,\n name,\n}: {\n raw: unknown\n sessionId: string\n name: string\n}): number => {\n const index = Number(raw ?? 0)\n if (!Number.isSafeInteger(index) || index < 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis ${name} for session '${sessionId}' is invalid`,\n )\n }\n return index\n}\n\n/**\n * A range read that met a gap. A clear moves the old lifetime's log aside, so\n * a read whose bounds predate it finds nothing where it expected events;\n * anywhere else a gap is corruption.\n */\nconst rangeGap = (sessionId: string, message: string): A2Error =>\n Object.assign(\n new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ${message}`,\n ),\n { rangeGap: true },\n )\n\nconst isRangeGap = (error: unknown): boolean =>\n error instanceof A2Error &&\n (error as { rangeGap?: boolean }).rangeGap === true\n\n/**\n * Run a whole read, bounds included. A gap means a clear raced it, so the\n * read runs once more from fresh bounds. A second gap, from another clear or\n * from corruption, surfaces as the retryable STORE_UNAVAILABLE.\n */\nconst retryAfterGap = async <T>(read: () => Promise<T>): Promise<T> => {\n try {\n return await read()\n } catch (error) {\n if (!isRangeGap(error)) throw error\n return read()\n }\n}\n\nconst wrap = async <T>(fn: () => Promise<T>): Promise<T> => {\n try {\n return await fn()\n } catch (err) {\n if (err instanceof A2Error || err instanceof TypeError) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'redis store operation failed', {\n cause: err,\n })\n }\n}\n\nexport function createRedisStoreCore(\n options: RedisStoreCoreOptions,\n): RedisStoreCore {\n const { call, callBytes, clock } = options\n const generateId = options.ids\n const prefix = options.keyPrefix\n\n const storeKey = (s: string): string => `${prefix}:${s}:log`\n const countKey = (s: string): string => `${prefix}:${s}:count`\n const metaKey = (s: string): string => `${prefix}:${s}:meta`\n const metaChunkKey = (s: string, index: number): string =>\n `${metaKey(s)}:${Math.floor((index - 1) / META_CHUNK_SIZE)}`\n const metaSplitKey = (s: string): string => `${prefix}:${s}:meta-split`\n const pendingKey = (s: string): string => `${prefix}:${s}:pending`\n const readyKey = (s: string): string => `${prefix}:${s}:ready`\n const snapKey = (s: string, r: string): string => `${prefix}:${s}:snap:${r}`\n const snapHistoryKey = (s: string, r: string): string =>\n `${prefix}:${s}:snap-history:${r}`\n const snapshotEventPinsKey = (s: string, index: number): string =>\n `${prefix}:${s}:snapshot-pins:${index}`\n const snapshotEventPinsPrefix = (s: string): string =>\n `${prefix}:${s}:snapshot-pins:`\n const snapshotPinCountsKey = (s: string): string =>\n `${prefix}:${s}:snapshot-pin-counts`\n const presenceKey = (s: string): string => `${prefix}:${s}:presence`\n // Scripts derive this from the counter key; keep the two in step.\n const clearKey = (s: string): string => `${prefix}:${s}:clear`\n // One ownership key per event id; reclaim deletes those naming cleared events.\n // Session ids are raw and may contain colons, so a raw id could name a\n // session key (`ids:count`). A JSON string with escaped colons starts with\n // a quote and has no colon, so it never matches a session key's suffix.\n const idKey = (id: string): string =>\n `${prefix}:ids:${JSON.stringify(id).replaceAll(':', '\\\\u003a')}`\n\n const evalScript = (\n script: string,\n keys: string[],\n args: Array<string | number>,\n ): Promise<unknown> => call('EVAL', script, keys.length, ...keys, ...args)\n\n const readRangeBounds = async (\n sessionId: string,\n ): Promise<{ frontier: number; metaSplit: number; clearIndex: number }> => {\n const [count, split, cleared] = (await call(\n 'MGET',\n countKey(sessionId),\n metaSplitKey(sessionId),\n clearKey(sessionId),\n )) as [unknown, unknown, unknown]\n const frontier = toIndex({ raw: count, sessionId, name: 'event count' })\n return {\n frontier,\n metaSplit:\n split === null\n ? frontier\n : toIndex({ raw: split, sessionId, name: 'metadata split' }),\n clearIndex: toIndex({ raw: cleared, sessionId, name: 'clear boundary' }),\n }\n }\n\n const readRangePage = async ({\n sessionId,\n afterIndex,\n throughIndex,\n metaSplit,\n }: {\n sessionId: string\n afterIndex: number\n throughIndex: number\n metaSplit: number\n }): Promise<StoredEvent[]> => {\n if (throughIndex <= afterIndex) return []\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n `${throughIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) return []\n\n const fieldsByKey = new Map<string, string[]>()\n for (const entry of entries) {\n const index = entryIndex(entry[0])\n const key =\n index <= metaSplit ? metaKey(sessionId) : metaChunkKey(sessionId, index)\n const fields = fieldsByKey.get(key) ?? []\n fields.push(\n ...STORED_EVENT_META_SUFFIXES.map((suffix) => `${index}:${suffix}`),\n )\n fieldsByKey.set(key, fields)\n }\n const lookups = [...fieldsByKey]\n const results = await Promise.allSettled(\n lookups.map(([key, fields]) => call('HMGET', key, ...fields)),\n )\n const meta: Record<string, string> = {}\n for (let position = 0; position < lookups.length; position += 1) {\n const result = results[position]!\n if (result.status === 'rejected') throw result.reason\n const fields = lookups[position]![1]\n const values = (result.value as Array<string | null> | null) ?? []\n for (let i = 0; i < fields.length; i += 1) {\n const value = values[i]\n if (value !== null && value !== undefined) {\n meta[fields[i]!] = value\n }\n }\n }\n return entries.map((entry) => {\n const event = toEvent(sessionId, entry)\n const idx = event.index\n const fields = fieldMap(entry[1])\n const processedAt = meta[`${idx}:p`]\n const processedByAttempt = meta[`${idx}:pa`]\n const returnedEventIds = meta[`${idx}:ri`]\n const claimHolder = meta[`${idx}:ch`]\n const claimExpiresAt = meta[`${idx}:ce`]\n const failedAt = meta[`${idx}:f`]\n const firstClaimedAt = meta[`${idx}:fc`]\n const lastClaimedAt = meta[`${idx}:lc`]\n const lastFailedAt = meta[`${idx}:lf`]\n const lastFailedAttempt = meta[`${idx}:lfa`]\n return Object.assign(event, {\n cause: toCause(fields['cause']),\n lane: fields['lane'] === '' ? null : fields['lane']!,\n processedAt:\n processedAt === undefined ? null : new Date(Number(processedAt)),\n processedByAttempt:\n processedByAttempt === undefined ? null : Number(processedByAttempt),\n returnedEventIds:\n returnedEventIds === undefined\n ? null\n : decodeReturnedEventIds(returnedEventIds),\n firstClaimedAt:\n firstClaimedAt === undefined\n ? null\n : new Date(Number(firstClaimedAt)),\n lastClaimedAt:\n lastClaimedAt === undefined ? null : new Date(Number(lastClaimedAt)),\n attemptCount: Number(meta[`${idx}:d`] ?? 0),\n claimHolder: claimHolder ?? null,\n claimExpiresAt:\n claimExpiresAt === undefined\n ? null\n : new Date(Number(claimExpiresAt)),\n failureCount: Number(meta[`${idx}:a`] ?? 0),\n lastFailedAt:\n lastFailedAt === undefined ? null : new Date(Number(lastFailedAt)),\n lastFailedAttempt:\n lastFailedAttempt === undefined ? null : Number(lastFailedAttempt),\n lastError: meta[`${idx}:e`] ?? null,\n failedAt: failedAt === undefined ? null : new Date(Number(failedAt)),\n })\n })\n }\n\n const readEventPages = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n if (throughIndex <= afterIndex) return []\n const events: Event[] = []\n let nextIndex = afterIndex + 1\n while (nextIndex <= throughIndex) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${nextIndex}-0`,\n `${throughIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) {\n throw rangeGap(sessionId, `ended before index ${throughIndex}`)\n }\n for (let position = 0; position < entries.length; position += 1) {\n const index = entryIndex(entries[position]![0])\n if (index !== nextIndex + position || index > throughIndex) {\n throw rangeGap(sessionId, 'is not contiguous')\n }\n }\n events.push(...entries.map((entry) => toEvent(sessionId, entry)))\n nextIndex = events.at(-1)!.index + 1\n }\n return events\n }\n\n const readEventRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n const events: Event[] = []\n for (\n let start = afterIndex;\n start < throughIndex;\n start += READ_PAGE_SIZE * READ_CONCURRENCY\n ) {\n const ranges = Array.from(\n {\n length: Math.min(\n READ_CONCURRENCY,\n Math.ceil((throughIndex - start) / READ_PAGE_SIZE),\n ),\n },\n (_, position) => {\n const after = start + position * READ_PAGE_SIZE\n return readEventPages(\n sessionId,\n after,\n Math.min(after + READ_PAGE_SIZE, throughIndex),\n )\n },\n )\n // oxlint-disable-next-line no-await-in-loop -- bound in-flight reads and settle every page before returning\n const results = await Promise.allSettled(ranges)\n for (const result of results) {\n if (result.status === 'rejected') throw result.reason\n events.push(...result.value)\n }\n }\n return events\n }\n\n /** Bounded XRANGE pages restore the full-read contract on capped providers. */\n const readRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]> =>\n (await readBoundedRange(sessionId, afterIndex, throughIndex)).rows\n\n /** `readRange`, with the clear boundary its rows start at. */\n const readBoundedRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<{ rows: StoredEvent[]; clearIndex: number }> => {\n if (throughIndex !== undefined && throughIndex <= afterIndex)\n return { rows: [], clearIndex: 0 }\n const {\n frontier: currentFrontier,\n metaSplit,\n clearIndex,\n } = await readRangeBounds(sessionId)\n const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier)\n // Reads start at the latest clear marker.\n const after = Math.max(afterIndex, clearIndex - 1)\n if (frontier <= after) return { rows: [], clearIndex }\n\n const rows: StoredEvent[] = []\n let nextIndex = after + 1\n while (nextIndex <= frontier) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const page = await readRangePage({\n sessionId,\n afterIndex: nextIndex - 1,\n throughIndex: frontier,\n metaSplit,\n })\n if (page.length === 0) {\n throw rangeGap(sessionId, `ended before index ${frontier}`)\n }\n for (let position = 0; position < page.length; position += 1) {\n if (page[position]!.index !== nextIndex + position) {\n throw rangeGap(sessionId, 'is not contiguous')\n }\n }\n const lastIndex = page.at(-1)!.index\n if (lastIndex < nextIndex || lastIndex > frontier) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' did not advance`,\n )\n }\n rows.push(...page)\n nextIndex = lastIndex + 1\n }\n return { rows, clearIndex }\n }\n\n const readStream = options.readStream\n return {\n ...(readStream\n ? {\n streamEvents: async function* ({\n sessionId,\n afterIndex,\n signal,\n }: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }): AsyncGenerator<SerializedEvent[], void> {\n try {\n for await (const entries of readStream({\n key: storeKey(sessionId),\n from: `${afterIndex + 1}-0`,\n signal,\n })) {\n signal.throwIfAborted()\n yield entries.map((entry) =>\n toSerializedEvent(sessionId, entry),\n )\n }\n } catch (cause) {\n if (cause instanceof A2Error) throw cause\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n },\n }\n : {}),\n async append(sessionId, events, appendOptions) {\n return wrap(async () => {\n const supplied = events.filter((e) => e.id !== undefined)\n const suppliedIds = supplied.map((e) => e.id as string)\n if (new Set(suppliedIds).size !== suppliedIds.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n const now = clock.now().getTime()\n const withIds = events.map((e) => ({\n id: e.id ?? generateId(),\n type: e.type,\n payload: e.payload,\n cause: e.cause ? { ...e.cause } : null,\n lane: e.lane ?? null,\n settled: e.settled === true,\n }))\n const args: Array<string | number> = [\n sessionId,\n now,\n withIds.length,\n ...withIds.flatMap((e) => [\n e.id,\n e.type,\n json(e.payload),\n e.cause ? json(e.cause) : '',\n e.lane ?? '',\n e.settled ? '1' : '0',\n ]),\n `${prefix}:${sessionId}:notify`,\n appendOptions?.originIndex ?? '',\n ]\n const reply = (await evalScript(\n APPEND_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n metaSplitKey(sessionId),\n ...withIds.map((e) => idKey(e.id)),\n ],\n args,\n )) as string[]\n\n const [verdict] = reply\n if (verdict === 'empty') {\n return { events: [], hasPending: reply[1] === '1' }\n }\n if (verdict === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${reply[1]}' already exists in another session`,\n )\n }\n if (verdict === 'no_parent') {\n throw new TypeError(\n `no event at index ${reply[1]} in session '${sessionId}'`,\n )\n }\n if (verdict === 'cleared') {\n throw sessionClearedError(\n `scheduled work from event ${reply[1]} was cleared in session '${sessionId}'`,\n )\n }\n if (verdict === 'superseded') {\n throw new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `a newer attempt owns event ${reply[1]} in session '${sessionId}'`,\n )\n }\n if (verdict === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${reply[1]} already-appended and ${events.length - Number(reply[1])} fresh events`,\n )\n }\n if (verdict === 'dup') {\n const hasPending = reply[1] === '1'\n const firstIndex = Math.min(...reply.slice(2).map(Number))\n // A valid retry occupies exactly events.length consecutive indexes.\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n if (firstIndex < clearIndex) {\n // Cleared after the script matched the retry: the ids are free.\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `session '${sessionId}' was cleared while an append retry replayed; retry it`,\n )\n }\n return {\n events: idempotentReplay(events, existing),\n hasPending,\n }\n }\n const base = Number(reply[1]) - 1\n return {\n events: withIds.map((e, i) => ({\n id: e.id,\n type: e.type,\n payload: structuredClone(e.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause: e.cause,\n lane: e.lane,\n processedAt: e.settled ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n claimHolder: null,\n claimExpiresAt: null,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n })),\n hasPending: reply[2] === '1',\n }\n })\n },\n\n async read(sessionId, opts) {\n return wrap(() =>\n retryAfterGap(() =>\n readRange(sessionId, opts?.afterIndex ?? 0, opts?.throughIndex),\n ),\n )\n },\n\n async readHistory(sessionId, opts) {\n const after = opts?.afterIndex ?? 0\n if (opts?.throughIndex !== undefined && opts.throughIndex <= after)\n return []\n return wrap(() =>\n retryAfterGap(async () => {\n const [count, cleared] = (await call(\n 'MGET',\n countKey(sessionId),\n clearKey(sessionId),\n )) as [unknown, unknown]\n const frontier = toIndex({\n raw: count,\n sessionId,\n name: 'event count',\n })\n const clearIndex = toIndex({\n raw: cleared,\n sessionId,\n name: 'clear boundary',\n })\n // Reads start at the latest clear marker.\n return readEventRange(\n sessionId,\n Math.max(after, clearIndex - 1),\n Math.min(opts?.throughIndex ?? frontier, frontier),\n )\n }),\n )\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n excludeIndexes,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const excluded = excludeIndexes ?? []\n const reply = (await evalScript(\n CLAIM_AVAILABLE_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n metaSplitKey(sessionId),\n ],\n [holder, now, expiresAt, excluded.length, ...excluded],\n )) as unknown[]\n return toClaimAvailableResult(sessionId, reply)\n })\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const reply = (await evalScript(\n RENEW_CLAIMS_LUA,\n [countKey(sessionId), metaKey(sessionId), metaSplitKey(sessionId)],\n [\n holder,\n now,\n expiresAt,\n claims.length,\n ...claims.flatMap((claim) => [claim.index, claim.attempt]),\n ],\n )) as string[]\n const renewedCount = Number(reply[0])\n const values = reply.slice(1).map(Number)\n return {\n renewed: values.slice(0, renewedCount).toSorted((a, b) => a - b),\n superseded: values.slice(renewedCount).toSorted((a, b) => a - b),\n }\n })\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n return wrap(async () => {\n const ids = events.map((event) => event.id)\n if (new Set(ids).size !== ids.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n const now = clock.now().getTime()\n const reply = (await evalScript(\n COMPLETE_ATTEMPT_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n snapshotEventPinsKey(sessionId, index),\n snapshotPinCountsKey(sessionId),\n metaSplitKey(sessionId),\n ...ids.map((id) => idKey(id)),\n ],\n [\n sessionId,\n now,\n index,\n attempt,\n events.length,\n ...events.flatMap((event) => [\n event.id,\n event.type,\n json(event.payload),\n event.lane ?? '',\n event.settled === true ? '1' : '0',\n ]),\n `${prefix}:${sessionId}:notify`,\n ],\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n if (outcome === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event ids do not match the completed attempt',\n )\n }\n if (outcome === 'superseded') return { outcome }\n\n const cause = { index, attempt }\n if (outcome === 'ok') {\n const base = Number(reply[1]) - 1\n return {\n outcome: 'completed',\n events: events.map((event, i) => ({\n id: event.id,\n type: event.type,\n payload: structuredClone(event.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause,\n lane: event.lane ?? null,\n processedAt: event.settled === true ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n claimHolder: null,\n claimExpiresAt: null,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n })),\n } satisfies CompleteAttemptResult\n }\n if (outcome === 'duplicate') {\n if (events.length === 0) {\n return { outcome: 'completed', events: [] }\n }\n const firstIndex = Math.min(...reply.slice(1).map(Number))\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n // Cleared after the script matched the retry.\n if (index <= clearIndex) return { outcome: 'superseded' }\n return {\n outcome: 'completed',\n events: idempotentReplay(events, existing),\n }\n }\n throw new TypeError(`unexpected completion outcome '${outcome}'`)\n })\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n return wrap(async () => {\n const reply = (await evalScript(\n FAIL_ATTEMPT_LUA,\n [\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n snapshotEventPinsKey(sessionId, index),\n snapshotPinCountsKey(sessionId),\n metaSplitKey(sessionId),\n ],\n [index, attempt, error, maxFailures, clock.now().getTime()],\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'cleared')\n return { outcome: 'superseded', failureCount: 0 }\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n return {\n outcome: outcome as 'failed' | 'dead_lettered' | 'superseded',\n failureCount: Number(reply[1]),\n }\n })\n },\n\n async readState(sessionId, reducerName, stateOptions) {\n return wrap(() =>\n retryAfterGap(async () => {\n const reply = (await evalScript(\n READ_STATE_LUA,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n countKey(sessionId),\n ],\n [\n stateOptions?.snapshotThroughIndex ??\n stateOptions?.throughIndex ??\n '',\n ],\n )) as [number | string, string, string, string]\n const rawHead = reply[0]\n const raw = reply[1]\n const headIndex = rawHead === '' ? null : Number(rawHead)\n const parsed =\n raw === ''\n ? null\n : (JSON.parse(raw) as { index: number; state: unknown })\n const currentFrontier = Number(reply[2])\n if (\n !Number.isSafeInteger(currentFrontier) ||\n currentFrontier < 0 ||\n (headIndex !== null &&\n (!Number.isSafeInteger(headIndex) || headIndex < 0)) ||\n (parsed !== null &&\n (!Number.isSafeInteger(parsed.index) || parsed.index < 0))\n ) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis state frontier for session '${sessionId}' is invalid`,\n )\n }\n const frontier = Math.min(\n currentFrontier,\n stateOptions?.throughIndex ?? currentFrontier,\n )\n const snapshot = parsed\n const clearIndex = Number(reply[3] ?? 0)\n // The fold starts at the latest clear marker when no checkpoint is newer.\n const events = await readEventRange(\n sessionId,\n Math.max(snapshot?.index ?? 0, clearIndex - 1),\n frontier,\n )\n return {\n ...(clearIndex > 0 ? { clearIndex } : {}),\n headIndex,\n snapshot: snapshot\n ? { index: snapshot.index, state: snapshot.state }\n : null,\n events,\n }\n }),\n )\n },\n\n async putSnapshots(sessionId, reducerName, writes) {\n const byIndex = new Map<\n number,\n { index: number; state: unknown; pinEventIndexes: Set<number> }\n >()\n for (const write of writes) {\n const existing = byIndex.get(write.index)\n if (existing) {\n existing.state = write.state\n for (const eventIndex of write.pinEventIndexes ?? []) {\n existing.pinEventIndexes.add(eventIndex)\n }\n } else {\n byIndex.set(write.index, {\n index: write.index,\n state: write.state,\n pinEventIndexes: new Set(write.pinEventIndexes ?? []),\n })\n }\n }\n const normalized = [...byIndex.values()].toSorted(\n (a, b) => a.index - b.index,\n )\n if (normalized.length === 0) return\n await wrap(async () => {\n const args: Array<string | number> = [\n clock.now().getTime(),\n snapshotEventPinsPrefix(sessionId),\n normalized.length,\n ]\n for (const write of normalized) {\n args.push(\n write.index,\n json({\n index: write.index,\n state: write.state,\n updatedAt: clock.now().getTime(),\n // This reducer is in the session's registry (PUT_SNAPSHOTS_LUA).\n r: 1,\n }),\n write.pinEventIndexes.size,\n ...write.pinEventIndexes,\n )\n }\n await evalScript(\n PUT_SNAPSHOTS_LUA,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n countKey(sessionId),\n metaKey(sessionId),\n snapshotPinCountsKey(sessionId),\n metaSplitKey(sessionId),\n ],\n [...args, reducerName],\n )\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n return wrap(async () => {\n const entries = Object.entries(values)\n if (entries.length === 0) return null\n const atMs = meta.at.getTime()\n // Expiry anchors on the injected storage clock — the sender's\n // `a` stamp orders writes, the `e` stamp bounds their\n // lifetime, and the two clocks never mix.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n // The hash field is JSON `[participant, field]`: both are\n // caller strings, so no separator character is\n // collision-safe — the JSON array is.\n const applied = (await evalScript(\n PRESENCE_SET_LUA,\n [presenceKey(ns)],\n [\n atMs,\n meta.ttlMs,\n entries.length,\n ...entries.flatMap(([field, value]) => [\n JSON.stringify([participant, field]),\n value === null\n ? ''\n : json({\n v: value,\n s: meta.seen,\n a: atMs,\n e: expiresAtMs,\n }),\n `${json(field)}:${json(value)}`,\n ]),\n presenceKey(ns),\n `${json({ src: options.presenceSource, participant, seen: meta.seen, at: atMs }).slice(0, -1)},\"values\":{`,\n ],\n )) as string[]\n if (applied.length === 0) return null\n // Field-keyed and caller-named — null-prototype, like every\n // presence map (see `nullProtoRecord`).\n const appliedValues: Record<string, unknown> = nullProtoRecord()\n for (const position of applied) {\n const [field, value] = entries[Number(position) - 1]!\n appliedValues[field] = structuredClone(value)\n }\n return {\n participant,\n values: appliedValues,\n seen: meta.seen,\n at: new Date(atMs),\n }\n })\n },\n\n async read(ns) {\n return wrap(async () => {\n const reply = (await evalScript(\n PRESENCE_READ_LUA,\n [presenceKey(ns)],\n [clock.now().getTime()],\n )) as string[]\n const rows: PresenceRow[] = []\n for (let i = 0; i + 1 < reply.length; i += 2) {\n const [participant, field] = JSON.parse(reply[i]!) as [\n string,\n string,\n ]\n const entry = JSON.parse(reply[i + 1]!) as {\n v: unknown\n s: number\n a: number\n e: number\n }\n rows.push({\n participant,\n field,\n value: entry.v,\n seen: entry.s,\n at: new Date(entry.a),\n expiresAt: new Date(entry.e),\n })\n }\n return rows\n })\n },\n },\n\n async readRange(sessionId, afterIndex, throughIndex) {\n return wrap(() => readRange(sessionId, afterIndex, throughIndex))\n },\n\n async readTail(sessionId, fromIndex, throughIndex) {\n return wrap(async () => {\n const newest = (await call(\n 'XREVRANGE',\n storeKey(sessionId),\n throughIndex === undefined ? '+' : `${throughIndex}-0`,\n `${fromIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n const page = newest\n .toReversed()\n .map((entry) => toEvent(sessionId, entry))\n for (let position = 1; position < page.length; position += 1) {\n if (page[position]!.index !== page[position - 1]!.index + 1) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n const oldest = page[0]?.index\n return oldest === undefined || oldest <= fromIndex ? page : null\n })\n },\n\n async clear({ sessionId, ifLastIndex, cause }) {\n return wrap(async () => {\n const markerId = generateId()\n const reply = (await evalScript(\n CLEAR_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n metaKey(sessionId),\n metaSplitKey(sessionId),\n idKey(markerId),\n pendingKey(sessionId),\n readyKey(sessionId),\n ],\n [\n sessionId,\n clock.now().getTime(),\n ifLastIndex ?? '',\n cause?.index ?? '',\n cause?.attempt ?? '',\n markerId,\n `${prefix}:${sessionId}:notify`,\n ],\n )) as string[]\n if (reply[0] === 'superseded') {\n throw supersededAttemptError(sessionId, cause!)\n }\n if (reply[0] === 'no_parent') {\n throw new TypeError(\n `no event at index ${reply[1]} in session '${sessionId}'`,\n )\n }\n if (reply[0] === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `the clear marker id '${markerId}' already exists`,\n )\n }\n return {\n cleared: reply[1] === '1',\n index: Number(reply[2]),\n clearIndex: Number(reply[3]),\n }\n })\n },\n\n async reclaim(sessionId) {\n return wrap(async () => {\n const [log, ...pairs] = (await evalScript(\n RECLAIM_BATCH_LUA,\n [countKey(sessionId)],\n [READ_PAGE_SIZE],\n )) as string[]\n if (log === undefined) return { deleted: 0, done: true }\n // Id keys are built here, like append's: they use JSON.stringify,\n // whose escaping cjson does not reproduce.\n const ids: string[] = []\n for (let i = 1; i < pairs.length; i += 2) ids.push(pairs[i]!)\n const [deleted, done] = (await evalScript(\n RECLAIM_LUA,\n [\n log,\n countKey(sessionId),\n metaKey(sessionId),\n metaSplitKey(sessionId),\n ...ids.map((id) => idKey(id)),\n ],\n [sessionId, snapshotEventPinsPrefix(sessionId), ids.length, ...pairs],\n )) as [number, number]\n return { deleted: Number(deleted), done: Number(done) === 1 }\n })\n },\n\n async readEvents(sessionId, afterIndex) {\n return wrap(async () => {\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n return entries.map((entry) => toEvent(sessionId, entry))\n })\n },\n async readFeedPage(sessionId, afterIndex) {\n return wrap(async () => {\n const range = [\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n 'COUNT',\n FEED_PAGE_SIZE,\n ] as const\n if (callBytes) {\n const entries = (await callBytes(\n 'XRANGE',\n ...range,\n )) as ByteStreamEntry[]\n return entries.map((entry) =>\n bytesToSerializedEvent(sessionId, entry),\n )\n }\n const entries = (await call('XRANGE', ...range)) as StreamEntry[]\n return entries.map((entry) => toSerializedEvent(sessionId, entry))\n })\n },\n }\n}\n","import type { PresencePatch } from './contract.ts'\nimport { A2Error } from './errors.ts'\nimport { NOTIFY_TIMINGS, stateTailReaders } from './internal.ts'\nimport { defaultSleep, pollingStream, type Sleeper } from './store-polling.ts'\nimport { pollPresence } from './store-presence-polling.ts'\nimport { presenceRecovery } from './presence-recovery.ts'\nimport { createStreamActivity, streamActivities } from './stream-activity.ts'\nimport {\n bufferedEvents,\n isSerializedEvent,\n parseSerializedEvent,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport {\n createRedisStoreCore,\n FEED_PAGE_SIZE,\n type RedisCall,\n type RedisStreamRead,\n} from './store-redis-core.ts'\nimport type { A2Store, Clock, Event, IdSource } from './store.ts'\n\nexport type RedisConnection = {\n call(command: string, ...args: Array<string | number>): Promise<unknown>\n duplicate(): RedisConnection\n on(\n event: 'message',\n listener: (channel: string, message: string) => void,\n ): unknown\n disconnect(): void\n}\n\ntype PresenceWireMessage = {\n src: string\n participant: string\n values: Record<string, unknown>\n seen: number\n at: number\n}\n\ntype PresenceSubscription = {\n listeners: Set<(patch: PresencePatch) => void>\n poll?: ReturnType<typeof pollPresence> | undefined\n recovery?: ReturnType<typeof presenceRecovery> | undefined\n}\n\n/**\n * One live event feed's view of its channel: wake-up messages received\n * since it subscribed, whether it must catch up with XRANGE before trusting\n * them, and the resolver of its park, if parked.\n */\ntype Feed = {\n inbox: string[]\n bytes: number\n dirty: boolean\n wake: (() => void) | null\n stopRead: () => Promise<void>\n}\n\n/** A feed that falls this far behind drops its inbox and catches up. */\nconst INBOX_BYTES = 512 * 1024\n\ntype WakeEntry = [index: number, id: string, type: string, payload: unknown]\n\n/**\n * Parses a wake-up message: `{\"f\":frontier}` or a bare frontier number\n * announces events without carrying them; `{\"f\",\"t\",\"e\"}` carries them.\n * Returns null for anything else, which callers treat as a hint to catch up.\n */\nconst parseWake = (\n message: string,\n): { frontier: number; createdAt: number; entries: WakeEntry[] } | null => {\n let parsed: unknown\n try {\n parsed = JSON.parse(message)\n } catch {\n return null\n }\n if (typeof parsed === 'number') {\n return Number.isSafeInteger(parsed)\n ? { frontier: parsed, createdAt: 0, entries: [] }\n : null\n }\n if (typeof parsed !== 'object' || parsed === null) return null\n const { f, t, e } = parsed as { f?: unknown; t?: unknown; e?: unknown }\n if (typeof f !== 'number' || !Number.isSafeInteger(f)) return null\n if (e === undefined) return { frontier: f, createdAt: 0, entries: [] }\n if (!Array.isArray(e) || typeof t !== 'number' || !Number.isFinite(t))\n return null\n for (const entry of e) {\n if (\n !Array.isArray(entry) ||\n entry.length !== 4 ||\n !Number.isSafeInteger(entry[0]) ||\n typeof entry[1] !== 'string' ||\n typeof entry[2] !== 'string'\n )\n return null\n }\n return { frontier: f, createdAt: t, entries: e as WakeEntry[] }\n}\n\nexport function createRedisNotifyStore(options: {\n client: () => Promise<RedisConnection>\n callBytes?: RedisCall\n closeClient: () => Promise<void>\n clock: Clock\n ids: IdSource\n keyPrefix: string\n onReconnect?: (listener: (channel: string) => void) => void\n polling?: () => boolean\n readStream?: RedisStreamRead\n}): A2Store & { close(): Promise<void> } {\n const { client, clock, ids, keyPrefix: prefix } = options\n const notifyChannel = (sessionId: string): string =>\n `${prefix}:${sessionId}:notify`\n const presenceChannel = (ns: string): string => `${prefix}:${ns}:presence`\n\n const instanceId = crypto.randomUUID()\n const core = createRedisStoreCore({\n call: async (command, ...args) => {\n const c = await client()\n return c.call(command, ...args)\n },\n clock,\n ids,\n keyPrefix: prefix,\n presenceSource: instanceId,\n ...(options.callBytes ? { callBytes: options.callBytes } : {}),\n ...(options.readStream ? { readStream: options.readStream } : {}),\n })\n\n let severed = false\n let subscriber: Promise<RedisConnection> | null = null\n /** channel → live event feeds, registered before they subscribe. */\n const feeds = new Map<string, Set<Feed>>()\n /** channel → refcounted subscription held for each live iterator. */\n const subscriptions = new Map<\n string,\n { refs: number; ready: Promise<unknown> }\n >()\n const presenceSubscriptions = new Map<string, PresenceSubscription>()\n const pollingStreams = new Set<AsyncIterator<Event>>()\n\n options.onReconnect?.((channel) => {\n for (const feed of feeds.get(channel) ?? []) {\n feed.dirty = true\n feed.wake?.()\n }\n void presenceSubscriptions\n .get(channel)\n ?.recovery?.reconnect()\n .catch(() => {})\n })\n\n const deliverRemotePresence = ({\n subscription,\n message,\n }: {\n subscription: PresenceSubscription\n message: string\n }): void => {\n let parsed: PresenceWireMessage\n try {\n parsed = JSON.parse(message) as PresenceWireMessage\n } catch {\n return\n }\n if (parsed.src === instanceId) return\n const patch: PresencePatch = {\n participant: parsed.participant,\n values: parsed.values,\n seen: parsed.seen,\n at: new Date(parsed.at),\n }\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n\n const getSubscriber = (): Promise<RedisConnection> => {\n subscriber ??= (async () => {\n const conn = (await client()).duplicate()\n conn.on('message', (channel, message) => {\n for (const feed of feeds.get(channel) ?? []) {\n if (feed.bytes + message.length <= INBOX_BYTES) {\n feed.inbox.push(message)\n feed.bytes += message.length\n } else {\n feed.inbox.length = 0\n feed.bytes = 0\n feed.dirty = true\n }\n feed.wake?.()\n }\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) deliverRemotePresence({ subscription, message })\n })\n return conn\n })()\n return subscriber\n }\n\n /**\n * The subscriber, gated on connection readiness — for channel\n * commands only (`close` uses the ungated promise, so it can never\n * hang on an unreachable server). SUBSCRIBE carries redis's\n * ok-loading flag, so ioredis writes it mid-handshake, ahead of the\n * ready check's INFO — which then finds the connection already in\n * subscriber mode and fails the check (a spurious error event plus\n * a reconnect). ECHO has no such flag: it resolves only once the\n * connection is ready, so every channel command chained behind it\n * lands after the check.\n */\n let subscriberGate: Promise<RedisConnection> | null = null\n const gatedSubscriber = (): Promise<RedisConnection> => {\n subscriberGate ??= getSubscriber().then(async (conn) => {\n await conn.call('echo', 'ready')\n return conn\n })\n return subscriberGate\n }\n\n const acquireChannel = async (channel: string): Promise<void> => {\n const existing = subscriptions.get(channel)\n if (existing) {\n existing.refs += 1\n try {\n await existing.ready\n } catch (err) {\n existing.refs -= 1\n throw err\n }\n return\n }\n const lease: { refs: number; ready: Promise<void> } = {\n refs: 1,\n // Lowercase on purpose: ioredis keys its subscriber-mode\n // bookkeeping (and reconnect resubscription) on the exact\n // command name.\n ready: gatedSubscriber().then(async (conn) => {\n await conn.call('subscribe', channel)\n if (subscriptions.get(channel) !== lease || severed) return\n const presence = presenceSubscriptions.get(channel)\n if (options.polling?.() && presence) {\n presence.recovery = undefined\n const ns = channel.slice(prefix.length + 1, -':presence'.length)\n presence.poll = pollPresence({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of presence.listeners) listener(patch)\n },\n })\n await presence.poll.ready\n }\n return undefined\n }),\n }\n subscriptions.set(channel, lease)\n try {\n await lease.ready\n } catch (err) {\n if (subscriptions.get(channel) === lease) {\n subscriptions.delete(channel)\n const presence = presenceSubscriptions.get(channel)\n presence?.poll?.stop()\n if (presence) presence.poll = undefined\n }\n throw err\n }\n }\n\n const releaseChannel = (channel: string): void => {\n const lease = subscriptions.get(channel)\n if (!lease) return\n lease.refs -= 1\n if (lease.refs > 0) return\n subscriptions.delete(channel)\n void gatedSubscriber()\n .then((conn) => conn.call('unsubscribe', channel))\n .catch(() => {})\n }\n\n const store: A2Store & { close(): Promise<void> } = {\n append: core.append,\n read: core.read,\n readHistory: core.readHistory!,\n claimAvailable: core.claimAvailable,\n renewClaims: core.renewClaims,\n completeAttempt: core.completeAttempt,\n failAttempt: core.failAttempt,\n readState: core.readState,\n putSnapshots: core.putSnapshots,\n clear: core.clear!,\n reclaim: core.reclaim!,\n\n presence: {\n async set(ns, participant, values, meta) {\n const patch = await core.presence.set(ns, participant, values, meta)\n if (!patch) return\n const channel = presenceChannel(ns)\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) {\n subscription.poll?.update(patch)\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n },\n\n async read(ns) {\n const channel = presenceChannel(ns)\n await subscriptions.get(channel)?.ready\n return (\n presenceSubscriptions.get(channel)?.recovery?.read() ??\n core.presence.read(ns)\n )\n },\n\n subscribe(ns, onPatch) {\n const channel = presenceChannel(ns)\n let subscription = presenceSubscriptions.get(channel)\n if (!subscription) {\n subscription = { listeners: new Set() }\n presenceSubscriptions.set(channel, subscription)\n if (options.onReconnect && !options.polling?.()) {\n const current = subscription\n subscription.recovery = presenceRecovery({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of current.listeners) listener(patch)\n },\n isActive: () =>\n !severed && presenceSubscriptions.get(channel) === current,\n })\n }\n }\n subscription.listeners.add(onPatch)\n // Fire-and-forget on the shared refcounted subscription: it\n // only carries remote patches, so settling late (or failing)\n // degrades cross-instance latency, never same-process\n // delivery.\n void acquireChannel(channel).catch(() => {})\n let stopped = false\n return () => {\n if (stopped) return\n stopped = true\n subscription.listeners.delete(onPatch)\n if (subscription.listeners.size === 0) {\n subscription.poll?.stop()\n presenceSubscriptions.delete(channel)\n }\n releaseChannel(channel)\n }\n },\n },\n\n stream(sessionId, opts) {\n const startAfter = opts?.startAfter ?? 0\n const channel = notifyChannel(sessionId)\n\n return {\n [Symbol.asyncIterator](): AsyncIterator<Event> {\n let last = startAfter\n let buffer: HeldEvent[] = []\n let closed = false\n let subscribed = false\n let pending: Sleeper | null = null\n let poller: AsyncIterator<Event> | undefined\n let stopActivity: (() => void) | undefined\n let catchup:\n | {\n controller: AbortController\n iterator: AsyncGenerator<SerializedEvent[], void>\n delivered: boolean\n }\n | undefined\n let stoppingRead: Promise<void> | undefined\n const stopRead = (): Promise<void> => {\n const reading = catchup\n catchup = undefined\n if (reading) {\n reading.controller.abort()\n stoppingRead = reading.iterator\n .return(undefined)\n .then(() => undefined)\n .catch(() => {})\n }\n return stoppingRead ?? Promise.resolve()\n }\n const nextRead = async (\n after: number,\n ): Promise<IteratorResult<SerializedEvent[], void>> => {\n if (!catchup) {\n const controller = new AbortController()\n catchup = {\n controller,\n delivered: false,\n iterator: core.streamEvents!({\n sessionId,\n afterIndex: after,\n signal: controller.signal,\n })[Symbol.asyncIterator](),\n }\n }\n const reading = catchup\n const result = await reading.iterator.next()\n if (result.done && catchup === reading) {\n catchup = undefined\n if (reading.delivered) feed.dirty = true\n } else if (!result.done) reading.delivered = true\n return result\n }\n const feed: Feed = {\n inbox: [],\n bytes: 0,\n dirty: true,\n wake: null,\n stopRead,\n }\n const activity = createStreamActivity(() => {\n if (poller) streamActivities.get(poller)?.activate()\n })\n\n const release = (): void => {\n void stopRead()\n stopActivity?.()\n if (poller) {\n void poller.return?.()\n pollingStreams.delete(poller)\n }\n if (!subscribed) return\n subscribed = false\n const set = feeds.get(channel)\n set?.delete(feed)\n if (set?.size === 0) feeds.delete(channel)\n releaseChannel(channel)\n }\n\n /**\n * Moves carried events that continue this feed's sequence into\n * the buffer. A gap, a message without events, or an unreadable\n * message marks the feed dirty so it catches up with XRANGE.\n */\n const applyInbox = (): void => {\n feed.bytes = 0\n for (const message of feed.inbox.splice(0)) {\n const wake = parseWake(message)\n if (!wake) {\n feed.dirty = true\n continue\n }\n for (const [index, id, type, payload] of wake.entries) {\n const expected = (buffer.at(-1)?.index ?? last) + 1\n if (index < expected) continue\n if (index > expected) {\n feed.dirty = true\n break\n }\n buffer.push({\n id,\n type,\n payload,\n index,\n sessionId,\n createdAt: new Date(wake.createdAt),\n })\n }\n if (wake.frontier > (buffer.at(-1)?.index ?? last))\n feed.dirty = true\n }\n }\n\n const iterator: AsyncIterator<Event> = {\n async next(): Promise<IteratorResult<Event>> {\n for (;;) {\n if (closed) {\n release()\n return { value: undefined, done: true }\n }\n const row = buffer.shift()\n if (row) {\n let event: Event\n try {\n event = isSerializedEvent(row)\n ? parseSerializedEvent(row)\n : row\n } catch (cause) {\n // A retry re-reads from the last delivered event.\n buffer = []\n feed.dirty = true\n release()\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n last = row.index\n return { value: event, done: false }\n }\n if (severed) {\n release()\n throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')\n }\n try {\n if (!subscribed) {\n // Register and subscribe before the first read:\n // anything appended after it arrives in the inbox,\n // so nothing slips between catch-up and park.\n subscribed = true\n let set = feeds.get(channel)\n if (!set) {\n set = new Set()\n feeds.set(channel, set)\n }\n set.add(feed)\n // oxlint-disable-next-line no-await-in-loop -- one-time setup\n await acquireChannel(channel)\n if (closed || severed) continue\n }\n if (options.polling?.()) {\n if (!poller) {\n poller = pollingStream(\n (after) => core.readEvents(sessionId, after),\n { startAfter: last },\n )[Symbol.asyncIterator]()\n pollingStreams.add(poller)\n stopActivity = streamActivities\n .get(poller)\n ?.subscribe(activity.setIdle)\n }\n // oxlint-disable-next-line no-await-in-loop -- delegate the fallback to the shared poller\n const result = await poller.next()\n if (!result.done)\n presenceSubscriptions\n .get(presenceChannel(sessionId))\n ?.poll?.wake()\n if (severed)\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'the store was closed',\n )\n return result\n }\n if (catchup) {\n // oxlint-disable-next-line no-await-in-loop -- decode the next catch-up batch on demand\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n applyInbox()\n if (buffer.length > 0) continue\n if (feed.dirty) {\n feed.dirty = false\n if (core.streamEvents) {\n // oxlint-disable-next-line no-await-in-loop -- begin an incremental catch-up\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n buffer = await core.readFeedPage(sessionId, last)\n if (buffer.length >= FEED_PAGE_SIZE) feed.dirty = true\n continue\n }\n // Caught up: park until a message, a reconnect, or the\n // safety read that heals a lost message.\n const woken = new Promise<'wake'>((resolve) => {\n feed.wake = () => resolve('wake')\n })\n pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs)\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n const outcome = await Promise.race([\n woken,\n pending.promise.then(() => 'timer' as const),\n ])\n pending.cancel()\n pending = null\n if (outcome === 'timer') feed.dirty = true\n } catch (err) {\n release()\n if (closed) return { value: undefined, done: true }\n if (err instanceof A2Error) throw err\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis store operation failed',\n { cause: err },\n )\n } finally {\n feed.wake = null\n }\n }\n },\n async return(): Promise<IteratorResult<Event>> {\n closed = true\n pending?.cancel()\n feed.wake?.() // wakes an in-flight next() immediately\n release()\n await stopRead()\n return { value: undefined, done: true }\n },\n }\n streamActivities.set(iterator, activity)\n bufferedEvents.set(iterator, () => {\n if (closed) return undefined\n const row = buffer.shift()\n if (row) last = row.index\n return row\n })\n return iterator\n },\n }\n },\n\n async close() {\n severed = true\n for (const subscription of presenceSubscriptions.values())\n subscription.poll?.stop()\n presenceSubscriptions.clear()\n for (const poller of pollingStreams) void poller.return?.()\n pollingStreams.clear()\n const reads: Promise<void>[] = []\n for (const set of feeds.values()) {\n for (const feed of set) {\n feed.wake?.()\n reads.push(feed.stopRead())\n }\n }\n await Promise.allSettled(reads)\n if (subscriber) {\n const sub = await subscriber.catch(() => null)\n sub?.disconnect()\n }\n await options.closeClient()\n },\n }\n stateTailReaders.set(store, core.readTail)\n return store\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"telemetry-cZH-cibj.d.ts","names":[],"sources":["../src/telemetry.ts"],"mappings":";;;;;;;;KAQY;;KAGA;KAeA;;EAEV,aAAa,aAAa,OAAO;;;;;;EAMjC,YAAY;;KAGF;;;;;;;EAOV,KAAK,GACH,MAAM,YACN,YAAY,eAAe,mBAC3B,KAAK,MAAM,iBAAiB,QAAQ,KACnC,QAAQ"}
@@ -1,237 +0,0 @@
1
- ---
2
- title: Contracts and sessions
3
- description: A contract names one kind of lifecycle. A session is one instance, with its own history, state, recovery, and live stream.
4
- ---
5
-
6
- ## Contracts
7
-
8
- A2 has two nouns. A **contract** defines what can happen in one kind of
9
- lifecycle. A **session** is one instance of it.
10
-
11
- ```ts contracts.ts
12
- import { z } from 'zod'
13
- import * as a2 from 'experimental-a2'
14
-
15
- export const orders = a2.contract({
16
- name: 'orders',
17
- events: {
18
- created: z.object({ shopId: z.string(), items: z.array(z.string()) }),
19
- 'shop.notified': z.object({}),
20
- 'shop.started': z.object({}),
21
- 'driver.notified': z.object({ driverId: z.string() }),
22
- expired: z.object({}),
23
- },
24
- })
25
- ```
26
-
27
- The name identifies the contract everywhere: in storage, in queue
28
- messages, in logs. It's what keeps an app with several contracts
29
- unambiguous.
30
-
31
- `events` is the contract's full vocabulary. Each key is an event name;
32
- each value is a schema for its payload. Zod in these docs, but any
33
- [Standard Schema](https://standardschema.dev) library works: Valibot,
34
- ArkType, your own. Every append is validated against it: a bad payload
35
- throws `INVALID_PAYLOAD` before anything is written, an unknown type
36
- throws `UNKNOWN_EVENT_TYPE`. What lands in the log is the validated
37
- output, with defaults applied and transforms run.
38
-
39
- The contract is a plain, importable, **isomorphic** value. It carries the
40
- schemas and their type helpers. The server implements it,
41
- [reducers](/concepts/state) derive from it, and the browser types its pushes
42
- off it. One artifact, shared by every side of the wire.
43
-
44
- The contract names the complete vocabulary. What happens on each event is
45
- defined where the contract is served. [Reducers](/concepts/state) define what
46
- things look like right now.
47
-
48
- A contract can also declare `presence`, a second vocabulary for ephemeral
49
- participant state (cursors, typing, live progress) that is replicated but
50
- never stored in the log. Events are facts; presence is the moment. See
51
- [Presence](/guides/presence).
52
-
53
- :::tip
54
- Events are facts, so name them in past tense: `created`, `expired`,
55
- `shop.notified`. If a name reads like a command (`notifyShop`), it's
56
- describing what a handler should do, not what happened. Dots are a naming
57
- convention, not a namespace.
58
- :::
59
-
60
- ## Serving a contract
61
-
62
- Storage and optional handlers bind to the contract with `createServer` from
63
- `experimental-a2/server`, the entry point that is allowed to touch a store backend
64
- (and the one that fails loudly if a browser bundle ever imports it):
65
-
66
- ```ts server/orders.ts
67
- import { createServer } from 'experimental-a2/server'
68
- import { postgres } from 'experimental-a2/store-postgres'
69
- import { orders } from '@/contracts'
70
-
71
- export const ordersServer = createServer({
72
- contract: orders,
73
- store: postgres({ connectionString: process.env.DATABASE_URL }),
74
- handlers: {
75
- created: async ({ event }) => {
76
- // your side effect, then:
77
- return { type: 'shop.notified', payload: {} }
78
- },
79
- },
80
- })
81
- ```
82
-
83
- Handlers are one table of reactions, declared at construction, so a
84
- handler can never be silently missing because the module that would
85
- have registered it wasn't imported. Compose across files by spreading
86
- objects into `handlers`.
87
-
88
- | Entry point | Backend | Use it for |
89
- | ----------------- | --------- | ----------------------------------------- |
90
- | `experimental-a2/store-postgres` | Postgres | Production |
91
- | `experimental-a2/store-sqlite` | SQLite | Development: the default, at `.a2/dev.db` |
92
- | `experimental-a2/store-memory` | In-memory | Tests: the default when `NODE_ENV=test` |
93
-
94
- When you omit `store`, development gets SQLite (state survives `next dev`
95
- restarts) and tests get memory. Production gets an error: the server
96
- throws `STORE_NOT_CONFIGURED` at startup rather than guess where your
97
- events should live. A failed boot beats events written to a filesystem
98
- that evaporates.
99
-
100
- ## Sessions
101
-
102
- ```ts
103
- // anywhere on the server: a route, a handler, a script
104
- const order = ordersServer.session('order-42')
105
- ```
106
-
107
- A session is one instance of the contract: one order, one signup, one
108
- chat, one approval, one import. It has its own ordered log, state, recovery,
109
- and live stream. The id is yours: use whatever identifies the subject in your
110
- system.
111
-
112
- Creating a session does no I/O. It's a handle; nothing loads until you
113
- append, read, or stream.
114
-
115
- Handlers are concurrent by default, including within one session. Add a
116
- [`lane`](/concepts/handlers#lanes) when events share a resource and must run
117
- one at a time.
118
-
119
- ## Pick the boundary
120
-
121
- The session is the boundary A2 keeps durable and synchronized. Put events in
122
- the same session when they form one history, fold, recovery unit, or live
123
- view. Split things that should be read and recovered independently.
124
-
125
- A contract is not a database table, and a session is not a table or a general
126
- query boundary. One contract serves every instance of that lifecycle. An
127
- `orders` contract may serve millions of order sessions through the same server
128
- and handler table.
129
-
130
- This boundary works well when someone opens a thing by id and watches it
131
- progress. Collections, joins, search, and constraints across many records
132
- usually belong in an application database. See
133
- [A2 and your database](/guides/application-data).
134
-
135
- ## Clearing a session
136
-
137
- `session.clear()` ends a session's current lifetime. Its history is gone,
138
- every reducer is back at its initial state, and the session id keeps working.
139
-
140
- ```ts server/clear-order.ts
141
- import { ordersServer } from './orders'
142
-
143
- export async function clearOrder({ orderId, lastIndex }: {
144
- orderId: string
145
- lastIndex: number
146
- }) {
147
- // here's where you'd do auth, or any other checks
148
- return ordersServer.session(orderId).clear({ ifLastIndex: lastIndex })
149
- }
150
- ```
151
-
152
- A clear appends one reserved marker, so indexes never restart. Clearing a
153
- session whose last event is `42` returns `{ cleared: true, index: 43 }`, and
154
- the next event is `44`.
155
-
156
- Pass `ifLastIndex` when newer events must survive. The store checks it in the
157
- same atomic step that appends the marker. If another event landed first, the
158
- clear skips and returns `{ cleared: false, index }` with the current head. You
159
- need no state read when you already know an event's index. A conditional clear
160
- is safe to retry.
161
-
162
- Work from the old lifetime stops counting. Its pending events are never
163
- dispatched, and a handler still running for one of them can't append, return
164
- events, or read the new lifetime's state. Clearing can't stop code that is
165
- already running or undo its side effects.
166
-
167
- Pushes and appends that land after the marker start the new lifetime, like
168
- any other event. Connected clients reset when the marker reaches them. Once a
169
- session has been cleared, `session.state()` also returns `clearIndex`. Pass the
170
- whole result as a client's `initial` snapshot so a cached client drops its old
171
- history too; the automatic snapshot on subscribe carries it already.
172
-
173
- After the marker, `clear()` deletes the old events and cached state in small
174
- batches and resolves when they're gone. Appends and reads keep running between
175
- batches, and the cost grows with the size of the old history. If it fails
176
- partway, the session is still cleared and the leftovers are invisible: retry
177
- the call, or clear again later, to finish deleting. For a very large session,
178
- a [scheduled clear](#scheduling-a-clear) is sturdier, because the provider
179
- retries it and each retry resumes the deletion. Presence is separate and keeps its own expiry.
180
-
181
- Clearing is a server operation. `server.fetch` has no clear endpoint, so
182
- authorize your own route before calling it.
183
-
184
- ### Scheduling a clear
185
-
186
- With a [scheduler](/guides/production) configured, a handler can schedule a
187
- clear for later. This one clears an idle cart a day after its last activity:
188
-
189
- ```ts server/carts.ts
190
- import * as a2 from 'experimental-a2'
191
- import { createServer } from 'experimental-a2/server'
192
- import { vercelQueues } from 'experimental-a2/scheduler-vercel'
193
- import { z } from 'zod'
194
-
195
- export const carts = a2.contract({
196
- name: 'carts',
197
- events: {
198
- 'item.added': z.object({ sku: z.string() }),
199
- },
200
- })
201
-
202
- export const cartsServer = createServer({
203
- contract: carts,
204
- scheduler: vercelQueues(),
205
- handlers: {
206
- 'item.added': async ({ event, session }) => {
207
- await session.schedule.clear('idle', {
208
- delay: '1d',
209
- ifLastIndex: event.index,
210
- })
211
- },
212
- },
213
- })
214
- ```
215
-
216
- The delay counts from the event's stored time. Any later event moves the head
217
- past `event.index`, so this delivery skips; that event's own handler schedules
218
- the next check. Only events with a handler can extend the policy this way.
219
-
220
- The handler's own writes move the head too. This handler appends and returns
221
- nothing, so `event.index` stays the head. A handler that appends should pass
222
- the index its last append returned.
223
-
224
- Delivery runs the same conditional clear and finishes the deletion before it
225
- acknowledges, so provider retries complete an interrupted delete. No cleanup
226
- event is appended. The clear happens when the task is delivered, not at an
227
- exact deadline, and provider limits apply: Vercel Queues accepts delays up to
228
- six days.
229
-
230
- ## One more option: a scheduler
231
-
232
- Servers accept a `scheduler` option. When a top-level append leaves or finds
233
- pending handler work, the scheduler arms a durable, delayed drain. The handler
234
- starts without waiting for the scheduler, and a successful initial arm is durable
235
- before `append` returns. Event types without handlers settle during append and
236
- create no scheduler work when the session has no older backlog. Covered in
237
- [Going to production](/guides/production).