experimental-a2 0.20.0 → 0.22.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 (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  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-DFET5q16.js.map +1 -0
  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-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.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 +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  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 +3 -3
  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 +86 -73
  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-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  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-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.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-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
@@ -0,0 +1,73 @@
1
+ /**
2
+ * a2slack's database: Postgres at `DATABASE_URL`, or PGlite in development
3
+ * (in memory under test).
4
+ *
5
+ * Production never changes the schema at runtime: apply ./migrations with
6
+ * `pnpm db:migrate` before deploying. PGlite applies them when it opens, since
7
+ * a development database is only ever this app's.
8
+ */
9
+ import 'server-only'
10
+ import { join } from 'node:path'
11
+ import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core'
12
+ import * as schema from './schema'
13
+
14
+ export type Db = PgDatabase<PgQueryResultHKT, typeof schema>
15
+
16
+ const url =
17
+ process.env.DATABASE_URL?.replace(
18
+ /sslmode=(require|prefer|verify-ca)/,
19
+ 'sslmode=verify-full',
20
+ ) || undefined
21
+
22
+ const migrations = {
23
+ migrationsFolder: join(process.cwd(), 'app/a2slack/db/migrations'),
24
+ migrationsTable: 'a2slack_migrations',
25
+ migrationsSchema: 'public',
26
+ }
27
+
28
+ /** A PGlite database at `dataDir`, migrated. */
29
+ export async function openPglite(dataDir: string): Promise<Db> {
30
+ const [{ PGlite }, { pg_trgm }, { drizzle }, { migrate }] = await Promise.all(
31
+ [
32
+ import('@electric-sql/pglite'),
33
+ import('@electric-sql/pglite/contrib/pg_trgm'),
34
+ import('drizzle-orm/pglite'),
35
+ import('drizzle-orm/pglite/migrator'),
36
+ ],
37
+ )
38
+ const client = new PGlite(dataDir, { extensions: { pg_trgm } })
39
+ const db = drizzle({ client, schema, casing: 'snake_case' })
40
+ await migrate(db, migrations)
41
+ return db
42
+ }
43
+
44
+ async function open(): Promise<Db> {
45
+ if (url !== undefined) {
46
+ const { drizzle } = await import('drizzle-orm/node-postgres')
47
+ const db = drizzle({ connection: url, schema, casing: 'snake_case' })
48
+ // The server closing a connection (a restart, Neon scaling to zero) is
49
+ // reported on the pool when the connection is idle, and on the connection
50
+ // when a transaction holds it. Without a listener either one is an
51
+ // uncaught exception, which ends the process. The query it cut off fails
52
+ // either way, and its consumer retries.
53
+ db.$client.on('error', () => {})
54
+ db.$client.on('connect', (connection) => connection.on('error', () => {}))
55
+ return db
56
+ }
57
+ // A serverless filesystem keeps nothing, so a deployment needs Postgres.
58
+ if (process.env.VERCEL) throw new Error('a2slack needs DATABASE_URL')
59
+ return openPglite(
60
+ process.env.NODE_ENV === 'test' ? 'memory://' : '.a2/a2slack',
61
+ )
62
+ }
63
+
64
+ const shared = globalThis as { a2slackDb?: Promise<Db> }
65
+
66
+ /** The database, opened once per process. */
67
+ export function database(): Promise<Db> {
68
+ shared.a2slackDb ??= open()
69
+ shared.a2slackDb.catch(() => {
70
+ delete shared.a2slackDb
71
+ })
72
+ return shared.a2slackDb
73
+ }
@@ -0,0 +1,4 @@
1
+ -- Message search falls back to trigram matching when there's no Upstash Redis
2
+ -- Search (a2slack_messages_text_trgm, in 0001). drizzle-kit can't declare an
3
+ -- extension, so this migration is written by hand.
4
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
@@ -0,0 +1,74 @@
1
+ CREATE TABLE "a2slack_channels" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "created_by_name" text NOT NULL,
4
+ "created_at" timestamp with time zone NOT NULL,
5
+ "log_index" bigint NOT NULL
6
+ );
7
+ --> statement-breakpoint
8
+ CREATE TABLE "a2slack_messages" (
9
+ "id" text PRIMARY KEY NOT NULL,
10
+ "channel" text NOT NULL,
11
+ "log_index" bigint NOT NULL,
12
+ "thread_id" text,
13
+ "author_id" text NOT NULL,
14
+ "author_name" text NOT NULL,
15
+ "text" text NOT NULL,
16
+ "posted_at" timestamp with time zone NOT NULL,
17
+ "edited_at" timestamp with time zone,
18
+ "deleted" boolean DEFAULT false NOT NULL,
19
+ "label" text
20
+ );
21
+ --> statement-breakpoint
22
+ CREATE TABLE "a2slack_notifications" (
23
+ "id" text PRIMARY KEY NOT NULL,
24
+ "user_id" text NOT NULL,
25
+ "thread_id" text NOT NULL,
26
+ "channel" text NOT NULL,
27
+ "reason" text NOT NULL,
28
+ "message_id" text NOT NULL,
29
+ "from_id" text NOT NULL,
30
+ "from_name" text NOT NULL,
31
+ "text" text NOT NULL,
32
+ "log_index" bigint NOT NULL,
33
+ "sent_at" timestamp with time zone NOT NULL
34
+ );
35
+ --> statement-breakpoint
36
+ CREATE TABLE "a2slack_people" (
37
+ "id" text PRIMARY KEY NOT NULL,
38
+ "name" text NOT NULL,
39
+ "log_index" bigint NOT NULL
40
+ );
41
+ --> statement-breakpoint
42
+ CREATE TABLE "a2slack_positions" (
43
+ "consumer" text PRIMARY KEY NOT NULL,
44
+ "log_index" bigint NOT NULL
45
+ );
46
+ --> statement-breakpoint
47
+ CREATE TABLE "a2slack_reactions" (
48
+ "message_id" text NOT NULL,
49
+ "emoji" text NOT NULL,
50
+ "author_id" text NOT NULL,
51
+ "author_name" text NOT NULL,
52
+ "log_index" bigint NOT NULL,
53
+ CONSTRAINT "a2slack_reactions_message_id_emoji_author_id_pk" PRIMARY KEY("message_id","emoji","author_id")
54
+ );
55
+ --> statement-breakpoint
56
+ CREATE TABLE "a2slack_reads" (
57
+ "user_id" text NOT NULL,
58
+ "thread_id" text NOT NULL,
59
+ "through" bigint NOT NULL,
60
+ CONSTRAINT "a2slack_reads_user_id_thread_id_pk" PRIMARY KEY("user_id","thread_id")
61
+ );
62
+ --> statement-breakpoint
63
+ CREATE TABLE "a2slack_thread_members" (
64
+ "thread_id" text NOT NULL,
65
+ "person_id" text NOT NULL,
66
+ "log_index" bigint NOT NULL,
67
+ CONSTRAINT "a2slack_thread_members_thread_id_person_id_pk" PRIMARY KEY("thread_id","person_id")
68
+ );
69
+ --> statement-breakpoint
70
+ CREATE INDEX "a2slack_messages_feed" ON "a2slack_messages" USING btree ("channel","thread_id","log_index");--> statement-breakpoint
71
+ CREATE INDEX "a2slack_messages_thread" ON "a2slack_messages" USING btree ("thread_id","log_index");--> statement-breakpoint
72
+ CREATE INDEX "a2slack_messages_text_trgm" ON "a2slack_messages" USING gin ("text" gin_trgm_ops);--> statement-breakpoint
73
+ CREATE INDEX "a2slack_notifications_inbox" ON "a2slack_notifications" USING btree ("user_id","thread_id","log_index");--> statement-breakpoint
74
+ CREATE INDEX "a2slack_people_name" ON "a2slack_people" USING btree ("name");
@@ -0,0 +1,215 @@
1
+ -- A2's tables: experimental-a2's sql/postgres/schema.sql, in place of the
2
+ -- CREATE TABLEs drizzle-kit generated from db/a2.ts, so this migration also
3
+ -- upgrades a database that already has A2's tables (from an earlier release
4
+ -- or a schema.sql applied by hand), keeping their events.
5
+ --
6
+ -- A2's tables for experimental-a2/store-postgres. drizzle-schema.ts, next to
7
+ -- this file, shows the same tables as Drizzle definitions, as an example.
8
+ --
9
+ -- Apply this file before the first deploy, and again when a release's
10
+ -- changelog says the tables changed. Every statement creates only what is
11
+ -- missing and leaves existing data alone, so the file is safe to re-run on any
12
+ -- A2 database, whichever release created it. A2 checks nothing up front: a
13
+ -- query that finds a table or column missing throws STORE_NOT_CONFIGURED.
14
+ --
15
+ -- On a database from a release that ran handlers, it drops the work those
16
+ -- handlers hadn't finished, and the columns they dispatched by, so drain
17
+ -- earlier deployments and apply it while none is running.
18
+ --
19
+ -- Each index sits behind a catalog check: `create index if not exists` takes
20
+ -- a share lock on its table, and requires owning it, even when the index
21
+ -- exists. The check makes a re-run on a live database wait for no writer.
22
+ --
23
+ -- The file works as a migration as it ships: copy it into your migration
24
+ -- tool's folder. The breakpoint comment after each statement splits it for
25
+ -- tools that send one statement per query, like Drizzle on PGlite; to every
26
+ -- other tool, psql included, it's only a comment.
27
+
28
+ create table if not exists a2_events (
29
+ session_id text not null,
30
+ idx bigint not null,
31
+ event_type text not null,
32
+ payload jsonb not null,
33
+ event_id text not null,
34
+ created_at bigint not null,
35
+ cause jsonb,
36
+ primary key (session_id, idx)
37
+ );
38
+ --> statement-breakpoint
39
+
40
+ -- One row per run of a cursor: an event cursor's one event, a lane's run of
41
+ -- queued events, or a log cursor's position. A run is keyed by the head it
42
+ -- started at (`epoch`), so a clear starts new runs instead of rewriting old
43
+ -- ones.
44
+ create table if not exists a2_cursors (
45
+ session_id text not null,
46
+ consumer text not null,
47
+ cursor_key text not null,
48
+ epoch bigint not null,
49
+ lane text,
50
+ kind text not null,
51
+ position bigint not null,
52
+ head bigint,
53
+ types jsonb,
54
+ attempt_count integer not null default 0,
55
+ first_claimed_at bigint,
56
+ last_claimed_at bigint,
57
+ claim_holder text,
58
+ claim_expires_at bigint,
59
+ failure_count integer not null default 0,
60
+ last_failed_at bigint,
61
+ last_failed_attempt integer,
62
+ last_error text,
63
+ failed_at bigint,
64
+ completed_idx bigint,
65
+ completed_attempt integer,
66
+ returned_event_ids jsonb,
67
+ primary key (session_id, cursor_key, consumer, epoch)
68
+ );
69
+ --> statement-breakpoint
70
+
71
+ -- A lane cursor's queue: the events appended for the lane, in log order.
72
+ create table if not exists a2_lane_events (
73
+ session_id text not null,
74
+ consumer text not null,
75
+ lane text not null,
76
+ idx bigint not null,
77
+ done boolean not null default false,
78
+ completed_attempt integer,
79
+ returned_event_ids jsonb,
80
+ primary key (session_id, consumer, lane, idx)
81
+ );
82
+ --> statement-breakpoint
83
+
84
+ create table if not exists a2_snapshots (
85
+ session_id text not null,
86
+ reducer_name text not null,
87
+ up_to_index bigint not null,
88
+ state jsonb not null,
89
+ updated_at bigint not null,
90
+ primary key (session_id, reducer_name)
91
+ );
92
+ --> statement-breakpoint
93
+
94
+ create table if not exists a2_snapshot_history (
95
+ session_id text not null,
96
+ reducer_name text not null,
97
+ up_to_index bigint not null,
98
+ state jsonb not null,
99
+ updated_at bigint not null,
100
+ primary key (session_id, reducer_name, up_to_index)
101
+ );
102
+ --> statement-breakpoint
103
+
104
+ -- Earlier releases pinned snapshots per event; this one pins them per
105
+ -- cursor head. Pins only hold history for work in flight, so old ones go.
106
+ do $$ begin
107
+ if to_regclass('a2_snapshot_pins') is not null and not exists (
108
+ select 1 from pg_attribute
109
+ where attrelid = to_regclass('a2_snapshot_pins')
110
+ and attname = 'cursor_key' and not attisdropped
111
+ ) then
112
+ drop table a2_snapshot_pins;
113
+ end if;
114
+ end $$;
115
+ --> statement-breakpoint
116
+
117
+ create table if not exists a2_snapshot_pins (
118
+ session_id text not null,
119
+ event_index bigint not null,
120
+ consumer text not null,
121
+ cursor_key text not null,
122
+ reducer_name text not null,
123
+ up_to_index bigint not null,
124
+ primary key (session_id, event_index, consumer, cursor_key, reducer_name, up_to_index)
125
+ );
126
+ --> statement-breakpoint
127
+
128
+ create table if not exists a2_clears (
129
+ session_id text primary key,
130
+ clear_index bigint not null
131
+ );
132
+ --> statement-breakpoint
133
+
134
+ create table if not exists a2_presence (
135
+ ns text not null,
136
+ participant text not null,
137
+ field text not null,
138
+ value jsonb not null,
139
+ seen bigint not null,
140
+ at bigint not null,
141
+ expires_at bigint not null,
142
+ primary key (ns, participant, field)
143
+ );
144
+ --> statement-breakpoint
145
+
146
+ -- From an earlier release: work its handlers hadn't finished is dropped,
147
+ -- since this release runs no handlers to finish it. Every event is kept.
148
+ -- Causes gain the dispatcher that appended them, the handlers table
149
+ -- (`a2.handlers`), and its lane. Then the old dispatch columns go, and
150
+ -- their indexes with them. The block locks the events first, so an earlier
151
+ -- release still writing waits and then fails, rather than committing between
152
+ -- its statements, and it commits whole, or not at all.
153
+ do $$ begin
154
+ if exists (
155
+ select 1 from pg_attribute
156
+ where attrelid = to_regclass('a2_events')
157
+ and attname = 'processed_at' and not attisdropped
158
+ ) then
159
+ lock table a2_events in access exclusive mode;
160
+
161
+ update a2_events child
162
+ set cause = jsonb_build_object(
163
+ 'consumer', 'a2.handlers',
164
+ 'lane', (select parent.lane from a2_events parent
165
+ where parent.session_id = child.session_id
166
+ and parent.idx = (child.cause ->> 'index')::bigint)
167
+ ) || child.cause
168
+ where child.cause is not null and not child.cause ? 'consumer';
169
+
170
+ alter table a2_events
171
+ drop column lane,
172
+ drop column lane_ready,
173
+ drop column processed_at,
174
+ drop column processed_by_attempt,
175
+ drop column returned_event_ids,
176
+ drop column first_claimed_at,
177
+ drop column last_claimed_at,
178
+ drop column attempt_count,
179
+ drop column failure_count,
180
+ drop column last_failed_at,
181
+ drop column last_failed_attempt,
182
+ drop column last_error,
183
+ drop column failed_at,
184
+ drop column claim_holder,
185
+ drop column claim_expires_at;
186
+ end if;
187
+ end $$;
188
+ --> statement-breakpoint
189
+
190
+ -- Earlier releases recorded a schema version here and checked it before
191
+ -- their first query. This one checks nothing up front.
192
+ drop table if exists a2_schema;
193
+ --> statement-breakpoint
194
+
195
+ do $$ begin
196
+ if to_regclass('a2_events_event_id') is null then
197
+ create unique index if not exists a2_events_event_id on a2_events (event_id);
198
+ end if;
199
+ if to_regclass('a2_events_types') is null then
200
+ create index if not exists a2_events_types
201
+ on a2_events (session_id, event_type, idx);
202
+ end if;
203
+ if to_regclass('a2_cursors_heads') is null then
204
+ create index if not exists a2_cursors_heads
205
+ on a2_cursors (session_id, head) where head is not null;
206
+ end if;
207
+ if to_regclass('a2_cursors_lanes') is null then
208
+ create index if not exists a2_cursors_lanes
209
+ on a2_cursors (session_id) where kind = 'lane';
210
+ end if;
211
+ if to_regclass('a2_snapshot_pins_checkpoint') is null then
212
+ create index if not exists a2_snapshot_pins_checkpoint
213
+ on a2_snapshot_pins (session_id, reducer_name, up_to_index);
214
+ end if;
215
+ end $$;
@@ -0,0 +1,67 @@
1
+ -- The a2_batch function from experimental-a2's sql/postgres/schema.sql, which
2
+ -- Drizzle can't declare: A2 runs its writes through it.
3
+
4
+ -- Runs an operation's statements in one round trip: A2 sends each write that
5
+ -- takes a session's lock as one call, the lock and then the statements that
6
+ -- need it, so it holds the lock only while the server runs them. Each
7
+ -- statement takes its snapshot when it starts, so those after the lock see
8
+ -- what its last holder committed; a single statement can't, since its
9
+ -- snapshot predates the lock. Each statement is a select of jsonb rows: a row
10
+ -- with an `abort` key fails the call and rolls it back, and one with a `stop`
11
+ -- key ends it. It needs read committed, the default: under a stricter level
12
+ -- every statement would share the first one's snapshot, so it refuses to run.
13
+ -- Created only if missing, as the indexes are: a later release that changes it
14
+ -- adds a function with a new name.
15
+ do $$ declare grantee text; begin
16
+ if to_regprocedure('a2_batch(text[], jsonb)') is null then
17
+ create function a2_batch(statements text[], params jsonb)
18
+ returns setof jsonb
19
+ language plpgsql
20
+ volatile
21
+ as $batch$
22
+ declare
23
+ statement text;
24
+ result jsonb;
25
+ begin
26
+ if current_setting('transaction_isolation') <> 'read committed' then
27
+ raise exception using errcode = 'A2002',
28
+ message = 'a2_batch needs the read committed isolation level';
29
+ end if;
30
+ foreach statement in array statements loop
31
+ for result in execute statement using params loop
32
+ if result ? 'abort' then
33
+ raise exception using errcode = 'A2001',
34
+ message = 'a2_batch aborted', detail = result::text;
35
+ end if;
36
+ return next result;
37
+ if result ? 'stop' then
38
+ return;
39
+ end if;
40
+ end loop;
41
+ end loop;
42
+ end
43
+ $batch$;
44
+ -- Default privileges can grant a new function to roles besides PUBLIC, as
45
+ -- platforms that serve functions over an API grant theirs: it starts with
46
+ -- none but its owner's. Grants made after it exist are left alone.
47
+ for grantee in
48
+ select acl.grantee::regrole::text
49
+ from pg_proc proc, aclexplode(proc.proacl) acl
50
+ where proc.oid = to_regprocedure('a2_batch(text[], jsonb)')
51
+ and acl.grantee not in (0, proc.proowner)
52
+ loop
53
+ execute format('revoke execute on function a2_batch(text[], jsonb) from %s', grantee);
54
+ end loop;
55
+ end if;
56
+ -- It runs any statement it's handed, as its caller: only its owner, the role
57
+ -- that applies this file, may call it, and not every role, as Postgres lets
58
+ -- by default. Platforms that serve functions over an API would serve it too.
59
+ if pg_has_role(
60
+ current_user,
61
+ (select proowner from pg_proc
62
+ where oid = to_regprocedure('a2_batch(text[], jsonb)')),
63
+ 'usage'
64
+ ) then
65
+ revoke execute on function a2_batch(text[], jsonb) from public;
66
+ end if;
67
+ end $$;
@@ -0,0 +1,2 @@
1
+ CREATE UNIQUE INDEX "a2_events_session_event_id" ON "a2_events" USING btree ("session_id","event_id");--> statement-breakpoint
2
+ DROP INDEX "a2_events_event_id";
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "3375264b-f603-4311-bef0-6f2a628dbdce",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {},
7
+ "enums": {},
8
+ "schemas": {},
9
+ "views": {},
10
+ "sequences": {},
11
+ "roles": {},
12
+ "policies": {},
13
+ "_meta": {
14
+ "columns": {},
15
+ "schemas": {},
16
+ "tables": {}
17
+ }
18
+ }