experimental-a2 0.19.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
  4. package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/{actor-shared-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-CvEnqVfo.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
  17. package/dist/ai-CzE672Q3.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +1 -1
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +36 -9
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
  25. package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
  26. package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +4 -4
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +84 -75
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
  40. package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
  52. package/dist/internal-Bc4PvfHk.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-BWmht60z.js +12 -0
  65. package/dist/runtime-consumer-BWmht60z.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +7 -7
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  71. package/dist/scheduler-vercel.d.ts +2 -2
  72. package/dist/scheduler-vercel.d.ts.map +1 -1
  73. package/dist/scheduler-vercel.js +5 -4
  74. package/dist/scheduler-vercel.js.map +1 -1
  75. package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
  76. package/dist/server-BshFZ0eL.d.ts.map +1 -0
  77. package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
  78. package/dist/server-DvIB8kj5.js.map +1 -0
  79. package/dist/server.browser.js +1 -1
  80. package/dist/server.browser.js.map +1 -1
  81. package/dist/server.d.ts +4 -4
  82. package/dist/server.js +2 -2
  83. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
  84. package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
  85. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  86. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  87. package/dist/store-N8PXxDAS.js.map +1 -1
  88. package/dist/store-Z2qjw50n.d.ts +523 -0
  89. package/dist/store-Z2qjw50n.d.ts.map +1 -0
  90. package/dist/store-codec-DwrWlgQx.js +65 -0
  91. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  92. package/dist/store-memory.d.ts +1 -1
  93. package/dist/store-memory.d.ts.map +1 -1
  94. package/dist/store-memory.js +393 -210
  95. package/dist/store-memory.js.map +1 -1
  96. package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
  97. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
  98. package/dist/store-postgres.d.ts +1 -1
  99. package/dist/store-postgres.d.ts.map +1 -1
  100. package/dist/store-postgres.js +1595 -673
  101. package/dist/store-postgres.js.map +1 -1
  102. package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
  103. package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
  104. package/dist/store-redis-http.d.ts +1 -1
  105. package/dist/store-redis-http.js +4 -4
  106. package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
  107. package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
  108. package/dist/store-redis.d.ts +1 -1
  109. package/dist/store-redis.js +2 -2
  110. package/dist/store-sqlite.d.ts +1 -1
  111. package/dist/store-sqlite.d.ts.map +1 -1
  112. package/dist/store-sqlite.js +517 -287
  113. package/dist/store-sqlite.js.map +1 -1
  114. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  115. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  116. package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
  117. package/dist/wire-CZGqNZRx.js.map +1 -0
  118. package/docs/01-quickstart.mdx +28 -19
  119. package/docs/02-examples.mdx +1 -1
  120. package/docs/actors/01-introduction.mdx +9 -1
  121. package/docs/actors/02-concurrency.mdx +1 -1
  122. package/docs/actors/meta.ts +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +13 -12
  125. package/docs/agents/03-conversations.mdx +31 -21
  126. package/docs/agents/04-harnesses.mdx +7 -3
  127. package/docs/agents/meta.ts +1 -1
  128. package/docs/client/03-presence.mdx +18 -14
  129. package/docs/client/meta.ts +1 -1
  130. package/docs/contract/01-events.mdx +2 -2
  131. package/docs/contract/02-state.mdx +18 -14
  132. package/docs/contract/03-sessions.mdx +32 -25
  133. package/docs/index.mdx +31 -26
  134. package/docs/reference/01-core.mdx +12 -12
  135. package/docs/reference/02-server.mdx +247 -192
  136. package/docs/reference/04-ai.mdx +29 -20
  137. package/docs/reference/05-telemetry.mdx +146 -0
  138. package/docs/reference/06-errors.mdx +24 -18
  139. package/docs/reference/meta.ts +1 -1
  140. package/docs/schedulers/01-overview.mdx +112 -0
  141. package/docs/schedulers/02-vercel-queues.mdx +87 -0
  142. package/docs/schedulers/03-qstash.mdx +123 -0
  143. package/docs/schedulers/meta.ts +1 -0
  144. package/docs/server/01-consumers.mdx +415 -0
  145. package/docs/server/02-timers.mdx +33 -23
  146. package/docs/server/03-cancellation.mdx +52 -44
  147. package/docs/server/04-routes.mdx +4 -1
  148. package/docs/server/05-sql-projections.mdx +391 -0
  149. package/docs/server/06-guarantees.mdx +60 -50
  150. package/docs/stores/01-overview.mdx +37 -0
  151. package/docs/stores/02-redis.mdx +57 -0
  152. package/docs/stores/03-redis-http.mdx +56 -0
  153. package/docs/stores/04-postgres.mdx +172 -0
  154. package/docs/stores/05-sqlite.mdx +36 -0
  155. package/docs/stores/06-memory.mdx +22 -0
  156. package/docs/stores/meta.ts +1 -0
  157. package/docs/upgrades/01-overview.mdx +30 -0
  158. package/docs/upgrades/79-0-21.mdx +165 -0
  159. package/docs/upgrades/80-0-20.mdx +43 -0
  160. package/docs/upgrades/81-0-19.mdx +33 -0
  161. package/docs/upgrades/82-0-18.mdx +33 -0
  162. package/docs/upgrades/83-0-17.mdx +69 -0
  163. package/docs/upgrades/meta.ts +3 -0
  164. package/examples/playground/DEPLOY.md +48 -11
  165. package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
  166. package/examples/playground/app/a0/chat.test.tsx +421 -330
  167. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  168. package/examples/playground/app/a0/server.ts +4 -4
  169. package/examples/playground/app/a0/shell.tsx +1 -1
  170. package/examples/playground/app/a0/uploads/route.test.ts +59 -0
  171. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  172. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  173. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  174. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  175. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  176. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  177. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  178. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  179. package/examples/playground/app/a2slack/db/a2.ts +206 -0
  180. package/examples/playground/app/a2slack/db/index.ts +73 -0
  181. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  182. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  183. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  184. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  186. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  187. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  188. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  189. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
  190. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  191. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  192. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  193. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  194. package/examples/playground/app/a2slack/events/route.ts +36 -0
  195. package/examples/playground/app/a2slack/identity.ts +44 -0
  196. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  197. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  198. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  199. package/examples/playground/app/a2slack/lag.ts +73 -0
  200. package/examples/playground/app/a2slack/layout.tsx +43 -0
  201. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  202. package/examples/playground/app/a2slack/model.test.ts +208 -0
  203. package/examples/playground/app/a2slack/model.ts +369 -0
  204. package/examples/playground/app/a2slack/models.test.ts +72 -0
  205. package/examples/playground/app/a2slack/page.tsx +10 -0
  206. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  207. package/examples/playground/app/a2slack/search/route.ts +40 -0
  208. package/examples/playground/app/a2slack/search-index.ts +108 -0
  209. package/examples/playground/app/a2slack/server.test.ts +315 -0
  210. package/examples/playground/app/a2slack/server.ts +55 -0
  211. package/examples/playground/app/a2slack/sessions.ts +50 -0
  212. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  213. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  214. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  215. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  216. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  217. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  218. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  219. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  220. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  221. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  222. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  223. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  224. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  225. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  226. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  227. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  228. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
  229. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
  230. package/examples/playground/app/agent/ingress.test.ts +41 -17
  231. package/examples/playground/app/agent/model-input.test.tsx +92 -43
  232. package/examples/playground/app/agent/server.ts +3 -3
  233. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
  234. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  235. package/examples/playground/app/chat/server.test.ts +38 -65
  236. package/examples/playground/app/chat/server.ts +45 -35
  237. package/examples/playground/app/components/activity-history.test.tsx +60 -18
  238. package/examples/playground/app/components/demo-icons.tsx +7 -0
  239. package/examples/playground/app/components/demos.tsx +7 -0
  240. package/examples/playground/app/components/fast-link.test.tsx +3 -5
  241. package/examples/playground/app/initial-state.test.tsx +86 -45
  242. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  243. package/examples/playground/app/orders/page.tsx +1 -1
  244. package/examples/playground/app/orders/server.ts +49 -30
  245. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  246. package/examples/playground/app/recovery/page.tsx +4 -4
  247. package/examples/playground/app/recovery/server.ts +23 -32
  248. package/examples/playground/drizzle.config.ts +14 -0
  249. package/examples/playground/next.config.ts +6 -1
  250. package/examples/playground/package.json +13 -1
  251. package/examples/playground/scripts/migrate.mjs +31 -0
  252. package/examples/playground/test/agent.ts +35 -0
  253. package/examples/playground/test/model.ts +149 -0
  254. package/examples/playground/test/serve.ts +184 -0
  255. package/examples/playground/test/server-only.ts +1 -0
  256. package/examples/playground/vercel.json +9 -0
  257. package/examples/playground/vitest.config.ts +27 -0
  258. package/examples/playground/vitest.drills.config.ts +32 -0
  259. package/package.json +7 -2
  260. package/sql/postgres/drizzle-schema.ts +206 -0
  261. package/sql/postgres/notifications.sql +24 -0
  262. package/sql/postgres/schema.sql +276 -0
  263. package/src/actor.ts +101 -71
  264. package/src/ai-agent.ts +154 -115
  265. package/src/ai-control-server.ts +8 -8
  266. package/src/ai-fx.ts +41 -9
  267. package/src/ai-harness.ts +28 -2
  268. package/src/ai-pi.ts +2 -2
  269. package/src/ai-server.ts +2 -2
  270. package/src/ai.ts +7 -7
  271. package/src/contract.ts +1 -1
  272. package/src/deterministic-id.ts +26 -3
  273. package/src/errors.ts +9 -6
  274. package/src/idempotent-replay.ts +0 -1
  275. package/src/index.ts +1 -1
  276. package/src/internal.ts +17 -6
  277. package/src/otel.ts +1 -1
  278. package/src/postgres-notifications.ts +0 -22
  279. package/src/postgres-resources.ts +6 -9
  280. package/src/postgres-schema.ts +121 -0
  281. package/src/push-envelope.ts +20 -0
  282. package/src/runtime-consumer.ts +10 -0
  283. package/src/scheduler-qstash.ts +12 -5
  284. package/src/scheduler-task.ts +2 -2
  285. package/src/scheduler-vercel.ts +10 -4
  286. package/src/server.browser.ts +1 -1
  287. package/src/server.ts +839 -238
  288. package/src/session-socket.ts +1 -1
  289. package/src/store-codec.ts +66 -0
  290. package/src/store-memory.ts +581 -255
  291. package/src/store-postgres.ts +1984 -943
  292. package/src/store-redis-core.ts +1624 -1152
  293. package/src/store-redis-notify.ts +2 -0
  294. package/src/store-sqlite.ts +891 -486
  295. package/src/store.ts +227 -97
  296. package/src/telemetry.ts +2 -1
  297. package/src/wire.ts +1 -0
  298. package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
  299. package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
  300. package/dist/ai-mMiWul1T.js.map +0 -1
  301. package/dist/contract-BYKASlCV.js.map +0 -1
  302. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  303. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  304. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  305. package/dist/internal-CscSY7K_.js.map +0 -1
  306. package/dist/server-D-Y4pqwu.js.map +0 -1
  307. package/dist/server-DYAATFuT.d.ts.map +0 -1
  308. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  309. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  310. package/dist/store-CyLUy86v.d.ts +0 -404
  311. package/dist/store-CyLUy86v.d.ts.map +0 -1
  312. package/dist/store-codec-DTG0Ftek.js +0 -10
  313. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  314. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  315. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  316. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  317. package/dist/wire-BKxH1vbP.js.map +0 -1
  318. package/docs/reference/05-adapters.mdx +0 -351
  319. package/docs/server/01-handlers.mdx +0 -200
  320. package/docs/server/05-production.mdx +0 -323
@@ -0,0 +1,206 @@
1
+ /**
2
+ * An example: A2's tables for experimental-a2/store-postgres as Drizzle
3
+ * definitions, the same tables schema.sql next to this file creates. It isn't
4
+ * an API, and nothing imports it. Use it however fits your setup, for example
5
+ * as a copy next to your own schema, so your migrations create A2's tables
6
+ * with yours. Adapt it to your Drizzle version, your ORM, and how you lay out
7
+ * your schema.
8
+ *
9
+ * Whatever creates them, A2 relies on the table and column names, the column
10
+ * types, the primary keys, and the unique index on a2_events.event_id. The
11
+ * other indexes keep its queries fast. Every name is spelled out, so Drizzle's
12
+ * `casing` setting changes none of them.
13
+ *
14
+ * A2's tables change only when a release's changelog says so; compare your
15
+ * copy with that release's file. Drizzle can't declare functions or
16
+ * triggers: add schema.sql's `a2_batch` function, which A2 runs its writes
17
+ * through, as a custom migration (`drizzle-kit generate --custom`), and the
18
+ * triggers `listenConnectionString` needs are in notifications.sql.
19
+ */
20
+ import { sql } from 'drizzle-orm'
21
+ import {
22
+ bigint,
23
+ boolean,
24
+ index,
25
+ integer,
26
+ jsonb,
27
+ pgTable,
28
+ primaryKey,
29
+ text,
30
+ uniqueIndex,
31
+ } from 'drizzle-orm/pg-core'
32
+
33
+ const int8 = (name: string) => bigint(name, { mode: 'number' })
34
+
35
+ export const a2Events = pgTable(
36
+ 'a2_events',
37
+ {
38
+ sessionId: text('session_id').notNull(),
39
+ idx: int8('idx').notNull(),
40
+ eventType: text('event_type').notNull(),
41
+ payload: jsonb('payload').notNull(),
42
+ eventId: text('event_id').notNull(),
43
+ createdAt: int8('created_at').notNull(),
44
+ cause: jsonb('cause'),
45
+ },
46
+ (table) => [
47
+ primaryKey({
48
+ name: 'a2_events_pkey',
49
+ columns: [table.sessionId, table.idx],
50
+ }),
51
+ uniqueIndex('a2_events_event_id').on(table.eventId),
52
+ index('a2_events_types').on(table.sessionId, table.eventType, table.idx),
53
+ ],
54
+ )
55
+
56
+ /**
57
+ * One row per run of a cursor: an event cursor's one event, a lane's run of
58
+ * queued events, or a log cursor's position.
59
+ */
60
+ export const a2Cursors = pgTable(
61
+ 'a2_cursors',
62
+ {
63
+ sessionId: text('session_id').notNull(),
64
+ consumer: text('consumer').notNull(),
65
+ cursorKey: text('cursor_key').notNull(),
66
+ epoch: int8('epoch').notNull(),
67
+ lane: text('lane'),
68
+ kind: text('kind').notNull(),
69
+ position: int8('position').notNull(),
70
+ head: int8('head'),
71
+ types: jsonb('types'),
72
+ attemptCount: integer('attempt_count').notNull().default(0),
73
+ firstClaimedAt: int8('first_claimed_at'),
74
+ lastClaimedAt: int8('last_claimed_at'),
75
+ claimHolder: text('claim_holder'),
76
+ claimExpiresAt: int8('claim_expires_at'),
77
+ failureCount: integer('failure_count').notNull().default(0),
78
+ lastFailedAt: int8('last_failed_at'),
79
+ lastFailedAttempt: integer('last_failed_attempt'),
80
+ lastError: text('last_error'),
81
+ failedAt: int8('failed_at'),
82
+ completedIdx: int8('completed_idx'),
83
+ completedAttempt: integer('completed_attempt'),
84
+ returnedEventIds: jsonb('returned_event_ids'),
85
+ },
86
+ (table) => [
87
+ primaryKey({
88
+ name: 'a2_cursors_pkey',
89
+ columns: [table.sessionId, table.cursorKey, table.consumer, table.epoch],
90
+ }),
91
+ index('a2_cursors_heads')
92
+ .on(table.sessionId, table.head)
93
+ .where(sql`head is not null`),
94
+ index('a2_cursors_lanes')
95
+ .on(table.sessionId)
96
+ .where(sql`kind = 'lane'`),
97
+ ],
98
+ )
99
+
100
+ /** A lane cursor's queue: the events appended for the lane, in log order. */
101
+ export const a2LaneEvents = pgTable(
102
+ 'a2_lane_events',
103
+ {
104
+ sessionId: text('session_id').notNull(),
105
+ consumer: text('consumer').notNull(),
106
+ lane: text('lane').notNull(),
107
+ idx: int8('idx').notNull(),
108
+ done: boolean('done').notNull().default(false),
109
+ completedAttempt: integer('completed_attempt'),
110
+ returnedEventIds: jsonb('returned_event_ids'),
111
+ },
112
+ (table) => [
113
+ primaryKey({
114
+ name: 'a2_lane_events_pkey',
115
+ columns: [table.sessionId, table.consumer, table.lane, table.idx],
116
+ }),
117
+ ],
118
+ )
119
+
120
+ export const a2Snapshots = pgTable(
121
+ 'a2_snapshots',
122
+ {
123
+ sessionId: text('session_id').notNull(),
124
+ reducerName: text('reducer_name').notNull(),
125
+ upToIndex: int8('up_to_index').notNull(),
126
+ state: jsonb('state').notNull(),
127
+ updatedAt: int8('updated_at').notNull(),
128
+ },
129
+ (table) => [
130
+ primaryKey({
131
+ name: 'a2_snapshots_pkey',
132
+ columns: [table.sessionId, table.reducerName],
133
+ }),
134
+ ],
135
+ )
136
+
137
+ export const a2SnapshotHistory = pgTable(
138
+ 'a2_snapshot_history',
139
+ {
140
+ sessionId: text('session_id').notNull(),
141
+ reducerName: text('reducer_name').notNull(),
142
+ upToIndex: int8('up_to_index').notNull(),
143
+ state: jsonb('state').notNull(),
144
+ updatedAt: int8('updated_at').notNull(),
145
+ },
146
+ (table) => [
147
+ primaryKey({
148
+ name: 'a2_snapshot_history_pkey',
149
+ columns: [table.sessionId, table.reducerName, table.upToIndex],
150
+ }),
151
+ ],
152
+ )
153
+
154
+ export const a2SnapshotPins = pgTable(
155
+ 'a2_snapshot_pins',
156
+ {
157
+ sessionId: text('session_id').notNull(),
158
+ eventIndex: int8('event_index').notNull(),
159
+ consumer: text('consumer').notNull(),
160
+ cursorKey: text('cursor_key').notNull(),
161
+ reducerName: text('reducer_name').notNull(),
162
+ upToIndex: int8('up_to_index').notNull(),
163
+ },
164
+ (table) => [
165
+ primaryKey({
166
+ name: 'a2_snapshot_pins_pkey',
167
+ columns: [
168
+ table.sessionId,
169
+ table.eventIndex,
170
+ table.consumer,
171
+ table.cursorKey,
172
+ table.reducerName,
173
+ table.upToIndex,
174
+ ],
175
+ }),
176
+ index('a2_snapshot_pins_checkpoint').on(
177
+ table.sessionId,
178
+ table.reducerName,
179
+ table.upToIndex,
180
+ ),
181
+ ],
182
+ )
183
+
184
+ export const a2Clears = pgTable('a2_clears', {
185
+ sessionId: text('session_id').primaryKey(),
186
+ clearIndex: int8('clear_index').notNull(),
187
+ })
188
+
189
+ export const a2Presence = pgTable(
190
+ 'a2_presence',
191
+ {
192
+ ns: text('ns').notNull(),
193
+ participant: text('participant').notNull(),
194
+ field: text('field').notNull(),
195
+ value: jsonb('value').notNull(),
196
+ seen: int8('seen').notNull(),
197
+ at: int8('at').notNull(),
198
+ expiresAt: int8('expires_at').notNull(),
199
+ },
200
+ (table) => [
201
+ primaryKey({
202
+ name: 'a2_presence_pkey',
203
+ columns: [table.ns, table.participant, table.field],
204
+ }),
205
+ ],
206
+ )
@@ -0,0 +1,24 @@
1
+ -- Notification triggers for experimental-a2/store-postgres, needed only with
2
+ -- `listenConnectionString`. Apply after schema.sql. They wake live streams
3
+ -- for every write to A2's event and presence tables, including writes from
4
+ -- stores without the option. Safe to re-run.
5
+
6
+ create or replace function a2_notify_events() returns trigger language plpgsql as $$
7
+ begin
8
+ perform pg_notify('a2_events_' || md5(new.session_id), '');
9
+ return null;
10
+ end $$;
11
+ create or replace function a2_notify_presence() returns trigger language plpgsql as $$
12
+ begin
13
+ perform pg_notify('a2_presence_' || md5(case when TG_OP = 'DELETE' then old.ns else new.ns end),
14
+ coalesce(current_setting('a2.presence_source', true), ''));
15
+ return null;
16
+ end $$;
17
+ do $$ begin
18
+ if not exists (select 1 from pg_trigger where tgname = 'a2_notify_events' and tgrelid = 'a2_events'::regclass) then
19
+ create trigger a2_notify_events after insert on a2_events for each row execute function a2_notify_events();
20
+ end if;
21
+ if not exists (select 1 from pg_trigger where tgname = 'a2_notify_presence' and tgrelid = 'a2_presence'::regclass) then
22
+ create trigger a2_notify_presence after insert or update or delete on a2_presence for each row execute function a2_notify_presence();
23
+ end if;
24
+ end $$;
@@ -0,0 +1,276 @@
1
+ -- A2's tables for experimental-a2/store-postgres. drizzle-schema.ts, next to
2
+ -- this file, shows the same tables as Drizzle definitions, as an example.
3
+ --
4
+ -- Apply this file before the first deploy, and again when a release's
5
+ -- changelog says the tables changed. Every statement creates only what is
6
+ -- missing and leaves existing data alone, so the file is safe to re-run on any
7
+ -- A2 database, whichever release created it. A2 checks nothing up front: a
8
+ -- query that finds a table or column missing throws STORE_NOT_CONFIGURED.
9
+ --
10
+ -- On a database from a release that ran handlers, it drops the work those
11
+ -- handlers hadn't finished, and the columns they dispatched by, so drain
12
+ -- earlier deployments and apply it while none is running.
13
+ --
14
+ -- Each index sits behind a catalog check: `create index if not exists` takes
15
+ -- a share lock on its table, and requires owning it, even when the index
16
+ -- exists. The check makes a re-run on a live database wait for no writer.
17
+ --
18
+ -- The file works as a migration as it ships: copy it into your migration
19
+ -- tool's folder. The breakpoint comment after each statement splits it for
20
+ -- tools that send one statement per query, like Drizzle on PGlite; to every
21
+ -- other tool, psql included, it's only a comment.
22
+
23
+ create table if not exists a2_events (
24
+ session_id text not null,
25
+ idx bigint not null,
26
+ event_type text not null,
27
+ payload jsonb not null,
28
+ event_id text not null,
29
+ created_at bigint not null,
30
+ cause jsonb,
31
+ primary key (session_id, idx)
32
+ );
33
+ --> statement-breakpoint
34
+
35
+ -- One row per run of a cursor: an event cursor's one event, a lane's run of
36
+ -- queued events, or a log cursor's position. A run is keyed by the head it
37
+ -- started at (`epoch`), so a clear starts new runs instead of rewriting old
38
+ -- ones.
39
+ create table if not exists a2_cursors (
40
+ session_id text not null,
41
+ consumer text not null,
42
+ cursor_key text not null,
43
+ epoch bigint not null,
44
+ lane text,
45
+ kind text not null,
46
+ position bigint not null,
47
+ head bigint,
48
+ types jsonb,
49
+ attempt_count integer not null default 0,
50
+ first_claimed_at bigint,
51
+ last_claimed_at bigint,
52
+ claim_holder text,
53
+ claim_expires_at bigint,
54
+ failure_count integer not null default 0,
55
+ last_failed_at bigint,
56
+ last_failed_attempt integer,
57
+ last_error text,
58
+ failed_at bigint,
59
+ completed_idx bigint,
60
+ completed_attempt integer,
61
+ returned_event_ids jsonb,
62
+ primary key (session_id, cursor_key, consumer, epoch)
63
+ );
64
+ --> statement-breakpoint
65
+
66
+ -- A lane cursor's queue: the events appended for the lane, in log order.
67
+ create table if not exists a2_lane_events (
68
+ session_id text not null,
69
+ consumer text not null,
70
+ lane text not null,
71
+ idx bigint not null,
72
+ done boolean not null default false,
73
+ completed_attempt integer,
74
+ returned_event_ids jsonb,
75
+ primary key (session_id, consumer, lane, idx)
76
+ );
77
+ --> statement-breakpoint
78
+
79
+ create table if not exists a2_snapshots (
80
+ session_id text not null,
81
+ reducer_name text not null,
82
+ up_to_index bigint not null,
83
+ state jsonb not null,
84
+ updated_at bigint not null,
85
+ primary key (session_id, reducer_name)
86
+ );
87
+ --> statement-breakpoint
88
+
89
+ create table if not exists a2_snapshot_history (
90
+ session_id text not null,
91
+ reducer_name text not null,
92
+ up_to_index bigint not null,
93
+ state jsonb not null,
94
+ updated_at bigint not null,
95
+ primary key (session_id, reducer_name, up_to_index)
96
+ );
97
+ --> statement-breakpoint
98
+
99
+ -- Earlier releases pinned snapshots per event; this one pins them per
100
+ -- cursor head. Pins only hold history for work in flight, so old ones go.
101
+ do $$ begin
102
+ if to_regclass('a2_snapshot_pins') is not null and not exists (
103
+ select 1 from pg_attribute
104
+ where attrelid = to_regclass('a2_snapshot_pins')
105
+ and attname = 'cursor_key' and not attisdropped
106
+ ) then
107
+ drop table a2_snapshot_pins;
108
+ end if;
109
+ end $$;
110
+ --> statement-breakpoint
111
+
112
+ create table if not exists a2_snapshot_pins (
113
+ session_id text not null,
114
+ event_index bigint not null,
115
+ consumer text not null,
116
+ cursor_key text not null,
117
+ reducer_name text not null,
118
+ up_to_index bigint not null,
119
+ primary key (session_id, event_index, consumer, cursor_key, reducer_name, up_to_index)
120
+ );
121
+ --> statement-breakpoint
122
+
123
+ create table if not exists a2_clears (
124
+ session_id text primary key,
125
+ clear_index bigint not null
126
+ );
127
+ --> statement-breakpoint
128
+
129
+ create table if not exists a2_presence (
130
+ ns text not null,
131
+ participant text not null,
132
+ field text not null,
133
+ value jsonb not null,
134
+ seen bigint not null,
135
+ at bigint not null,
136
+ expires_at bigint not null,
137
+ primary key (ns, participant, field)
138
+ );
139
+ --> statement-breakpoint
140
+
141
+ -- From an earlier release: work its handlers hadn't finished is dropped,
142
+ -- since this release runs no handlers to finish it. Every event is kept.
143
+ -- Causes gain the dispatcher that appended them, the handlers table
144
+ -- (`a2.handlers`), and its lane. Then the old dispatch columns go, and
145
+ -- their indexes with them. The block locks the events first, so an earlier
146
+ -- release still writing waits and then fails, rather than committing between
147
+ -- its statements, and it commits whole, or not at all.
148
+ do $$ begin
149
+ if exists (
150
+ select 1 from pg_attribute
151
+ where attrelid = to_regclass('a2_events')
152
+ and attname = 'processed_at' and not attisdropped
153
+ ) then
154
+ lock table a2_events in access exclusive mode;
155
+
156
+ update a2_events child
157
+ set cause = jsonb_build_object(
158
+ 'consumer', 'a2.handlers',
159
+ 'lane', (select parent.lane from a2_events parent
160
+ where parent.session_id = child.session_id
161
+ and parent.idx = (child.cause ->> 'index')::bigint)
162
+ ) || child.cause
163
+ where child.cause is not null and not child.cause ? 'consumer';
164
+
165
+ alter table a2_events
166
+ drop column lane,
167
+ drop column lane_ready,
168
+ drop column processed_at,
169
+ drop column processed_by_attempt,
170
+ drop column returned_event_ids,
171
+ drop column first_claimed_at,
172
+ drop column last_claimed_at,
173
+ drop column attempt_count,
174
+ drop column failure_count,
175
+ drop column last_failed_at,
176
+ drop column last_failed_attempt,
177
+ drop column last_error,
178
+ drop column failed_at,
179
+ drop column claim_holder,
180
+ drop column claim_expires_at;
181
+ end if;
182
+ end $$;
183
+ --> statement-breakpoint
184
+
185
+ -- Earlier releases recorded a schema version here and checked it before
186
+ -- their first query. This one checks nothing up front.
187
+ drop table if exists a2_schema;
188
+ --> statement-breakpoint
189
+
190
+ -- Runs an operation's statements in one round trip: A2 sends each write that
191
+ -- takes a session's lock as one call, the lock and then the statements that
192
+ -- need it, so it holds the lock only while the server runs them. Each
193
+ -- statement takes its snapshot when it starts, so those after the lock see
194
+ -- what its last holder committed; a single statement can't, since its
195
+ -- snapshot predates the lock. Each statement is a select of jsonb rows: a row
196
+ -- with an `abort` key fails the call and rolls it back, and one with a `stop`
197
+ -- key ends it. It needs read committed, the default: under a stricter level
198
+ -- every statement would share the first one's snapshot, so it refuses to run.
199
+ -- Created only if missing, as the indexes are: a later release that changes it
200
+ -- adds a function with a new name.
201
+ do $$ declare grantee text; begin
202
+ if to_regprocedure('a2_batch(text[], jsonb)') is null then
203
+ create function a2_batch(statements text[], params jsonb)
204
+ returns setof jsonb
205
+ language plpgsql
206
+ volatile
207
+ as $batch$
208
+ declare
209
+ statement text;
210
+ result jsonb;
211
+ begin
212
+ if current_setting('transaction_isolation') <> 'read committed' then
213
+ raise exception using errcode = 'A2002',
214
+ message = 'a2_batch needs the read committed isolation level';
215
+ end if;
216
+ foreach statement in array statements loop
217
+ for result in execute statement using params loop
218
+ if result ? 'abort' then
219
+ raise exception using errcode = 'A2001',
220
+ message = 'a2_batch aborted', detail = result::text;
221
+ end if;
222
+ return next result;
223
+ if result ? 'stop' then
224
+ return;
225
+ end if;
226
+ end loop;
227
+ end loop;
228
+ end
229
+ $batch$;
230
+ -- Default privileges can grant a new function to roles besides PUBLIC, as
231
+ -- platforms that serve functions over an API grant theirs: it starts with
232
+ -- none but its owner's. Grants made after it exist are left alone.
233
+ for grantee in
234
+ select acl.grantee::regrole::text
235
+ from pg_proc proc, aclexplode(proc.proacl) acl
236
+ where proc.oid = to_regprocedure('a2_batch(text[], jsonb)')
237
+ and acl.grantee not in (0, proc.proowner)
238
+ loop
239
+ execute format('revoke execute on function a2_batch(text[], jsonb) from %s', grantee);
240
+ end loop;
241
+ end if;
242
+ -- It runs any statement it's handed, as its caller: only its owner, the role
243
+ -- that applies this file, may call it, and not every role, as Postgres lets
244
+ -- by default. Platforms that serve functions over an API would serve it too.
245
+ if pg_has_role(
246
+ current_user,
247
+ (select proowner from pg_proc
248
+ where oid = to_regprocedure('a2_batch(text[], jsonb)')),
249
+ 'usage'
250
+ ) then
251
+ revoke execute on function a2_batch(text[], jsonb) from public;
252
+ end if;
253
+ end $$;
254
+ --> statement-breakpoint
255
+
256
+ do $$ begin
257
+ if to_regclass('a2_events_event_id') is null then
258
+ create unique index if not exists a2_events_event_id on a2_events (event_id);
259
+ end if;
260
+ if to_regclass('a2_events_types') is null then
261
+ create index if not exists a2_events_types
262
+ on a2_events (session_id, event_type, idx);
263
+ end if;
264
+ if to_regclass('a2_cursors_heads') is null then
265
+ create index if not exists a2_cursors_heads
266
+ on a2_cursors (session_id, head) where head is not null;
267
+ end if;
268
+ if to_regclass('a2_cursors_lanes') is null then
269
+ create index if not exists a2_cursors_lanes
270
+ on a2_cursors (session_id) where kind = 'lane';
271
+ end if;
272
+ if to_regclass('a2_snapshot_pins_checkpoint') is null then
273
+ create index if not exists a2_snapshot_pins_checkpoint
274
+ on a2_snapshot_pins (session_id, reducer_name, up_to_index);
275
+ end if;
276
+ end $$;