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
@@ -0,0 +1,294 @@
1
+ ---
2
+ title: React
3
+ description: "Start from a snapshot, fold new events, and push optimistic writes through one shared session."
4
+ ---
5
+
6
+ ## Publish the state
7
+
8
+ The reducer lives with the event vocabulary, in a shared module:
9
+
10
+ ```ts model.ts
11
+ import { contract } from 'experimental-a2'
12
+ import { z } from 'zod'
13
+
14
+ export const counter = contract({
15
+ name: 'counter',
16
+ events: { added: z.number() },
17
+ reducer: {
18
+ name: 'count.v1',
19
+ initialState: 0,
20
+ fold: (state, event) => state + event.payload,
21
+ },
22
+ })
23
+ ```
24
+
25
+ A contract can publish one reducer. The client starts from a snapshot of its
26
+ state and folds later events locally, for one known session ID. Queries across
27
+ sessions belong in your [application database](/contract/sessions).
28
+
29
+ The reducer name identifies its [snapshot format](/contract/state#snapshots-are-a-cache):
30
+ change it when the fold's meaning or stored shape changes, and run the same
31
+ version on server and client. An inline fold returning a discriminated union
32
+ may need `as const` or an explicit return type; the
33
+ [`reducer` builder](/contract/state#reducers) fixes the state type first.
34
+ Other reducers read on the server with `session.state(reducer)` publish no
35
+ client state.
36
+
37
+ ```ts server/counter.ts
38
+ import { createServer } from 'experimental-a2/server'
39
+ import { counter } from '../model'
40
+
41
+ export const counterServer = createServer({ contract: counter })
42
+ ```
43
+
44
+ ```ts app/api/counter/route.ts
45
+ import { counterServer } from '../../../server/counter'
46
+
47
+ export const GET = counterServer.fetch
48
+ export const POST = counterServer.fetch
49
+ ```
50
+
51
+ `GET` serves the subscription and history reads; `POST` appends writes.
52
+ Authentication and `authorize` live on [Routes](/server/routes). Bootstrap
53
+ is authorized as a `stream` operation with `bootstrap: true` before A2 reads
54
+ state.
55
+
56
+ ## Bind React once
57
+
58
+ `createReact` binds the client's types into a Provider and hook. It creates
59
+ no separate connection pool.
60
+
61
+ ```tsx app/counter/session.ts
62
+ 'use client'
63
+ import { createClient } from 'experimental-a2/client'
64
+ import { createReact } from 'experimental-a2/react'
65
+ import { counter } from '../../model'
66
+
67
+ export const counterClient = createClient({
68
+ contract: counter,
69
+ api: '/api/counter',
70
+ })
71
+
72
+ export const { SessionProvider, useSession } = createReact({
73
+ client: counterClient,
74
+ })
75
+ ```
76
+
77
+ ```tsx app/counter/counter.tsx
78
+ 'use client'
79
+ import { useSession } from './session'
80
+
81
+ export function Counter() {
82
+ const { state, connection, push } = useSession()
83
+ const error = 'error' in connection ? connection.error : null
84
+
85
+ return (
86
+ <>
87
+ <button onClick={() => void push({ type: 'added', payload: 1 })}>
88
+ Count: {state}
89
+ </button>
90
+ {error && <p role="alert">{error.message}</p>}
91
+ </>
92
+ )
93
+ }
94
+ ```
95
+
96
+ `useSession()` returns `state`, `hydrated`, `events`, `index`, `connection`,
97
+ `push`, `reconnect`, `loadHistory`, and `history`; presence contracts add
98
+ `presence` and `setPresence`. The [reference](/reference/client) covers each.
99
+
100
+ Without a [seed](#seed-the-first-render), `state` starts at
101
+ `reducer.initialState` (through the reducer's client projection when present),
102
+ which can look empty while an existing session loads. The subscription's first
103
+ frame is a snapshot that replaces it atomically. Later events fold on top,
104
+ including any appended while the snapshot was read. `connection` stays
105
+ `connecting` until then; bootstrap failures retry like any stream failure, and
106
+ a reconnect keeps the last state.
107
+
108
+ A push during bootstrap is sent right away, and its optimistic update appears
109
+ once the snapshot is applied. If it is still unacknowledged when the snapshot
110
+ arrives, it stays hidden until its ack (the snapshot may already contain it),
111
+ along with the pushes queued behind it, so the view never skips a write. The
112
+ push promise resolves on the server ack; `.confirmed` waits for stream
113
+ delivery, and the snapshot counts as delivery through its index.
114
+
115
+ ## Without server rendering
116
+
117
+ A Provider needs only the session ID. It supplies scope and optional data;
118
+ mounted hooks own connection leases.
119
+
120
+ ```tsx app/counter/page.tsx
121
+ import { Counter } from './counter'
122
+ import { SessionProvider } from './session'
123
+
124
+ export default function Page() {
125
+ return (
126
+ <SessionProvider sessionId="shared">
127
+ <Counter />
128
+ </SessionProvider>
129
+ )
130
+ }
131
+ ```
132
+
133
+ Or skip the Provider. The bound hook already knows its client:
134
+
135
+ ```tsx app/counter/standalone.tsx
136
+ 'use client'
137
+ import { useSession } from './session'
138
+
139
+ export function StandaloneCounter({ sessionId }: { sessionId: string }) {
140
+ const { state, push } = useSession({ sessionId })
141
+ return (
142
+ <button onClick={() => void push({ type: 'added', payload: 1 })}>
143
+ {state}
144
+ </button>
145
+ )
146
+ }
147
+ ```
148
+
149
+ Without the factory, the direct `useSession(client, sessionId, options?)`
150
+ export behaves the same.
151
+
152
+ ## Seed the first render
153
+
154
+ Server rendering can skip the bootstrap read. Supply the state and its index
155
+ together:
156
+
157
+ ```tsx app/seeded-counter/page.tsx
158
+ import { counterServer } from '../../server/counter'
159
+ import { Counter } from '../counter/counter'
160
+ import { SessionProvider } from '../counter/session'
161
+
162
+ export default async function Page() {
163
+ const initial = await counterServer.session('shared').state()
164
+ return (
165
+ <SessionProvider sessionId="shared" initial={initial}>
166
+ <Counter />
167
+ </SessionProvider>
168
+ )
169
+ }
170
+ ```
171
+
172
+ Index 10 means the state includes events through 10, so the stream starts
173
+ after 10. Index 0 with the reducer's initial state requests replay from the
174
+ beginning; an index alone cannot describe a state. The standalone hook takes
175
+ the same `initial` option.
176
+
177
+ Seeds are render-local: one request's seed never changes another's state, and
178
+ hydration first renders the supplied snapshot even when the browser cache has
179
+ advanced.
180
+
181
+ `hydrated` turns `true` once a supplied snapshot or bootstrap initializes the
182
+ session (index 0 counts) and stays `true` through disconnections, reconnects,
183
+ and disabled readers while the session keeps its state. It is `false` while
184
+ only the reducer seed shows, including during a pending or rejected cold
185
+ preload. Use it to tell an empty queue from an unloaded one; it says nothing
186
+ about freshness, connectivity, or permission to write. Imperative code reads
187
+ `client.session(id).getSnapshot().hydrated`.
188
+
189
+ ## Start a preload on the server
190
+
191
+ `initial` also accepts a Promise of a snapshot:
192
+
193
+ ```tsx app/preloaded-counter/page.tsx
194
+ import { Suspense } from 'react'
195
+ import { counterServer } from '../../server/counter'
196
+ import { SessionProvider } from '../counter/session'
197
+ import { PreloadedCounter } from './counter'
198
+
199
+ export default function Page() {
200
+ const initial = counterServer.session('shared').state()
201
+ return (
202
+ <SessionProvider sessionId="shared" initial={initial}>
203
+ <h1>Counter</h1>
204
+ <Suspense fallback={<p>Loading counter…</p>}>
205
+ <PreloadedCounter />
206
+ </Suspense>
207
+ </SessionProvider>
208
+ )
209
+ }
210
+ ```
211
+
212
+ ```tsx app/preloaded-counter/counter.tsx
213
+ 'use client'
214
+ import { useSession } from '../counter/session'
215
+
216
+ export function PreloadedCounter() {
217
+ const { state, push } = useSession({ suspend: true })
218
+ return (
219
+ <button onClick={() => void push({ type: 'added', payload: 1 })}>
220
+ {state}
221
+ </button>
222
+ )
223
+ }
224
+ ```
225
+
226
+ A cold session waits for the Promise before subscribing, showing
227
+ `reducer.initialState` meanwhile. A snapshot result skips the bootstrap read;
228
+ `null` or `undefined` falls back to it. A2 starts no extra request.
229
+
230
+ `suspend: true` reads the Promise with React `use()`, so the nearest Suspense
231
+ and Error boundaries handle pending and rejected states. Without it, the
232
+ server response renders the reducer seed; to render the loaded session there,
233
+ await the snapshot or suspend inside a boundary. The Provider and automatic
234
+ bootstrap never suspend. Create the Promise outside the consuming component so
235
+ its identity stays stable.
236
+
237
+ A rejected cold preload pauses startup and reports through `connection`. A2
238
+ cannot retry your Promise: supply a new Promise, a snapshot, or `null`. To
239
+ fall back to bootstrap instead, catch the failure in your Promise and return
240
+ `null`.
241
+
242
+ A warm session keeps its state and connection while a new Promise is pending;
243
+ a newer snapshot hydrates as usual, and a late rejection does not disrupt it.
244
+ Replacing the source or calling the session's `close()` discards unapplied
245
+ results. Unmounting releases the hook's lease but never cancels your fetch,
246
+ and repeating the same Promise in a cached session does not restart cancelled
247
+ work.
248
+
249
+ ## Connections and activation
250
+
251
+ Hooks on the same client instance and session ID share one runtime,
252
+ connection, state, and optimistic queue, under any Providers. Another client
253
+ instance is isolated, even with the same URL. A reconnect resumes after the
254
+ last delivered event, without another snapshot.
255
+
256
+ `useSession({ enabled: false })` observes cached or initial data without
257
+ taking a connection lease (default `true`). Disabling or unmounting a hook
258
+ releases only its own lease. Explicit pushes still acquire a connection, so
259
+ disabling is not an authorization boundary.
260
+
261
+ The bound hook accepts `sessionId`, `client`, `participant`, `initial`,
262
+ `enabled`, and `suspend`. It inherits the Provider's initial data and
263
+ participant only when the resolved client and session ID match the
264
+ Provider's.
265
+
266
+ ## Events and history
267
+
268
+ `events` is the raw feed: supplied `initialEvents`, loaded history, live
269
+ events, and pending optimistic events. A snapshot adds no historical rows, so
270
+ without `initialEvents` the feed starts after the snapshot's index.
271
+
272
+ `loadHistory({ before, limit })` loads older entries into `events`, by default
273
+ up to 50 before the oldest loaded one, without touching state or the live
274
+ cursor. `history` reports loading, the oldest loaded index, and whether the
275
+ beginning is loaded. History reads need the HTTP
276
+ [transport](/client/transports).
277
+
278
+ `events` lives in memory, unpaginated, so long-lived sessions accumulate
279
+ events. Snapshots bound replay work, not the size of state or retained events:
280
+ choose reducer contents that fit.
281
+
282
+ ## Client lifetime
283
+
284
+ Define the client once in a module, as `session.ts` does, so components
285
+ share its sessions. `createClient({ gcTime })` keeps an idle session cached,
286
+ five minutes by default; active connections, listeners, and pending work keep
287
+ it alive.
288
+
289
+ The cache is in memory, so a reload discards it. A2 ships no IndexedDB adapter
290
+ (`experimental-a2/cache-indexeddb` has no implementation) and no persistent
291
+ offline write queue. Pushes retry a bounded number of times and roll back when
292
+ they fail. To persist state yourself, restore a state/index pair through
293
+ `initial`, only for the same session and reducer version, and keep
294
+ authentication and retention policy in your application.
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Transports
3
+ description: "One client, three wires: one HTTP route, split routes for per-verb timeouts, or one WebSocket."
4
+ ---
5
+
6
+ ## One wire seam, same client
7
+
8
+ `api` picks the wire:
9
+
10
+ ```ts
11
+ // in a 'use client' session module:
12
+ api: '/api/order-events'
13
+ api: (sessionId) => `/api/orders/${encodeURIComponent(sessionId)}/events`
14
+ api: { type: 'http', push: '/api/order-push', stream: '/api/order-stream' }
15
+ api: { type: 'ws', url: '/api/order-events' }
16
+ ```
17
+
18
+ - **One route** is the default and the recommendation: a fixed URL or a
19
+ session URL function, serving GET (SSE stream and history) and POST (push).
20
+ - **`http` split** serves the two verbs from separate routes, for platforms
21
+ whose duration limits differ per verb.
22
+ - **`ws`** carries streams, pushes, and presence over one socket per session.
23
+ A socket is one connection in both directions, so it has no split form.
24
+
25
+ The server side of each lives on [Routes](/server/routes). Everything above
26
+ the wire is shared: `useSession`, push acks and `confirmed`, presence
27
+ cadence, connection status, stall detection, and reconnection. SSE and
28
+ WebSocket resume from the same durable frontier and treat presence as
29
+ ephemeral, repaintable state.
30
+
31
+ ## Session URL functions
32
+
33
+ Every session-scoped URL may be a `(sessionId) => string` function. A2
34
+ resolves it for each request or reconnect, then adds its standard query
35
+ parameters. Encode the ID when placing it in a path segment.
36
+
37
+ The function chooses a route, not authority. If a path or connection token is
38
+ scoped to one session, [`authorize`](/server/routes#the-http-route) must
39
+ compare that scope to `operation.sessionId`. Bind URL tokens to the same
40
+ session and keep them short-lived, because URLs can appear in infrastructure
41
+ logs.
42
+
43
+ ## WebSocket
44
+
45
+ ```ts app/orders/[orderId]/session.ts
46
+ 'use client'
47
+ import { createClient } from 'experimental-a2/client'
48
+ import { createReact } from 'experimental-a2/react'
49
+ import { orders } from '@/contracts'
50
+
51
+ export const ordersClient = createClient({
52
+ contract: orders,
53
+ api: { type: 'ws', url: '/api/order-events' },
54
+ })
55
+ export const { SessionProvider, useSession } = createReact({
56
+ client: ordersClient,
57
+ })
58
+ ```
59
+
60
+ To share one socket across the client's sessions, set `multiplex: true` in
61
+ `api` and `multiplexWebSocket: true` on the
62
+ [route](/server/routes#the-websocket-route). A multiplexed URL is one plain
63
+ string, since the connection carries more than one session.
64
+
65
+ `ws` has no history lane. [`loadHistory`](/client/react#events-and-history)
66
+ is a bounded cold read over plain HTTP; on a `ws` api it throws a
67
+ `TypeError`.
68
+
69
+ ## Lifecycle
70
+
71
+ When a socket closes, the client reconnects at its current frontier per its
72
+ reconnection policy, receives a fresh presence snapshot, and re-sends its own
73
+ latest presence. A multiplexed socket re-subscribes every active session at
74
+ its own frontier. Pushes keep their client-generated ids, so retries stay
75
+ idempotent.
76
+
77
+ The `createClient({ reconnect })` policy returns a delay, or `false` to
78
+ pause. The default reconnects with backoff indefinitely. A clean close also
79
+ runs the policy, with `error: null`. A paused session keeps its state and
80
+ leases; `session.reconnect()` resumes it.
81
+
82
+ Upgrade authorization stays in the
83
+ [HTTP route](/server/routes#the-websocket-route). Browser WebSockets hide a
84
+ rejected upgrade's status and body, so it surfaces as an opaque
85
+ `CONNECTION_FAILED` error. Structured subscription errors arrive once a
86
+ multiplexed socket is open.
@@ -0,0 +1,224 @@
1
+ ---
2
+ title: Presence
3
+ description: "Ephemeral participant state on a durable session: cursors, typing, live progress. Presence repaints; the log replays."
4
+ ---
5
+
6
+ ## Two planes, one session
7
+
8
+ Events are facts: stored, folded by reducers, replayed on demand. Presence is
9
+ not. "Her cursor is at (312, 88)" is useful for about two seconds, has no
10
+ replay value, and a lost update is repainted by the next one. The test:
11
+
12
+ > Will anyone, human or reducer, care about this datum after the moment
13
+ > passes? Yes: event, in the log. No: presence.
14
+
15
+ Messages, document updates, cancellations: events. Cursors, selections,
16
+ typing, "who's online", a handler's live progress: presence.
17
+
18
+ Presence rides the session you already have: same contract, route, auth, and
19
+ stream. A2 replicates each participant's latest field values to every
20
+ subscriber and stores none of them in the log. Presence never appears in
21
+ `history()`, `state()`, or a reducer's input type; never arms the scheduler,
22
+ takes a claim, retries, or blocks an append; and never enters the browser
23
+ cache.
24
+
25
+ > **Presence repaints; the log replays.** Anything that decides (a fold,
26
+ > a retry, an abort match) reads the log. Presence only hurries pixels.
27
+
28
+ ## Declare it in the contract
29
+
30
+ `presence` is a second vocabulary next to `events`, one schema per field of a
31
+ participant's state:
32
+
33
+ ```ts contracts.ts
34
+ import { z } from 'zod'
35
+ import * as a2 from 'experimental-a2'
36
+
37
+ export const canvas = a2.contract({
38
+ name: 'canvas',
39
+ events: {
40
+ 'shape.created': z.object({
41
+ id: z.string(),
42
+ kind: z.enum(['rect', 'ellipse']),
43
+ x: z.number(),
44
+ y: z.number(),
45
+ }),
46
+ 'shape.moved': z.object({ id: z.string(), x: z.number(), y: z.number() }),
47
+ },
48
+ reducer: { name: 'scene', initialState: {}, fold: (state) => state },
49
+ presence: {
50
+ cursor: z.object({ x: z.number(), y: z.number() }),
51
+ dragging: z.object({ shapeId: z.string(), x: z.number(), y: z.number() }),
52
+ },
53
+ })
54
+ ```
55
+
56
+ The field is the unit of replication: last writer wins per participant per
57
+ field, so setting `cursor` leaves `dragging` alone. Fields are absent until
58
+ set. They clear when set to `null` (pointer leaves the canvas, blur, unmount)
59
+ or when their TTL lapses, so a closed tab disappears by silence, with no
60
+ explicit leave. The TTL is 60 seconds by default; set
61
+ `createServer({ presence: { ttlMs } })` to change it. Expiry counts on the
62
+ backend's clock, not the sender's stamp.
63
+
64
+ ## The route
65
+
66
+ ```ts app/api/canvas-events/route.ts
67
+ import { canvasServer } from '@/server/canvas'
68
+
69
+ export const GET = canvasServer.fetch
70
+ export const POST = canvasServer.fetch
71
+ ```
72
+
73
+ This is the usual [`server.fetch` route](/server/routes). A contract with
74
+ presence fields streams presence patches with events, starting with a snapshot
75
+ of the current map, and its push body accepts an optional `presence` sibling
76
+ to `events`, forwarded to `setPresence`. `authorize` sees the complete
77
+ presence patch, including its caller-supplied participant id, over HTTP and
78
+ WebSocket.
79
+
80
+ `setPresence` validates each field against the contract, then broadcasts: no
81
+ append, no dispatch, no log row. A bad field throws `INVALID_PAYLOAD` and an
82
+ unknown one throws `UNKNOWN_PRESENCE_FIELD`, broadcasting nothing. A
83
+ presence-only push acks `[]`.
84
+
85
+ ## The browser
86
+
87
+ Presence needs a `participant` id, yours to mint: a user id, a tab nonce. Set
88
+ it on `createClient({ participant })` when the app knows it at module scope,
89
+ or on the Provider, whose `participant` prop overrides the client's. The
90
+ session module is the usual [pair](/client/react#bind-react-once):
91
+
92
+ ```ts app/canvas/[canvasId]/session.ts
93
+ 'use client'
94
+ import { createClient } from 'experimental-a2/client'
95
+ import { createReact } from 'experimental-a2/react'
96
+ import { canvas } from '@/contracts'
97
+
98
+ export const canvasClient = createClient({
99
+ contract: canvas,
100
+ api: '/api/canvas-events',
101
+ })
102
+ export const { SessionProvider, useSession } = createReact({
103
+ client: canvasClient,
104
+ })
105
+ ```
106
+
107
+ ```tsx app/canvas/[canvasId]/canvas-client.tsx
108
+ 'use client'
109
+ import { useSession } from './session'
110
+
111
+ export function CanvasClient() {
112
+ const { presence, setPresence } = useSession()
113
+
114
+ return (
115
+ <div
116
+ onPointerMove={(e) => setPresence({ cursor: { x: e.clientX, y: e.clientY } })}
117
+ >
118
+ {Object.entries(presence).map(([participant, fields]) =>
119
+ // render-time expiry: a cursor silent for a few seconds is gone
120
+ fields.cursor && Date.now() - fields.cursor.at.getTime() < 5_000 ? (
121
+ <Cursor key={participant} {...fields.cursor.value} />
122
+ ) : null,
123
+ )}
124
+ </div>
125
+ )
126
+ }
127
+
128
+ // your cursor sprite:
129
+ // function Cursor({ x, y }: { x: number; y: number }) { ... }
130
+ const Cursor = (props: { x: number; y: number }) => null
131
+ ```
132
+
133
+ - **`setPresence(values)`** is fire-and-forget. It validates locally, merges
134
+ into the local map immediately, and coalesces on the wire: the first set
135
+ sends immediately, and later values within the interval merge per field
136
+ (latest wins, `null` included) into one trailing send. A 60Hz pointer
137
+ becomes about fifteen sends per second. No ack, no `confirmed`, no retry,
138
+ no rollback: nothing was ever true.
139
+ - **`presence`** is the replicated map,
140
+ `Record<participantId, { [field]: { value, seen, at } }>`, including your
141
+ own participant. `at` is the sender's stamp for when the value was set and
142
+ the last-writer-wins comparator. The client stamps sends monotonically, so
143
+ overlapping sends never invert. How long to paint a value is view logic,
144
+ decided at render time.
145
+
146
+ Both members are typed from the contract's `presence` map, with no type
147
+ arguments. On a contract without `presence` they do not exist, so reaching
148
+ for them is a compile error.
149
+
150
+ ## Reconciling the planes: `seen`
151
+
152
+ Every presence value carries `seen`, the sender's stream frontier when it was
153
+ set, stamped automatically. It orders the two planes without wall clocks.
154
+
155
+ The classic race: a participant drags a shape (presence), drops it (event),
156
+ and a stale `dragging` patch arrives after the durable `shape.moved`. Without
157
+ a shared order, the shape flickers backward. With `seen`, superseding is one
158
+ render-time rule:
159
+
160
+ ```ts
161
+ // in render code: a ghost is dead once the log has moved past it.
162
+ // show fields.dragging only if
163
+ // fields.dragging.seen >= state.shapes[fields.dragging.value.shapeId].movedAt
164
+ // where movedAt is the index your reducer recorded for the last shape.moved
165
+ ```
166
+
167
+ The durable event at index 214 beats any presence stamped 213 or lower. A2
168
+ carries the stamp; the comparison is yours, in view code.
169
+
170
+ ## Handlers broadcast too
171
+
172
+ `ctx.session.setPresence` is the same operation, so a handler can publish live
173
+ progress without writing non-facts to the log:
174
+
175
+ ```ts server/canvas.ts
176
+ import { createServer } from 'experimental-a2/server'
177
+ import { canvas } from '@/contracts'
178
+
179
+ export const canvasServer = createServer({
180
+ contract: canvas,
181
+ handlers: {
182
+ 'shape.created': async (ctx) => {
183
+ // long work; broadcast progress as it goes:
184
+ // await ctx.session.setPresence({
185
+ // participant: 'system:thumbnails',
186
+ // values: { progress: { step: 2, of: 5 } },
187
+ // })
188
+ },
189
+ },
190
+ })
191
+ ```
192
+
193
+ The participant id is the handler's choice; a `system:` prefix keeps robots
194
+ apart from humans in the map. When the work completes, append the outcome as
195
+ an event: the progress was never a fact, the result is.
196
+
197
+ ## Backends
198
+
199
+ Declaring `presence` requires a store with the presence capability;
200
+ otherwise `createServer` throws `PRESENCE_NOT_SUPPORTED`. Every built-in
201
+ store has it, at two tiers:
202
+
203
+ | Backend | Delivery |
204
+ | --- | --- |
205
+ | `store-redis` | push: TTL'd hash plus pub/sub patches |
206
+ | `store-redis-http` | push over HTTP subscriptions; adaptive polling when unsupported |
207
+ | `store-memory` | in-process, immediate |
208
+ | `store-postgres` | native notifications with `listenConnectionString`; adaptive polling by default |
209
+ | `store-sqlite` | degraded: bounded TTL'd rows, read on the live feed's poll cadence |
210
+
211
+ Degraded means later, not lost while watched. Presence-only traffic re-reads
212
+ adaptively, from 25ms backing off to two seconds while unchanged; new patches
213
+ or events restore the fast cadence. An idle remote viewer may wait up to two
214
+ seconds plus query latency for the first change. Cursors want the push tier;
215
+ typing indicators and progress read fine on either.
216
+
217
+ Redis Pub/Sub does not broadcast TTL expiry: silent departures keep their
218
+ last values in connected maps until a snapshot reconciles them, so expire at
219
+ render time as above. Native Postgres subscriptions reconcile missing or
220
+ expired fields on their ten-second safety reads.
221
+
222
+ Presence rows are bounded per session and participant, so they are not log
223
+ growth in disguise. On metered backends every patch is still a network
224
+ operation.
@@ -0,0 +1 @@
1
+ export default { order: 4 }
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Events
3
+ description: "A contract names every event a session can hold, with a schema for each payload."
4
+ ---
5
+
6
+ ## Declare the events
7
+
8
+ ```ts contracts.ts
9
+ import { z } from 'zod'
10
+ import * as a2 from 'experimental-a2'
11
+
12
+ export const orders = a2.contract({
13
+ name: 'orders',
14
+ events: {
15
+ created: z.object({ shopId: z.string(), items: z.array(z.string()) }),
16
+ 'shop.notified': z.object({}),
17
+ 'shop.started': z.object({}),
18
+ 'driver.notified': z.object({ driverId: z.string() }),
19
+ expired: z.object({}),
20
+ },
21
+ })
22
+ ```
23
+
24
+ A **contract** defines what can happen. A [session](/contract/sessions) is one
25
+ instance of it.
26
+
27
+ `name` identifies the contract everywhere: in storage, in queue messages, in
28
+ logs. It keeps an app with several contracts unambiguous.
29
+
30
+ `events` is the full vocabulary. Each key is an event name; each value is a
31
+ schema for its payload. These docs use Zod, but any
32
+ [Standard Schema](https://standardschema.dev) library works: Valibot, ArkType,
33
+ your own.
34
+
35
+ The contract is a plain, importable, **isomorphic** value. The server
36
+ implements it with [handlers](/server/handlers), [reducers](/contract/state)
37
+ derive the current view from it, and the browser types its pushes off it.
38
+
39
+ ## Validation
40
+
41
+ Every append is validated against the contract. A bad payload throws
42
+ `INVALID_PAYLOAD` before anything is written; an unknown type throws
43
+ `UNKNOWN_EVENT_TYPE`. The log stores the validated output, with defaults
44
+ applied and transforms run.
45
+
46
+ ## Presence
47
+
48
+ A contract can also declare `presence`: ephemeral participant state (cursors,
49
+ typing, live progress) that is replicated but never stored in the log. See
50
+ [Presence](/client/presence).
51
+
52
+ :::tip
53
+ Events are facts, so name them in past tense: `created`, `expired`,
54
+ `shop.notified`. A name that reads like a command (`notifyShop`) describes
55
+ what a handler should do, not what happened. Dots are a naming convention, not
56
+ a namespace.
57
+ :::