experimental-a2 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (406) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
  4. package/dist/actor-Bjce9_5U.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +2 -5
  6. package/dist/actor-client.d.ts.map +1 -1
  7. package/dist/actor-client.js +13 -4
  8. package/dist/actor-client.js.map +1 -1
  9. package/dist/actor-react.d.ts +5 -3
  10. package/dist/actor-react.d.ts.map +1 -1
  11. package/dist/actor-react.js +20 -10
  12. package/dist/actor-react.js.map +1 -1
  13. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  14. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  15. package/dist/actor.d.ts +1 -1
  16. package/dist/actor.js +42 -23
  17. package/dist/actor.js.map +1 -1
  18. package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
  19. package/dist/ai-BpOGm-zB.js.map +1 -0
  20. package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
  21. package/dist/ai-DrUaMyRt.d.ts.map +1 -0
  22. package/dist/ai-client.d.ts +27 -0
  23. package/dist/ai-client.d.ts.map +1 -0
  24. package/dist/ai-client.js +29 -0
  25. package/dist/ai-client.js.map +1 -0
  26. package/dist/ai-fx.browser.d.ts +1 -0
  27. package/dist/ai-fx.browser.js +6 -0
  28. package/dist/ai-fx.browser.js.map +1 -0
  29. package/dist/ai-fx.d.ts +10 -0
  30. package/dist/ai-fx.d.ts.map +1 -0
  31. package/dist/ai-fx.js +634 -0
  32. package/dist/ai-fx.js.map +1 -0
  33. package/dist/ai-harness-BuLh_EYi.js +273 -0
  34. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  35. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  36. package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
  37. package/dist/ai-pi.browser.d.ts +1 -0
  38. package/dist/ai-pi.browser.js +6 -0
  39. package/dist/ai-pi.browser.js.map +1 -0
  40. package/dist/ai-pi.d.ts +17 -0
  41. package/dist/ai-pi.d.ts.map +1 -0
  42. package/dist/ai-pi.js +904 -0
  43. package/dist/ai-pi.js.map +1 -0
  44. package/dist/ai-react.d.ts +96 -0
  45. package/dist/ai-react.d.ts.map +1 -0
  46. package/dist/ai-react.js +142 -0
  47. package/dist/ai-react.js.map +1 -0
  48. package/dist/ai-server.browser.js +2 -2
  49. package/dist/ai-server.browser.js.map +1 -1
  50. package/dist/ai-server.d.ts +109 -43
  51. package/dist/ai-server.d.ts.map +1 -1
  52. package/dist/ai-server.js +526 -283
  53. package/dist/ai-server.js.map +1 -1
  54. package/dist/ai.d.ts +2 -2
  55. package/dist/ai.js +2 -2
  56. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  57. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  58. package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
  59. package/dist/client-BReKqwZC.js.map +1 -0
  60. package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
  61. package/dist/client-h_DKFAjj.d.ts.map +1 -0
  62. package/dist/client.d.ts +4 -2
  63. package/dist/client.js +1 -1
  64. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  65. package/dist/contract-BYKASlCV.js.map +1 -0
  66. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  67. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  68. package/dist/index.d.ts +6 -5
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
  72. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
  73. package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
  74. package/dist/internal-B2T3U4Rq.js.map +1 -0
  75. package/dist/otel.d.ts +1 -1
  76. package/dist/react-BBXgHWFc.d.ts +86 -0
  77. package/dist/react-BBXgHWFc.d.ts.map +1 -0
  78. package/dist/react.d.ts +2 -122
  79. package/dist/react.js +107 -100
  80. package/dist/react.js.map +1 -1
  81. package/dist/reducer-BAXc8JyF.js +87 -0
  82. package/dist/reducer-BAXc8JyF.js.map +1 -0
  83. package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
  84. package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
  85. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  86. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  87. package/dist/scheduler-qstash.d.ts +3 -3
  88. package/dist/scheduler-qstash.d.ts.map +1 -1
  89. package/dist/scheduler-qstash.js +33 -10
  90. package/dist/scheduler-qstash.js.map +1 -1
  91. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  92. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  93. package/dist/scheduler-vercel.d.ts +4 -9
  94. package/dist/scheduler-vercel.d.ts.map +1 -1
  95. package/dist/scheduler-vercel.js +25 -44
  96. package/dist/scheduler-vercel.js.map +1 -1
  97. package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
  98. package/dist/server-Dw_k_7kO.js.map +1 -0
  99. package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
  100. package/dist/server-En7cbi3v.d.ts.map +1 -0
  101. package/dist/server.d.ts +4 -4
  102. package/dist/server.js +2 -2
  103. package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
  104. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  105. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  106. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  107. package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
  108. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  109. package/dist/store-N8PXxDAS.js.map +1 -1
  110. package/dist/store-memory.d.ts +1 -1
  111. package/dist/store-memory.d.ts.map +1 -1
  112. package/dist/store-memory.js +205 -66
  113. package/dist/store-memory.js.map +1 -1
  114. package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
  115. package/dist/store-polling-B5FcwJfo.js.map +1 -0
  116. package/dist/store-postgres.d.ts +1 -1
  117. package/dist/store-postgres.d.ts.map +1 -1
  118. package/dist/store-postgres.js +450 -108
  119. package/dist/store-postgres.js.map +1 -1
  120. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
  121. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  122. package/dist/store-redis-http.d.ts +1 -1
  123. package/dist/store-redis-http.d.ts.map +1 -1
  124. package/dist/store-redis-http.js +293 -11
  125. package/dist/store-redis-http.js.map +1 -1
  126. package/dist/store-redis-notify-BRITti_o.js +2493 -0
  127. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  128. package/dist/store-redis.d.ts +1 -1
  129. package/dist/store-redis.d.ts.map +1 -1
  130. package/dist/store-redis.js +11 -11
  131. package/dist/store-redis.js.map +1 -1
  132. package/dist/store-sqlite.d.ts +1 -1
  133. package/dist/store-sqlite.d.ts.map +1 -1
  134. package/dist/store-sqlite.js +197 -33
  135. package/dist/store-sqlite.js.map +1 -1
  136. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  137. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  138. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  139. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  140. package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
  141. package/dist/wire-rSUglYo8.js.map +1 -0
  142. package/docs/01-quickstart.mdx +50 -84
  143. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  144. package/docs/actors/01-introduction.mdx +48 -82
  145. package/docs/actors/02-concurrency.mdx +43 -70
  146. package/docs/actors/03-timers.mdx +28 -42
  147. package/docs/actors/04-routes.mdx +110 -137
  148. package/docs/actors/meta.ts +1 -1
  149. package/docs/agents/01-introduction.mdx +257 -0
  150. package/docs/agents/02-tools.mdx +247 -0
  151. package/docs/agents/03-conversations.mdx +538 -0
  152. package/docs/agents/04-harnesses.mdx +136 -0
  153. package/docs/agents/meta.ts +1 -0
  154. package/docs/client/01-react.mdx +294 -0
  155. package/docs/client/02-transports.mdx +86 -0
  156. package/docs/client/03-presence.mdx +224 -0
  157. package/docs/client/meta.ts +1 -0
  158. package/docs/contract/01-events.mdx +57 -0
  159. package/docs/contract/02-state.mdx +142 -0
  160. package/docs/contract/03-sessions.mdx +175 -0
  161. package/docs/index.mdx +112 -156
  162. package/docs/reference/01-core.mdx +139 -0
  163. package/docs/reference/02-server.mdx +855 -0
  164. package/docs/reference/03-client.mdx +301 -0
  165. package/docs/reference/04-ai.mdx +722 -0
  166. package/docs/reference/05-adapters.mdx +351 -0
  167. package/docs/reference/06-errors.mdx +138 -0
  168. package/docs/reference/meta.ts +1 -1
  169. package/docs/server/01-handlers.mdx +200 -0
  170. package/docs/server/02-timers.mdx +130 -0
  171. package/docs/server/03-cancellation.mdx +141 -0
  172. package/docs/server/04-routes.mdx +243 -0
  173. package/docs/server/05-production.mdx +322 -0
  174. package/docs/server/06-guarantees.mdx +123 -0
  175. package/examples/playground/DEPLOY.md +53 -7
  176. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  177. package/examples/playground/app/a0/[appId]/page.tsx +53 -0
  178. package/examples/playground/app/a0/a0.css +818 -0
  179. package/examples/playground/app/a0/app-list.tsx +218 -0
  180. package/examples/playground/app/a0/app-view.tsx +63 -0
  181. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  182. package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
  183. package/examples/playground/app/a0/apps.test.ts +128 -0
  184. package/examples/playground/app/a0/apps.ts +102 -0
  185. package/examples/playground/app/a0/chat.tsx +410 -0
  186. package/examples/playground/app/a0/columns.tsx +109 -0
  187. package/examples/playground/app/a0/context-menu.tsx +116 -0
  188. package/examples/playground/app/a0/cookies.ts +46 -0
  189. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  190. package/examples/playground/app/a0/crashes/route.ts +3 -0
  191. package/examples/playground/app/a0/crashes.test.ts +33 -0
  192. package/examples/playground/app/a0/crashes.ts +28 -0
  193. package/examples/playground/app/a0/devtools.tsx +164 -0
  194. package/examples/playground/app/a0/events/route.ts +6 -0
  195. package/examples/playground/app/a0/flags.ts +30 -0
  196. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  197. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  198. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  199. package/examples/playground/app/a0/layout.tsx +38 -0
  200. package/examples/playground/app/a0/list/route.ts +10 -0
  201. package/examples/playground/app/a0/model.test.ts +73 -0
  202. package/examples/playground/app/a0/model.ts +95 -0
  203. package/examples/playground/app/a0/no-photon.cjs +4 -0
  204. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  205. package/examples/playground/app/a0/page.tsx +14 -0
  206. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  207. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  208. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  209. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  210. package/examples/playground/app/a0/preview.test.ts +53 -0
  211. package/examples/playground/app/a0/preview.ts +26 -0
  212. package/examples/playground/app/a0/sandbox-log.ts +75 -0
  213. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  214. package/examples/playground/app/a0/sandbox.ts +191 -0
  215. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  216. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  217. package/examples/playground/app/a0/server.ts +213 -0
  218. package/examples/playground/app/a0/session.ts +59 -0
  219. package/examples/playground/app/a0/shell.tsx +158 -0
  220. package/examples/playground/app/a0/short/events/route.ts +9 -0
  221. package/examples/playground/app/a0/splitter.tsx +120 -0
  222. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  223. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  224. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  225. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  226. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  227. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  228. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  229. package/examples/playground/app/agent/events/route.ts +5 -2
  230. package/examples/playground/app/agent/ingress.test.ts +101 -0
  231. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  232. package/examples/playground/app/agent/model-input.tsx +115 -0
  233. package/examples/playground/app/agent/model.test.ts +73 -0
  234. package/examples/playground/app/agent/model.ts +58 -26
  235. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  236. package/examples/playground/app/agent/page.tsx +4 -4
  237. package/examples/playground/app/agent/server.ts +19 -15
  238. package/examples/playground/app/agent/session.ts +5 -6
  239. package/examples/playground/app/agent/telemetry.test.ts +189 -0
  240. package/examples/playground/app/agent/telemetry.ts +30 -0
  241. package/examples/playground/app/agent/trace.test.ts +23 -0
  242. package/examples/playground/app/agent/trace.ts +18 -0
  243. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
  244. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  245. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  246. package/examples/playground/app/canvas/model.ts +53 -53
  247. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  248. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  249. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  250. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  251. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  252. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  253. package/examples/playground/app/chat/agent/route.ts +9 -0
  254. package/examples/playground/app/chat/model.test.ts +46 -119
  255. package/examples/playground/app/chat/model.ts +82 -128
  256. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  257. package/examples/playground/app/chat/server.test.ts +112 -0
  258. package/examples/playground/app/chat/server.ts +113 -117
  259. package/examples/playground/app/components/activity-feed.tsx +30 -1
  260. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  261. package/examples/playground/app/components/demo-icons.tsx +89 -0
  262. package/examples/playground/app/components/demos.tsx +33 -0
  263. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  264. package/examples/playground/app/components/fast-link.tsx +55 -0
  265. package/examples/playground/app/components/site-header.tsx +96 -0
  266. package/examples/playground/app/counter/counter-client.tsx +3 -3
  267. package/examples/playground/app/counter/model.test.ts +3 -3
  268. package/examples/playground/app/counter/model.ts +11 -21
  269. package/examples/playground/app/counter/page.tsx +2 -15
  270. package/examples/playground/app/counter/session.ts +2 -2
  271. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  272. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  273. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  274. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  275. package/examples/playground/app/documents/model.ts +27 -27
  276. package/examples/playground/app/globals.css +420 -207
  277. package/examples/playground/app/initial-state.test.tsx +54 -0
  278. package/examples/playground/app/layout.tsx +7 -23
  279. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  280. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  281. package/examples/playground/app/orders/model.ts +49 -49
  282. package/examples/playground/app/page.tsx +13 -102
  283. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  284. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  285. package/examples/playground/app/recovery/model.ts +30 -30
  286. package/examples/playground/instrumentation.ts +5 -0
  287. package/examples/playground/next.config.ts +17 -5
  288. package/examples/playground/package.json +13 -2
  289. package/examples/playground/scripts/a0-template.ts +77 -0
  290. package/examples/playground/vercel.json +36 -0
  291. package/package.json +29 -3
  292. package/src/actor-client.ts +16 -4
  293. package/src/actor-react.ts +25 -11
  294. package/src/actor-shared.ts +21 -6
  295. package/src/actor.ts +76 -31
  296. package/src/ai-agent-actions.ts +219 -0
  297. package/src/ai-agent.ts +2517 -0
  298. package/src/ai-app-state.ts +91 -0
  299. package/src/ai-client-state.ts +6 -3
  300. package/src/ai-client.ts +62 -0
  301. package/src/ai-context-schema.ts +25 -5
  302. package/src/ai-context.ts +90 -40
  303. package/src/ai-control-server.ts +92 -72
  304. package/src/ai-coordinator.ts +23 -22
  305. package/src/ai-fx.browser.ts +5 -0
  306. package/src/ai-fx.ts +833 -0
  307. package/src/ai-harness.ts +518 -0
  308. package/src/ai-message-base.ts +209 -0
  309. package/src/ai-message-projection.ts +262 -64
  310. package/src/ai-model-metadata.ts +11 -6
  311. package/src/ai-pi.browser.ts +5 -0
  312. package/src/ai-pi.ts +1263 -0
  313. package/src/ai-progress-batches.ts +26 -0
  314. package/src/ai-projector.ts +95 -14
  315. package/src/ai-react.ts +146 -0
  316. package/src/ai-sdk-step.ts +2 -2
  317. package/src/ai-server.browser.ts +2 -2
  318. package/src/ai-server.ts +23 -1965
  319. package/src/ai-stored-state.ts +48 -163
  320. package/src/ai.ts +339 -155
  321. package/src/bootstrap-stream.ts +72 -0
  322. package/src/bounded-cache.ts +55 -0
  323. package/src/client-render-session.ts +49 -0
  324. package/src/client.ts +688 -151
  325. package/src/contract.ts +92 -42
  326. package/src/deterministic-id.ts +37 -0
  327. package/src/equal-json.ts +32 -0
  328. package/src/errors.ts +13 -1
  329. package/src/index.ts +5 -1
  330. package/src/initial-snapshot.ts +39 -0
  331. package/src/internal.ts +52 -0
  332. package/src/libfx.d.ts +34 -0
  333. package/src/postgres-notification-scope.ts +14 -3
  334. package/src/react.ts +171 -206
  335. package/src/redis-http-reply.ts +256 -0
  336. package/src/redis-http-stream.ts +73 -0
  337. package/src/redis-http-subscriptions.ts +13 -5
  338. package/src/reducer-runtime.ts +52 -0
  339. package/src/reducer.ts +93 -11
  340. package/src/scheduler-qstash.ts +49 -20
  341. package/src/scheduler-task.ts +34 -2
  342. package/src/scheduler-vercel.ts +32 -84
  343. package/src/server-fetch.ts +139 -35
  344. package/src/server.ts +1133 -255
  345. package/src/session-clear.ts +23 -0
  346. package/src/session-socket.ts +154 -49
  347. package/src/sse.ts +71 -4
  348. package/src/store-memory.ts +252 -75
  349. package/src/store-polling.ts +18 -4
  350. package/src/store-postgres.ts +541 -114
  351. package/src/store-redis-core.ts +1450 -493
  352. package/src/store-redis-http.ts +18 -2
  353. package/src/store-redis-notify.ts +262 -85
  354. package/src/store-redis.ts +19 -9
  355. package/src/store-sqlite.ts +319 -45
  356. package/src/store.ts +79 -1
  357. package/src/stream-buffer.ts +43 -0
  358. package/src/telemetry.ts +14 -1
  359. package/src/wire.ts +241 -13
  360. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  361. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  362. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  363. package/dist/ai-DgOBltJ_.js.map +0 -1
  364. package/dist/client-BYzHjkwU.js.map +0 -1
  365. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  366. package/dist/contract-CKRg_E4q.js.map +0 -1
  367. package/dist/errors-DCk6ch5n.js.map +0 -1
  368. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  369. package/dist/internal-Dq2qYxou.js.map +0 -1
  370. package/dist/react.d.ts.map +0 -1
  371. package/dist/reducer-DEMjEY_O.js +0 -29
  372. package/dist/reducer-DEMjEY_O.js.map +0 -1
  373. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  374. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  375. package/dist/server-BD5ckxZb.js.map +0 -1
  376. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  377. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  378. package/dist/store-polling-CmxUbV93.js.map +0 -1
  379. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  380. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  381. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  382. package/dist/validate-XKT4FSNn.js +0 -30
  383. package/dist/validate-XKT4FSNn.js.map +0 -1
  384. package/dist/wire-BO5wWCb1.js.map +0 -1
  385. package/docs/concepts/01-contracts.mdx +0 -142
  386. package/docs/concepts/02-handlers.mdx +0 -284
  387. package/docs/concepts/03-durability.mdx +0 -314
  388. package/docs/concepts/04-state.mdx +0 -209
  389. package/docs/guides/01-timers.mdx +0 -185
  390. package/docs/guides/02-cancellation.mdx +0 -161
  391. package/docs/guides/03-react.mdx +0 -384
  392. package/docs/guides/04-local-first.mdx +0 -88
  393. package/docs/guides/05-production.mdx +0 -521
  394. package/docs/guides/06-ai-agents.mdx +0 -1318
  395. package/docs/guides/08-application-data.mdx +0 -113
  396. package/docs/guides/09-presence.mdx +0 -266
  397. package/docs/guides/10-transports.mdx +0 -221
  398. package/docs/reference/01-api.mdx +0 -1896
  399. package/docs/reference/02-errors.mdx +0 -149
  400. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  401. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  402. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  403. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  404. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
  405. /package/docs/{concepts → contract}/meta.ts +0 -0
  406. /package/docs/{guides → server}/meta.ts +0 -0
@@ -1,6 +1,11 @@
1
1
  import { A2Error } from './errors.ts'
2
2
  import { NOTIFY_TIMINGS } from './internal.ts'
3
3
  import { redisHttpSubscriptions } from './redis-http-subscriptions.ts'
4
+ import {
5
+ readRedisStreamPage,
6
+ REDIS_HTTP_STREAM_READ_COUNT,
7
+ } from './redis-http-stream.ts'
8
+ import type { RedisStreamRead } from './store-redis-core.ts'
4
9
  import {
5
10
  createRedisNotifyStore,
6
11
  type RedisConnection,
@@ -74,6 +79,14 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
74
79
  return body.result
75
80
  }
76
81
 
82
+ const readStream: RedisStreamRead = (read) =>
83
+ readRedisStreamPage({
84
+ ...read,
85
+ url,
86
+ token,
87
+ count: REDIS_HTTP_STREAM_READ_COUNT,
88
+ })
89
+
77
90
  let subscriber: RedisConnection | undefined
78
91
  let onReconnect: ((channel: string) => void) | undefined
79
92
  const client: RedisConnection = {
@@ -85,7 +98,7 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
85
98
  onReconnect: (channel) => onReconnect?.(channel),
86
99
  fallback: {
87
100
  enabled: () => polling,
88
- enable() {
101
+ checkCommands() {
89
102
  fallbackCheck ??= call('PING')
90
103
  .then((result) => {
91
104
  if (result !== 'PONG')
@@ -93,7 +106,6 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
93
106
  'STORE_UNAVAILABLE',
94
107
  'redis rest capability check failed',
95
108
  )
96
- polling = true
97
109
  return undefined
98
110
  })
99
111
  .catch((error: unknown) => {
@@ -102,6 +114,9 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
102
114
  })
103
115
  return fallbackCheck
104
116
  },
117
+ enable() {
118
+ polling = true
119
+ },
105
120
  },
106
121
  })),
107
122
  on() {},
@@ -117,5 +132,6 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
117
132
  clock: options.clock ?? SYSTEM_CLOCK,
118
133
  ids: options.ids ?? RANDOM_IDS,
119
134
  keyPrefix: options.keyPrefix ?? 'a2',
135
+ readStream,
120
136
  })
121
137
  }
@@ -1,11 +1,23 @@
1
1
  import type { PresencePatch } from './contract.ts'
2
2
  import { A2Error } from './errors.ts'
3
- import { NOTIFY_TIMINGS } from './internal.ts'
3
+ import { NOTIFY_TIMINGS, stateTailReaders } from './internal.ts'
4
4
  import { defaultSleep, pollingStream, type Sleeper } from './store-polling.ts'
5
5
  import { pollPresence } from './store-presence-polling.ts'
6
6
  import { presenceRecovery } from './presence-recovery.ts'
7
7
  import { createStreamActivity, streamActivities } from './stream-activity.ts'
8
- import { createRedisStoreCore } from './store-redis-core.ts'
8
+ import {
9
+ bufferedEvents,
10
+ isSerializedEvent,
11
+ parseSerializedEvent,
12
+ type HeldEvent,
13
+ type SerializedEvent,
14
+ } from './stream-buffer.ts'
15
+ import {
16
+ createRedisStoreCore,
17
+ FEED_PAGE_SIZE,
18
+ type RedisCall,
19
+ type RedisStreamRead,
20
+ } from './store-redis-core.ts'
9
21
  import type { A2Store, Clock, Event, IdSource } from './store.ts'
10
22
 
11
23
  export type RedisConnection = {
@@ -32,14 +44,72 @@ type PresenceSubscription = {
32
44
  recovery?: ReturnType<typeof presenceRecovery> | undefined
33
45
  }
34
46
 
47
+ /**
48
+ * One live event feed's view of its channel: wake-up messages received
49
+ * since it subscribed, whether it must catch up with XRANGE before trusting
50
+ * them, and the resolver of its park, if parked.
51
+ */
52
+ type Feed = {
53
+ inbox: string[]
54
+ bytes: number
55
+ dirty: boolean
56
+ wake: (() => void) | null
57
+ stopRead: () => Promise<void>
58
+ }
59
+
60
+ /** A feed that falls this far behind drops its inbox and catches up. */
61
+ const INBOX_BYTES = 512 * 1024
62
+
63
+ type WakeEntry = [index: number, id: string, type: string, payload: unknown]
64
+
65
+ /**
66
+ * Parses a wake-up message: `{"f":frontier}` or a bare frontier number
67
+ * announces events without carrying them; `{"f","t","e"}` carries them.
68
+ * Returns null for anything else, which callers treat as a hint to catch up.
69
+ */
70
+ const parseWake = (
71
+ message: string,
72
+ ): { frontier: number; createdAt: number; entries: WakeEntry[] } | null => {
73
+ let parsed: unknown
74
+ try {
75
+ parsed = JSON.parse(message)
76
+ } catch {
77
+ return null
78
+ }
79
+ if (typeof parsed === 'number') {
80
+ return Number.isSafeInteger(parsed)
81
+ ? { frontier: parsed, createdAt: 0, entries: [] }
82
+ : null
83
+ }
84
+ if (typeof parsed !== 'object' || parsed === null) return null
85
+ const { f, t, e } = parsed as { f?: unknown; t?: unknown; e?: unknown }
86
+ if (typeof f !== 'number' || !Number.isSafeInteger(f)) return null
87
+ if (e === undefined) return { frontier: f, createdAt: 0, entries: [] }
88
+ if (!Array.isArray(e) || typeof t !== 'number' || !Number.isFinite(t))
89
+ return null
90
+ for (const entry of e) {
91
+ if (
92
+ !Array.isArray(entry) ||
93
+ entry.length !== 4 ||
94
+ !Number.isSafeInteger(entry[0]) ||
95
+ typeof entry[1] !== 'string' ||
96
+ typeof entry[2] !== 'string'
97
+ )
98
+ return null
99
+ }
100
+ return { frontier: f, createdAt: t, entries: e as WakeEntry[] }
101
+ }
102
+
35
103
  export function createRedisNotifyStore(options: {
36
104
  client: () => Promise<RedisConnection>
105
+ callBytes?: RedisCall
37
106
  closeClient: () => Promise<void>
38
107
  clock: Clock
39
108
  ids: IdSource
40
109
  keyPrefix: string
41
110
  onReconnect?: (listener: (channel: string) => void) => void
42
111
  polling?: () => boolean
112
+ readStream?: RedisStreamRead
43
113
  }): A2Store & { close(): Promise<void> } {
44
114
  const { client, clock, ids, keyPrefix: prefix } = options
45
115
  const notifyChannel = (sessionId: string): string =>
@@ -56,31 +126,27 @@ export function createRedisNotifyStore(options: {
56
126
  ids,
57
127
  keyPrefix: prefix,
58
128
  presenceSource: instanceId,
129
+ ...(options.callBytes ? { callBytes: options.callBytes } : {}),
130
+ ...(options.readStream ? { readStream: options.readStream } : {}),
59
131
  })
60
132
 
61
133
  let severed = false
62
134
  let subscriber: Promise<RedisConnection> | null = null
63
- /** channel → parked-feed wake-ups, armed before each catch-up read. */
64
- const wakers = new Map<string, Set<() => void>>()
135
+ /** channel → live event feeds, registered before they subscribe. */
136
+ const feeds = new Map<string, Set<Feed>>()
65
137
  /** channel → refcounted subscription held for each live iterator. */
66
138
  const subscriptions = new Map<
67
139
  string,
68
140
  { refs: number; ready: Promise<unknown> }
69
141
  >()
70
- /**
71
- * channel → last presence-marker refresh, throttling the SET.
72
- * Deliberately real time (`Date.now()`), not the injected clock:
73
- * the marker's PX expiry runs on server real time, and the throttle
74
- * must tick with the TTL it refreshes or time-traveling tests would
75
- * desync the two. Process-local and never stored, so the injected
76
- * clock's every-stored-timestamp pledge is untouched.
77
- */
78
- const watched = new Map<string, { at: number; ready: Promise<void> }>()
79
142
  const presenceSubscriptions = new Map<string, PresenceSubscription>()
80
143
  const pollingStreams = new Set<AsyncIterator<Event>>()
81
144
 
82
145
  options.onReconnect?.((channel) => {
83
- for (const wake of wakers.get(channel) ?? []) wake()
146
+ for (const feed of feeds.get(channel) ?? []) {
147
+ feed.dirty = true
148
+ feed.wake?.()
149
+ }
84
150
  void presenceSubscriptions
85
151
  .get(channel)
86
152
  ?.recovery?.reconnect()
@@ -115,11 +181,16 @@ export function createRedisNotifyStore(options: {
115
181
  subscriber ??= (async () => {
116
182
  const conn = (await client()).duplicate()
117
183
  conn.on('message', (channel, message) => {
118
- // wake() only resolves a promise; removal happens later via
119
- // disarm(), so iterating the live set is safe.
120
- const set = wakers.get(channel)
121
- if (set) {
122
- for (const wake of set) wake()
184
+ for (const feed of feeds.get(channel) ?? []) {
185
+ if (feed.bytes + message.length <= INBOX_BYTES) {
186
+ feed.inbox.push(message)
187
+ feed.bytes += message.length
188
+ } else {
189
+ feed.inbox.length = 0
190
+ feed.bytes = 0
191
+ feed.dirty = true
192
+ }
193
+ feed.wake?.()
123
194
  }
124
195
  const subscription = presenceSubscriptions.get(channel)
125
196
  if (subscription) deliverRemotePresence({ subscription, message })
@@ -204,36 +275,12 @@ export function createRedisNotifyStore(options: {
204
275
  lease.refs -= 1
205
276
  if (lease.refs > 0) return
206
277
  subscriptions.delete(channel)
207
- watched.delete(channel)
208
278
  void gatedSubscriber()
209
279
  .then((conn) => conn.call('unsubscribe', channel))
210
280
  .catch(() => {})
211
281
  }
212
282
 
213
- const armWaker = (
214
- channel: string,
215
- ): { wakeup: Promise<void>; wake: () => void; disarm: () => void } => {
216
- let wake!: () => void
217
- const wakeup = new Promise<void>((resolve) => {
218
- wake = resolve // assigned synchronously by the executor
219
- })
220
- let set = wakers.get(channel)
221
- if (!set) {
222
- set = new Set()
223
- wakers.set(channel, set)
224
- }
225
- set.add(wake)
226
- return {
227
- wakeup,
228
- wake,
229
- disarm: () => {
230
- set.delete(wake)
231
- if (set.size === 0) wakers.delete(channel)
232
- },
233
- }
234
- }
235
-
236
- return {
283
+ const store: A2Store & { close(): Promise<void> } = {
237
284
  append: core.append,
238
285
  read: core.read,
239
286
  readHistory: core.readHistory!,
@@ -243,6 +290,10 @@ export function createRedisNotifyStore(options: {
243
290
  failAttempt: core.failAttempt,
244
291
  readState: core.readState,
245
292
  putSnapshots: core.putSnapshots,
293
+ clear: core.clear!,
294
+ reclaim: core.reclaim!,
295
+ deadLetters: core.deadLetters!,
296
+ redrive: core.redrive!,
246
297
 
247
298
  presence: {
248
299
  async set(ns, participant, values, meta) {
@@ -311,18 +362,68 @@ export function createRedisNotifyStore(options: {
311
362
  return {
312
363
  [Symbol.asyncIterator](): AsyncIterator<Event> {
313
364
  let last = startAfter
314
- let buffer: Event[] = []
365
+ let buffer: HeldEvent[] = []
315
366
  let closed = false
316
367
  let subscribed = false
317
368
  let pending: Sleeper | null = null
318
- let interrupt: (() => void) | null = null
319
369
  let poller: AsyncIterator<Event> | undefined
320
370
  let stopActivity: (() => void) | undefined
371
+ let catchup:
372
+ | {
373
+ controller: AbortController
374
+ iterator: AsyncGenerator<SerializedEvent[], void>
375
+ delivered: boolean
376
+ }
377
+ | undefined
378
+ let stoppingRead: Promise<void> | undefined
379
+ const stopRead = (): Promise<void> => {
380
+ const reading = catchup
381
+ catchup = undefined
382
+ if (reading) {
383
+ reading.controller.abort()
384
+ stoppingRead = reading.iterator
385
+ .return(undefined)
386
+ .then(() => undefined)
387
+ .catch(() => {})
388
+ }
389
+ return stoppingRead ?? Promise.resolve()
390
+ }
391
+ const nextRead = async (
392
+ after: number,
393
+ ): Promise<IteratorResult<SerializedEvent[], void>> => {
394
+ if (!catchup) {
395
+ const controller = new AbortController()
396
+ catchup = {
397
+ controller,
398
+ delivered: false,
399
+ iterator: core.streamEvents!({
400
+ sessionId,
401
+ afterIndex: after,
402
+ signal: controller.signal,
403
+ })[Symbol.asyncIterator](),
404
+ }
405
+ }
406
+ const reading = catchup
407
+ const result = await reading.iterator.next()
408
+ if (result.done && catchup === reading) {
409
+ catchup = undefined
410
+ if (reading.delivered) feed.dirty = true
411
+ } else if (!result.done) reading.delivered = true
412
+ return result
413
+ }
414
+ const feed: Feed = {
415
+ inbox: [],
416
+ bytes: 0,
417
+ dirty: true,
418
+ wake: null,
419
+ stopRead,
420
+ }
321
421
  const activity = createStreamActivity(() => {
322
422
  if (poller) streamActivities.get(poller)?.activate()
323
423
  })
324
424
 
325
425
  const release = (): void => {
426
+ void stopRead()
326
427
  stopActivity?.()
327
428
  if (poller) {
328
429
  void poller.return?.()
@@ -330,9 +431,46 @@ export function createRedisNotifyStore(options: {
330
431
  }
331
432
  if (!subscribed) return
332
433
  subscribed = false
434
+ const set = feeds.get(channel)
435
+ set?.delete(feed)
436
+ if (set?.size === 0) feeds.delete(channel)
333
437
  releaseChannel(channel)
334
438
  }
335
439
 
440
+ /**
441
+ * Moves carried events that continue this feed's sequence into
442
+ * the buffer. A gap, a message without events, or an unreadable
443
+ * message marks the feed dirty so it catches up with XRANGE.
444
+ */
445
+ const applyInbox = (): void => {
446
+ feed.bytes = 0
447
+ for (const message of feed.inbox.splice(0)) {
448
+ const wake = parseWake(message)
449
+ if (!wake) {
450
+ feed.dirty = true
451
+ continue
452
+ }
453
+ for (const [index, id, type, payload] of wake.entries) {
454
+ const expected = (buffer.at(-1)?.index ?? last) + 1
455
+ if (index < expected) continue
456
+ if (index > expected) {
457
+ feed.dirty = true
458
+ break
459
+ }
460
+ buffer.push({
461
+ id,
462
+ type,
463
+ payload,
464
+ index,
465
+ sessionId,
466
+ createdAt: new Date(wake.createdAt),
467
+ })
468
+ }
469
+ if (wake.frontier > (buffer.at(-1)?.index ?? last))
470
+ feed.dirty = true
471
+ }
472
+ }
473
+
336
474
  const iterator: AsyncIterator<Event> = {
337
475
  async next(): Promise<IteratorResult<Event>> {
338
476
  for (;;) {
@@ -342,21 +480,41 @@ export function createRedisNotifyStore(options: {
342
480
  }
343
481
  const row = buffer.shift()
344
482
  if (row) {
483
+ let event: Event
484
+ try {
485
+ event = isSerializedEvent(row)
486
+ ? parseSerializedEvent(row)
487
+ : row
488
+ } catch (cause) {
489
+ // A retry re-reads from the last delivered event.
490
+ buffer = []
491
+ feed.dirty = true
492
+ release()
493
+ throw new A2Error(
494
+ 'STORE_UNAVAILABLE',
495
+ 'redis stream read failed',
496
+ { cause },
497
+ )
498
+ }
345
499
  last = row.index
346
- return { value: row, done: false }
500
+ return { value: event, done: false }
347
501
  }
348
502
  if (severed) {
349
503
  release()
350
504
  throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')
351
505
  }
352
- const waker = armWaker(channel)
353
- interrupt = waker.wake
354
506
  try {
355
507
  if (!subscribed) {
356
- // Subscribe before reading: anything appended
357
- // after the read lands as a wake-up, so nothing
358
- // can slip between catch-up and park.
508
+ // Register and subscribe before the first read:
509
+ // anything appended after it arrives in the inbox,
510
+ // so nothing slips between catch-up and park.
359
511
  subscribed = true
512
+ let set = feeds.get(channel)
513
+ if (!set) {
514
+ set = new Set()
515
+ feeds.set(channel, set)
516
+ }
517
+ set.add(feed)
360
518
  // oxlint-disable-next-line no-await-in-loop -- one-time setup
361
519
  await acquireChannel(channel)
362
520
  if (closed || severed) continue
@@ -385,37 +543,43 @@ export function createRedisNotifyStore(options: {
385
543
  )
386
544
  return result
387
545
  }
388
- // Mark watched before reading, for the same reason:
389
- // a write after this read sees the marker and
390
- // publishes. Throttled — one SET per safety period
391
- // per session per process, shared across feeds.
392
- let marker = watched.get(channel)
393
- if (
394
- !marker ||
395
- Date.now() - marker.at >= NOTIFY_TIMINGS.safetyReadMs
396
- ) {
397
- marker = {
398
- at: Date.now(),
399
- ready: core.markWatched(
400
- sessionId,
401
- NOTIFY_TIMINGS.safetyReadMs * 3,
402
- ),
403
- }
404
- watched.set(channel, marker)
546
+ if (catchup) {
547
+ // oxlint-disable-next-line no-await-in-loop -- decode the next catch-up batch on demand
548
+ const result = await nextRead(last)
549
+ if (closed || severed) continue
550
+ if (!result.done) buffer = result.value
551
+ continue
405
552
  }
406
- // Every viewer waits until writers can observe the marker.
407
- // oxlint-disable-next-line no-await-in-loop -- live-feed loop
408
- await marker.ready
409
- if (closed || severed) continue
410
- // oxlint-disable-next-line no-await-in-loop -- live-feed loop
411
- buffer = await core.readEvents(sessionId, last)
412
- if (buffer.length === 0 && !closed && !severed) {
413
- pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs)
553
+ applyInbox()
554
+ if (buffer.length > 0) continue
555
+ if (feed.dirty) {
556
+ feed.dirty = false
557
+ if (core.streamEvents) {
558
+ // oxlint-disable-next-line no-await-in-loop -- begin an incremental catch-up
559
+ const result = await nextRead(last)
560
+ if (closed || severed) continue
561
+ if (!result.done) buffer = result.value
562
+ continue
563
+ }
414
564
  // oxlint-disable-next-line no-await-in-loop -- live-feed loop
415
- await Promise.race([waker.wakeup, pending.promise])
416
- pending.cancel()
417
- pending = null
565
+ buffer = await core.readFeedPage(sessionId, last)
566
+ if (buffer.length >= FEED_PAGE_SIZE) feed.dirty = true
567
+ continue
418
568
  }
569
+ // Caught up: park until a message, a reconnect, or the
570
+ // safety read that heals a lost message.
571
+ const woken = new Promise<'wake'>((resolve) => {
572
+ feed.wake = () => resolve('wake')
573
+ })
574
+ pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs)
575
+ // oxlint-disable-next-line no-await-in-loop -- live-feed loop
576
+ const outcome = await Promise.race([
577
+ woken,
578
+ pending.promise.then(() => 'timer' as const),
579
+ ])
580
+ pending.cancel()
581
+ pending = null
582
+ if (outcome === 'timer') feed.dirty = true
419
583
  } catch (err) {
420
584
  release()
421
585
  if (closed) return { value: undefined, done: true }
@@ -426,20 +590,26 @@ export function createRedisNotifyStore(options: {
426
590
  { cause: err },
427
591
  )
428
592
  } finally {
429
- waker.disarm()
430
- interrupt = null
593
+ feed.wake = null
431
594
  }
432
595
  }
433
596
  },
434
597
  async return(): Promise<IteratorResult<Event>> {
435
598
  closed = true
436
599
  pending?.cancel()
437
- interrupt?.() // wakes an in-flight next() immediately
600
+ feed.wake?.() // wakes an in-flight next() immediately
438
601
  release()
602
+ await stopRead()
439
603
  return { value: undefined, done: true }
440
604
  },
441
605
  }
442
606
  streamActivities.set(iterator, activity)
607
+ bufferedEvents.set(iterator, () => {
608
+ if (closed) return undefined
609
+ const row = buffer.shift()
610
+ if (row) last = row.index
611
+ return row
612
+ })
443
613
  return iterator
444
614
  },
445
615
  }
@@ -452,9 +622,14 @@ export function createRedisNotifyStore(options: {
452
622
  presenceSubscriptions.clear()
453
623
  for (const poller of pollingStreams) void poller.return?.()
454
624
  pollingStreams.clear()
455
- for (const set of wakers.values()) {
456
- for (const wake of set) wake()
625
+ const reads: Promise<void>[] = []
626
+ for (const set of feeds.values()) {
627
+ for (const feed of set) {
628
+ feed.wake?.()
629
+ reads.push(feed.stopRead())
630
+ }
457
631
  }
632
+ await Promise.allSettled(reads)
458
633
  if (subscriber) {
459
634
  const sub = await subscriber.catch(() => null)
460
635
  sub?.disconnect()
@@ -462,4 +637,6 @@ export function createRedisNotifyStore(options: {
462
637
  await options.closeClient()
463
638
  },
464
639
  }
640
+ stateTailReaders.set(store, core.readTail)
641
+ return store
465
642
  }
@@ -4,15 +4,14 @@
4
4
  * with experimental-a2/store-redis-http); this module owns the connection and
5
5
  * the live feed.
6
6
  *
7
- * `stream()` is notify-driven: writes to watched sessions fire a
8
- * disposable PUBLISH wake-up (feeds hold a TTL'd presence marker the
9
- * write script checks, so unwatched sessions cost no wake-up), one
10
- * shared subscriber connection per backend serves every local feed,
11
- * and each wake triggers an `XRANGE` catch-up read. The notification
12
- * only decides when to read, never what — a lost one is healed by a
13
- * safety re-read (NOTIFY_TIMINGS), so delivery never depends on
14
- * pub/sub. Connections scale with processes (one command client plus
15
- * one subscriber), not with concurrent viewers.
7
+ * `stream()` is notify-driven: every write publishes a wake-up that
8
+ * carries the appended events, one shared subscriber connection per backend
9
+ * serves every local feed, and a caught-up feed applies carried events that
10
+ * continue its sequence without reading. A gap, an oversized batch, a
11
+ * reconnect, or the safety re-read (NOTIFY_TIMINGS) triggers an `XRANGE`
12
+ * catch-up, so a lost wake-up costs latency, never delivery. Connections
13
+ * scale with processes (one command client plus one subscriber), not with
14
+ * concurrent viewers.
16
15
  *
17
16
  * Works with any Redis-protocol server on a single instance or a
18
17
  * non-cluster provider (Upstash — durable by default — Redis, Valkey).
@@ -27,6 +26,7 @@ import {
27
26
  type RedisConnection,
28
27
  } from './store-redis-notify.ts'
29
28
  import { retryableLazy } from './retryable-lazy.ts'
29
+ import type { RedisCall } from './store-redis-core.ts'
30
30
  import {
31
31
  RANDOM_IDS,
32
32
  SYSTEM_CLOCK,
@@ -89,6 +89,16 @@ export function redis(options: RedisStoreOptions = {}): RedisStore {
89
89
 
90
90
  return createRedisNotifyStore({
91
91
  client,
92
+ // Only a client created from `url` is known to be ioredis, whose
93
+ // `callBuffer` returns bulk replies as raw bytes.
94
+ ...(options.client
95
+ ? {}
96
+ : {
97
+ callBytes: async (command, ...args) =>
98
+ (
99
+ (await client()) as RedisConnection & { callBuffer: RedisCall }
100
+ ).callBuffer(command, ...args),
101
+ }),
92
102
  clock,
93
103
  ids,
94
104
  keyPrefix: prefix,