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.
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
- package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/{actor-shared-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-CvEnqVfo.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
- package/dist/ai-CzE672Q3.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +36 -9
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
- package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +4 -4
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +84 -75
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
- package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
- package/dist/internal-Bc4PvfHk.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-BWmht60z.js +12 -0
- package/dist/runtime-consumer-BWmht60z.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +7 -7
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +5 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
- package/dist/server-BshFZ0eL.d.ts.map +1 -0
- package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
- package/dist/server-DvIB8kj5.js.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
- package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-Z2qjw50n.d.ts +523 -0
- package/dist/store-Z2qjw50n.d.ts.map +1 -0
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +393 -210
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1595 -673
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
- package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
- package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +517 -287
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
- package/dist/wire-CZGqNZRx.js.map +1 -0
- package/docs/01-quickstart.mdx +28 -19
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +9 -1
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +13 -12
- package/docs/agents/03-conversations.mdx +31 -21
- package/docs/agents/04-harnesses.mdx +7 -3
- package/docs/agents/meta.ts +1 -1
- package/docs/client/03-presence.mdx +18 -14
- package/docs/client/meta.ts +1 -1
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +31 -26
- package/docs/reference/01-core.mdx +12 -12
- package/docs/reference/02-server.mdx +247 -192
- package/docs/reference/04-ai.mdx +29 -20
- package/docs/reference/05-telemetry.mdx +146 -0
- package/docs/reference/06-errors.mdx +24 -18
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-consumers.mdx +415 -0
- package/docs/server/02-timers.mdx +33 -23
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +60 -50
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +57 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +172 -0
- package/docs/stores/05-sqlite.mdx +36 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- package/docs/upgrades/01-overview.mdx +30 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +48 -11
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +206 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +13 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +7 -2
- package/sql/postgres/drizzle-schema.ts +206 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +276 -0
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +154 -115
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +41 -9
- package/src/ai-harness.ts +28 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +26 -3
- package/src/errors.ts +9 -6
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +17 -6
- package/src/otel.ts +1 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +121 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.browser.ts +1 -1
- package/src/server.ts +839 -238
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +581 -255
- package/src/store-postgres.ts +1984 -943
- package/src/store-redis-core.ts +1624 -1152
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +891 -486
- package/src/store.ts +227 -97
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-D-Y4pqwu.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/reference/05-adapters.mdx +0 -351
- package/docs/server/01-handlers.mdx +0 -200
- package/docs/server/05-production.mdx +0 -323
|
@@ -1,45 +1,49 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Guarantees
|
|
3
|
-
description: Appends are effectively-once,
|
|
3
|
+
description: Appends are effectively-once, consumers are at-least-once, and retries keep event identity across crashes.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## In short
|
|
7
7
|
|
|
8
8
|
- **Appends are effectively-once.** Event ids deduplicate writes. A batch commits
|
|
9
|
-
before its
|
|
10
|
-
- **
|
|
11
|
-
[side effects use idempotency keys](/server/
|
|
9
|
+
before its consumers start; they may start before `append` resolves.
|
|
10
|
+
- **Consumers are at-least-once.** They may run again after a crash, so
|
|
11
|
+
[side effects use idempotency keys](/server/consumers#consumers-can-run-twice).
|
|
12
12
|
- **The log stays ordered.** Every session has one immutable append order.
|
|
13
|
-
- **
|
|
14
|
-
Only events in the same lane run one at a time in log order.
|
|
13
|
+
- **Consumers are concurrent by default.** Log order does not serialize them.
|
|
14
|
+
Only events in the same lane of one consumer run one at a time in log order.
|
|
15
|
+
- **Every consumer keeps its own place.** One consumer's failures, retries, and
|
|
16
|
+
dead letters never hold up another.
|
|
15
17
|
|
|
16
18
|
## One event, many attempts
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
A consumer keeps a durable cursor in every session: where it has gotten to,
|
|
21
|
+
and the event it runs next, its head. An event is pending for a consumer until
|
|
22
|
+
that consumer's cursor moves past it. Every wakeup (an append, a scheduler
|
|
23
|
+
watchdog, or `server.drain()`) claims the session's eligible heads, and the
|
|
24
|
+
cursors decide what remains. Reads never dispatch consumers.
|
|
21
25
|
|
|
22
26
|
Each claim expires. If its holder dies, the claim lapses and the next wakeup
|
|
23
|
-
claims the
|
|
27
|
+
claims the head again with a larger `ctx.attempt`. The retry gets the same
|
|
24
28
|
event id, index, type, and payload. A kill after the claim can use up an
|
|
25
|
-
attempt before your code runs, and that attempt counts toward the
|
|
26
|
-
[failure budget](#when-a-
|
|
29
|
+
attempt before your code runs, and that attempt counts toward the head's
|
|
30
|
+
[failure budget](#when-a-consumer-keeps-failing). An older attempt can never
|
|
27
31
|
overwrite a newer one's completion or failure.
|
|
28
32
|
|
|
29
33
|
:::note[Recovery is not an event]
|
|
30
34
|
A2 does not append `recovered` or `continued`. Recovery retries the same event.
|
|
31
|
-
Your log contains only application facts.
|
|
35
|
+
Your log contains only application facts; cursors live beside it.
|
|
32
36
|
:::
|
|
33
37
|
|
|
34
38
|
## Immediate and dependent events
|
|
35
39
|
|
|
36
|
-
[`ctx.session.append(name, ...events)`](/server/
|
|
40
|
+
[`ctx.session.append(name, ...events)`](/server/consumers#append-what-should-happen-now)
|
|
37
41
|
commits immediately: its events stay in the log if the parent later crashes,
|
|
38
42
|
and run concurrently with it unless both share a lane.
|
|
39
|
-
[Returned events](/server/
|
|
40
|
-
with the
|
|
43
|
+
[Returned events](/server/consumers#return-what-happens-after-success) commit
|
|
44
|
+
with the cursor's advance past the parent, in one transaction.
|
|
41
45
|
|
|
42
|
-
If
|
|
46
|
+
If `run` throws or dies before that transaction, neither the completion
|
|
43
47
|
nor the returned events exist, and the retry produces the same deterministic
|
|
44
48
|
ids. If the transaction commits but its acknowledgement is lost, the retry gets
|
|
45
49
|
the committed rows back. A partial or changed batch is rejected.
|
|
@@ -51,11 +55,11 @@ the external system.
|
|
|
51
55
|
|
|
52
56
|
## Lanes
|
|
53
57
|
|
|
54
|
-
[Lanes](/server/
|
|
55
|
-
lowest-index unfinished event in a lane is eligible, so a
|
|
56
|
-
events in that lane, not the whole session
|
|
57
|
-
|
|
58
|
-
affects only new events.
|
|
58
|
+
[Lanes](/server/consumers#lanes) are serial groups of one consumer in one
|
|
59
|
+
session. Only the lowest-index unfinished event in a lane is eligible, so a
|
|
60
|
+
failure blocks later events in that lane, not the whole session and not other
|
|
61
|
+
consumers. Unlaned events and different lanes run concurrently. A2 stores each
|
|
62
|
+
consumer's lane at append, so a changed lane resolver affects only new events.
|
|
59
63
|
|
|
60
64
|
A lane controls A2 dispatch, not the outside world. A2 claims every eligible
|
|
61
65
|
event, with no global concurrency limit or priority queue. Put capacity limits
|
|
@@ -64,52 +68,57 @@ runtime.
|
|
|
64
68
|
|
|
65
69
|
## Append and scheduling
|
|
66
70
|
|
|
67
|
-
A top-level append commits before any wakeup. A2 then starts the
|
|
68
|
-
inline and, with a scheduler, arms a watchdog in parallel.
|
|
69
|
-
for the queue, and
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
A top-level append commits before any wakeup. A2 then starts the consumers
|
|
72
|
+
inline and, with a scheduler, arms a watchdog in parallel. Consumers never wait
|
|
73
|
+
for the queue, and one may start before `append` returns. `append` waits up to
|
|
74
|
+
two seconds for the watchdog send, never for consumers. A failed arm records
|
|
75
|
+
`a2.append.armed = false` on the trace. Consumer appends and returned events
|
|
76
|
+
join the running drain and add no arm.
|
|
73
77
|
|
|
74
|
-
[`append.dispatch`](/
|
|
78
|
+
[`append.dispatch`](/schedulers/overview#dispatch-in-a-fresh-invocation) skips
|
|
75
79
|
the inline drain and hands the work to the scheduler instead.
|
|
76
80
|
|
|
77
|
-
An event
|
|
78
|
-
|
|
81
|
+
An event no consumer takes starts no work. It starts no drain or arm unless
|
|
82
|
+
older work in the session is still pending. A dead letter is not pending: it
|
|
83
|
+
waits for a redrive, which wakes the session itself.
|
|
79
84
|
|
|
80
85
|
## Causal trees are durable
|
|
81
86
|
|
|
82
|
-
Every event a
|
|
83
|
-
event's index and
|
|
84
|
-
of one
|
|
85
|
-
keeps its original cause.
|
|
87
|
+
Every event a consumer appends or returns stores a `cause`: the consumer, its
|
|
88
|
+
lane, the triggering event's index, and the attempt. Top-level appends store
|
|
89
|
+
`null`. Children of one run are siblings, nested runs add levels, and a
|
|
90
|
+
retried child keeps its original cause.
|
|
86
91
|
|
|
87
92
|
## A chain that crosses the function timeout
|
|
88
93
|
|
|
89
|
-
A function killed mid-
|
|
90
|
-
fresh invocation: same event, new attempt. Events already
|
|
91
|
-
|
|
94
|
+
A function killed mid-run restarts that consumer's `run` from its entry point
|
|
95
|
+
in a fresh invocation: same event, new attempt. Events a cursor already moved
|
|
96
|
+
past stay done, so a chain of events can outlast one invocation, but each run
|
|
92
97
|
must fit in a fresh one: an attempt that runs out of time is a failed attempt,
|
|
93
|
-
and a
|
|
98
|
+
and a run that never fits dead-letters after ten. Split longer work into
|
|
94
99
|
smaller events or increase the function duration. On Vercel, A2 caps the claim window
|
|
95
100
|
at the function deadline when the platform exposes it, so a retry starts
|
|
96
101
|
promptly after a timeout. Once the deadline passes, A2 claims nothing more in
|
|
97
102
|
that invocation, so a timeout costs the event one attempt.
|
|
98
103
|
|
|
99
|
-
## When a
|
|
104
|
+
## When a consumer keeps failing
|
|
100
105
|
|
|
101
|
-
A caught failure counts against that
|
|
102
|
-
other eligible
|
|
106
|
+
A caught failure counts against that consumer's head, releases its claim, and
|
|
107
|
+
leaves other eligible heads running. A2 holds it back for a delay that starts at
|
|
103
108
|
one second and doubles with each failed attempt up to 30 seconds, then retries
|
|
104
109
|
it on the session's next wake. With a scheduler, that's a few seconds at first,
|
|
105
110
|
then the delay itself. An attempt that ends before it finishes, because its
|
|
106
111
|
process timed out, crashed, or lost its claim, counts too, once the next
|
|
107
|
-
attempt takes its claim over. Ten failed attempts dead-letter the event
|
|
108
|
-
events in its lane stay blocked, while unlaned events
|
|
112
|
+
attempt takes its claim over. Ten failed attempts dead-letter the event for
|
|
113
|
+
that consumer: later events in its lane stay blocked, while unlaned events,
|
|
114
|
+
other lanes, and other consumers continue.
|
|
109
115
|
A crash costs every event it interrupts one attempt, so a session whose work
|
|
110
116
|
keeps outrunning its function dead-letters: bound its fan-out, or put work that
|
|
111
|
-
waits on the same resource in one lane.
|
|
112
|
-
|
|
117
|
+
waits on the same resource in one lane. A `NonRetriableError`, or a returned
|
|
118
|
+
batch the store refuses because it reuses an event id, dead-letters the event
|
|
119
|
+
on that first attempt instead: a retry would fail the same way. The event
|
|
120
|
+
waits until you
|
|
121
|
+
[retry or skip it](/server/consumers#resolve-a-dead-letter) with
|
|
113
122
|
`session.redrive()`.
|
|
114
123
|
|
|
115
124
|
## Limits
|
|
@@ -120,13 +129,14 @@ waits on the same resource in one lane. The event waits until you
|
|
|
120
129
|
| No scheduler configured | A later top-level append or explicit `drain()` wakes the session. Reads never do. |
|
|
121
130
|
| Scheduler invocation dies after claiming but before arming | Its unacknowledged queue delivery is the slower fallback. |
|
|
122
131
|
| Store backend unavailable | Safe progress stops until it returns. |
|
|
123
|
-
| External effect succeeds before A2 completion | The
|
|
132
|
+
| External effect succeeds before A2 completion | The consumer may repeat it. Use idempotency or external fencing. |
|
|
124
133
|
|
|
125
134
|
## What append never throws for
|
|
126
135
|
|
|
127
|
-
A failing
|
|
128
|
-
if persistent, [dead-letter the event](#when-a-
|
|
136
|
+
A failing consumer never makes `append` throw. Its failures are retried and,
|
|
137
|
+
if persistent, [dead-letter the event](#when-a-consumer-keeps-failing) for
|
|
138
|
+
that consumer.
|
|
129
139
|
|
|
130
|
-
Append success means the event is durable, not that its
|
|
140
|
+
Append success means the event is durable, not that its consumers finished. An
|
|
131
141
|
identical batch retry returns rows with the same ids. A mixed batch throws
|
|
132
142
|
[`PARTIAL_DUPLICATE_BATCH`](/reference/errors).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: Where A2 keeps your session histories.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
14
|
+
consumers: [
|
|
15
|
+
/* ... */
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Store | Use it for |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| [Redis](/stores/redis) | Production (recommended) |
|
|
23
|
+
| [Redis over HTTP](/stores/redis-http) | Production, when your provider only offers a REST API |
|
|
24
|
+
| [Postgres](/stores/postgres) | Production |
|
|
25
|
+
| [SQLite](/stores/sqlite) | Development: the default |
|
|
26
|
+
| [Memory](/stores/memory) | Tests: the default when `NODE_ENV=test` |
|
|
27
|
+
|
|
28
|
+
When you omit `store`, development gets SQLite, so state survives `next dev`
|
|
29
|
+
restarts, and tests get memory. Production has no default, on purpose: the
|
|
30
|
+
server throws `STORE_NOT_CONFIGURED` at startup rather than write events to a
|
|
31
|
+
filesystem that evaporates.
|
|
32
|
+
|
|
33
|
+
A store holds A2's session histories. It doesn't connect A2 to your
|
|
34
|
+
application tables; see
|
|
35
|
+
[Sessions and your database](/contract/sessions#sessions-and-your-database).
|
|
36
|
+
|
|
37
|
+
Servers with consumers or timers also need a [scheduler](/schedulers/overview).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Redis
|
|
3
|
+
description: The recommended production store. Live streams are pushed, not polled.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
14
|
+
consumers: [
|
|
15
|
+
/* ... */
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```package-install
|
|
21
|
+
npm i ioredis
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Works on a single Redis instance and on non-cluster providers such as Upstash,
|
|
25
|
+
which is durable by default (use its TCP `rediss://` URL). There's nothing to
|
|
26
|
+
set up: A2 creates its keys as it writes.
|
|
27
|
+
|
|
28
|
+
## Options
|
|
29
|
+
|
|
30
|
+
| Option | |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `url` | creates an `ioredis` client |
|
|
33
|
+
| `client` | your own `ioredis` client instead |
|
|
34
|
+
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'` |
|
|
35
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
36
|
+
|
|
37
|
+
## Upgrade
|
|
38
|
+
|
|
39
|
+
The store records its key layout in `<keyPrefix>:schema`, and refuses a prefix
|
|
40
|
+
marked with another one with `STORE_NOT_CONFIGURED`. A release that changes
|
|
41
|
+
the layout says what to do on its [upgrade page](/upgrades/overview).
|
|
42
|
+
[0.21](/upgrades/0-21) reads what earlier releases wrote under the same
|
|
43
|
+
prefix.
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
|
|
47
|
+
- Each session is a Redis Stream.
|
|
48
|
+
- Every write publishes a wake-up, so live streams are push-driven, and
|
|
49
|
+
connections scale with your processes, not with your viewers.
|
|
50
|
+
- A wake-up is disposable: a safety re-read recovers a lost one within ten
|
|
51
|
+
seconds.
|
|
52
|
+
|
|
53
|
+
## Cost per command
|
|
54
|
+
|
|
55
|
+
On providers that bill per command, such as Upstash, appending an event with a
|
|
56
|
+
consumer and running it takes 19 commands, and an idle live stream takes 6 a
|
|
57
|
+
minute.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Redis over HTTP
|
|
3
|
+
description: The same Redis store over fetch, for providers with only a REST API.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redisHttp } from 'experimental-a2/store-redis-http'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
const url = process.env.UPSTASH_REDIS_REST_URL
|
|
12
|
+
const token = process.env.UPSTASH_REDIS_REST_TOKEN
|
|
13
|
+
if (!url || !token) throw new Error('set the Upstash REST URL and token')
|
|
14
|
+
|
|
15
|
+
export const ordersServer = createServer({
|
|
16
|
+
contract: orders,
|
|
17
|
+
store: redisHttp({ url, token }),
|
|
18
|
+
consumers: [
|
|
19
|
+
/* ... */
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Stores sessions exactly like [Redis](/stores/redis), with no client dependency
|
|
25
|
+
and nothing to set up. Use it when your provider only offers a REST API.
|
|
26
|
+
|
|
27
|
+
:::warning
|
|
28
|
+
Live streams trust the messages on the store's `:notify` channels. Browsers
|
|
29
|
+
never hold Redis credentials, so they can't reach those channels. The risk is a
|
|
30
|
+
credential that can publish but not write, such as a token another service
|
|
31
|
+
uses: it could add or hide events in live views. Grant publish access only to
|
|
32
|
+
credentials that can also write the store.
|
|
33
|
+
:::
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
| Option | |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `url`, `token` | your provider's REST endpoint and token |
|
|
40
|
+
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'` |
|
|
41
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
42
|
+
|
|
43
|
+
## Live updates
|
|
44
|
+
|
|
45
|
+
- Live events and presence use Upstash's HTTP `SUBSCRIBE` endpoint, with the
|
|
46
|
+
same URL and token. Each active channel holds one outbound HTTP subscription
|
|
47
|
+
per store instance, shared by its viewers and closed when the last one
|
|
48
|
+
leaves.
|
|
49
|
+
- Writes publish their wake-ups in the same Redis script as the append. A
|
|
50
|
+
rejected notification doesn't fail the write.
|
|
51
|
+
- Idle streams re-read every ten seconds to recover a missed notification.
|
|
52
|
+
Subscriptions reconnect on their own and catch up from the event cursor.
|
|
53
|
+
- After a reconnect, A2 re-reads presence and clears fields deleted during the
|
|
54
|
+
gap, so presence may repeat its latest values.
|
|
55
|
+
- If `SUBSCRIBE` answers 404, 405, or 501, the store polls instead, every 25ms
|
|
56
|
+
while busy and up to every two seconds when idle. Other errors stay errors.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Postgres
|
|
3
|
+
description: Any Postgres database. You create A2's tables; A2 never changes your schema.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { postgres } from 'experimental-a2/store-postgres'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: postgres({ connectionString: process.env.DATABASE_URL }),
|
|
14
|
+
consumers: [
|
|
15
|
+
/* ... */
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```package-install
|
|
21
|
+
npm i pg
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Works with Neon, Supabase, RDS, or your own server, including transaction-mode
|
|
25
|
+
poolers.
|
|
26
|
+
|
|
27
|
+
## Create the tables
|
|
28
|
+
|
|
29
|
+
A2 doesn't create or change tables in your database. This is the SQL it needs:
|
|
30
|
+
[`schema.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/schema.sql),
|
|
31
|
+
also in the package at `node_modules/experimental-a2/sql/postgres/schema.sql`.
|
|
32
|
+
Apply it before your first deploy, however you run SQL against your database
|
|
33
|
+
or as one of your migrations, as the role your app connects with. A breakpoint
|
|
34
|
+
comment ends each statement, for tools that send one statement per query.
|
|
35
|
+
`a2_batch` runs the statements it's handed, so only its owner may call it,
|
|
36
|
+
whatever your default privileges grant new functions. An app that connects as
|
|
37
|
+
another role needs `grant execute on function a2_batch(text[], jsonb)` beside
|
|
38
|
+
its grants on the tables, and keeps it when you apply the file again.
|
|
39
|
+
|
|
40
|
+
A2 checks nothing up front. It relies on the table and column names, the
|
|
41
|
+
column types, the primary keys, the unique index on `a2_events.event_id`, and
|
|
42
|
+
the `a2_batch` function; the other indexes keep its queries fast. A query that
|
|
43
|
+
finds a table, column, or the function missing throws `STORE_NOT_CONFIGURED`,
|
|
44
|
+
naming the file. Once it's applied, the next request works, with no redeploy.
|
|
45
|
+
|
|
46
|
+
`a2_batch` runs several statements in one round trip. Each append, claim,
|
|
47
|
+
completion, and `consume` poll is one call of it, so they hold a session's lock
|
|
48
|
+
only while the database runs the call, not across round trips from your app.
|
|
49
|
+
That's what keeps one busy session fast on a database a few milliseconds
|
|
50
|
+
away. It needs `read committed`, Postgres's default isolation level, and
|
|
51
|
+
throws `STORE_NOT_CONFIGURED` under a stricter default.
|
|
52
|
+
|
|
53
|
+
## With Drizzle
|
|
54
|
+
|
|
55
|
+
[`drizzle-schema.ts`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/drizzle-schema.ts),
|
|
56
|
+
next to `schema.sql`, shows the same tables as Drizzle definitions. It's an
|
|
57
|
+
example, not an API: one way to fit A2's tables into a Drizzle project is a
|
|
58
|
+
copy next to your own schema, so your migrations create them with yours.
|
|
59
|
+
|
|
60
|
+
```ts drizzle.config.ts
|
|
61
|
+
import { defineConfig } from 'drizzle-kit'
|
|
62
|
+
|
|
63
|
+
export default defineConfig({
|
|
64
|
+
dialect: 'postgresql',
|
|
65
|
+
// your tables, and a copy of drizzle-schema.ts:
|
|
66
|
+
schema: ['./db/schema.ts', './db/a2.ts'],
|
|
67
|
+
out: './drizzle',
|
|
68
|
+
dbCredentials: { url: process.env.DATABASE_URL ?? '' },
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`drizzle-kit generate` then writes A2's tables into a migration you can read,
|
|
73
|
+
and `drizzle-kit migrate` applies it. Adapt the copy to your Drizzle version
|
|
74
|
+
and layout, as long as the names and types stay A2's.
|
|
75
|
+
|
|
76
|
+
Drizzle can't declare a function, so add `a2_batch` as a custom migration:
|
|
77
|
+
run `drizzle-kit generate --custom`, and paste the `do` block that creates
|
|
78
|
+
`a2_batch` from `schema.sql` into the file it makes. It creates the function
|
|
79
|
+
only if it's missing, so the migration is safe on any database.
|
|
80
|
+
|
|
81
|
+
A generated migration only creates tables. On a database that already has
|
|
82
|
+
A2's tables, from an earlier release or a `schema.sql` applied by hand, put
|
|
83
|
+
`schema.sql` in place of the SQL drizzle-kit generated for them: it creates
|
|
84
|
+
what's missing and upgrades the rest, keeping their events, and your schema
|
|
85
|
+
still describes them for the next `generate`. To keep a table of your own
|
|
86
|
+
from A2's logs, see [SQL projections](/server/sql-projections).
|
|
87
|
+
|
|
88
|
+
## Upgrade
|
|
89
|
+
|
|
90
|
+
A release that changes A2's tables says so in its changelog, and its
|
|
91
|
+
[upgrade page](/upgrades/overview) says when to apply its `schema.sql`. With
|
|
92
|
+
Drizzle, bring your copy of `drizzle-schema.ts` in line with the new one.
|
|
93
|
+
|
|
94
|
+
## Live updates (polling)
|
|
95
|
+
|
|
96
|
+
By default, live streams poll every 25ms while a session is busy, backing off
|
|
97
|
+
to every two seconds when it's quiet, so an update from another client can take
|
|
98
|
+
up to two seconds, plus query latency, to reach a quiet stream. A client's own
|
|
99
|
+
`push()` refreshes its stream immediately, even when the write and the stream
|
|
100
|
+
run in different invocations.
|
|
101
|
+
|
|
102
|
+
## Live updates (notifications)
|
|
103
|
+
|
|
104
|
+
For instant updates, pass `listenConnectionString`, a direct connection to the
|
|
105
|
+
same database (not through a pooler), and apply
|
|
106
|
+
[`notifications.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/notifications.sql)
|
|
107
|
+
once, the same way as `schema.sql` (with Drizzle, as a custom migration:
|
|
108
|
+
`drizzle-kit generate --custom`). A missing trigger fails no query, so this is
|
|
109
|
+
the one thing A2 looks for: a listening store's first subscription throws
|
|
110
|
+
`STORE_NOT_CONFIGURED` until the triggers exist.
|
|
111
|
+
|
|
112
|
+
Its triggers notify listeners of every write, including writes from stores
|
|
113
|
+
without the option. Notifications arrive after commit, a reconnect catches up
|
|
114
|
+
from the cursor, and subscriptions also reconcile every ten seconds. Every
|
|
115
|
+
serverless instance opens its own direct connection, so leave connection
|
|
116
|
+
headroom.
|
|
117
|
+
|
|
118
|
+
## Use postgres.js
|
|
119
|
+
|
|
120
|
+
`connectionString` creates a `pg` pool. To use
|
|
121
|
+
[postgres.js](https://github.com/porsager/postgres) instead, wrap it as
|
|
122
|
+
`client`:
|
|
123
|
+
|
|
124
|
+
```ts server/store.ts
|
|
125
|
+
import postgresJs from 'postgres'
|
|
126
|
+
import { postgres } from 'experimental-a2/store-postgres'
|
|
127
|
+
|
|
128
|
+
const url = process.env.DATABASE_URL
|
|
129
|
+
if (!url) throw new Error('set DATABASE_URL')
|
|
130
|
+
|
|
131
|
+
const sql = postgresJs(url, {
|
|
132
|
+
// A2 sends JSON as text; this keeps postgres.js from encoding it again.
|
|
133
|
+
types: {
|
|
134
|
+
json: {
|
|
135
|
+
to: 114,
|
|
136
|
+
from: [114, 3802],
|
|
137
|
+
serialize: (x: unknown) => (typeof x === 'string' ? x : JSON.stringify(x)),
|
|
138
|
+
parse: (x: string) => JSON.parse(x),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
export const store = postgres({
|
|
144
|
+
client: {
|
|
145
|
+
query: async (text, values = []) => ({
|
|
146
|
+
rows: [...(await sql.unsafe(text, values))],
|
|
147
|
+
}),
|
|
148
|
+
connect: async () => {
|
|
149
|
+
const connection = await sql.reserve()
|
|
150
|
+
return {
|
|
151
|
+
query: async (text, values = []) => ({
|
|
152
|
+
rows: [...(await connection.unsafe(text, values))],
|
|
153
|
+
}),
|
|
154
|
+
release: () => connection.release(),
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
end: () => sql.end(),
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`listenConnectionString` still opens its listener with `pg`, so live
|
|
163
|
+
notifications need `pg` installed too.
|
|
164
|
+
|
|
165
|
+
## Options
|
|
166
|
+
|
|
167
|
+
| Option | |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `connectionString` | creates a `pg` pool, shared by stores with the same settings |
|
|
170
|
+
| `client` | your own pool instead: a `pg` `Pool`, PGlite, or a pooler wrapper. Its lifecycle is yours |
|
|
171
|
+
| `listenConnectionString` | a direct connection for notifications |
|
|
172
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SQLite
|
|
3
|
+
description: The development default, in a file at `.a2/dev.db`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { sqlite } from 'experimental-a2/store-sqlite'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
store: sqlite({ path: '.a2/orders.db' }),
|
|
14
|
+
consumers: [
|
|
15
|
+
/* ... */
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
In development, a server without a `store` uses SQLite at `.a2/dev.db`, so
|
|
21
|
+
your sessions survive `next dev` restarts. Pass it yourself to pick another
|
|
22
|
+
file.
|
|
23
|
+
|
|
24
|
+
It runs on `node:sqlite`, with no dependencies, and creates its tables when it
|
|
25
|
+
opens the file. A file a release before [0.21](/upgrades/0-21) wrote throws
|
|
26
|
+
`STORE_NOT_CONFIGURED` when it opens: delete it, or pass another `path`. It's
|
|
27
|
+
for development: a serverless filesystem doesn't last or
|
|
28
|
+
share one file across instances, so production needs another
|
|
29
|
+
[store](/stores/overview).
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
| Option | |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `path` | the database file. Default `.a2/dev.db`; `:memory:` works |
|
|
36
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Memory
|
|
3
|
+
description: An in-process store for tests, the default when `NODE_ENV=test`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// in a test:
|
|
8
|
+
import { createServer } from 'experimental-a2/server'
|
|
9
|
+
import { memory } from 'experimental-a2/store-memory'
|
|
10
|
+
import { orders } from '@/contracts'
|
|
11
|
+
|
|
12
|
+
const ordersServer = createServer({ contract: orders, store: memory() })
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Sessions live in the process and vanish when it exits. Under `NODE_ENV=test`, a
|
|
16
|
+
server without a `store` already uses memory.
|
|
17
|
+
|
|
18
|
+
## Options
|
|
19
|
+
|
|
20
|
+
| Option | |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { order: 4 }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: What to do when you move to a new release. One page per release that needs steps from you, newest first.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## The releases
|
|
7
|
+
|
|
8
|
+
| Release | What you do |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| [0.21](/upgrades/0-21) | Rewrite handlers as consumers. Let earlier deployments finish; on Postgres, then apply the new `schema.sql`. SQLite starts fresh. |
|
|
11
|
+
| [0.20](/upgrades/0-20) | Postgres: apply `schema.sql` yourself, before you deploy. |
|
|
12
|
+
| [0.19](/upgrades/0-19) | Start new agent sessions: earlier ones don't fold. |
|
|
13
|
+
| [0.18](/upgrades/0-18) | Move the AI SDK loop's options into `harness`. |
|
|
14
|
+
| [0.17](/upgrades/0-17) | Redis: use a fresh `keyPrefix`. Serve agents with `agent()`. |
|
|
15
|
+
|
|
16
|
+
## Across several releases
|
|
17
|
+
|
|
18
|
+
Read every page between the release you run and the one you're moving to,
|
|
19
|
+
oldest first. Code changes add up: make each page's changes. Store steps
|
|
20
|
+
don't always: each release's `schema.sql` covers a database from any earlier
|
|
21
|
+
release, so a Postgres database needs only the newest one, applied the way the
|
|
22
|
+
newest page says, and one fresh Redis `keyPrefix` covers every release that
|
|
23
|
+
asks for one.
|
|
24
|
+
|
|
25
|
+
## Everything else
|
|
26
|
+
|
|
27
|
+
Each release's changelog lists everything it changes, including what needs no
|
|
28
|
+
steps: [`CHANGELOG.md`](https://github.com/vercel-labs/a2/blob/main/packages/a2/CHANGELOG.md),
|
|
29
|
+
also in the package at `node_modules/experimental-a2/CHANGELOG.md`. Releases
|
|
30
|
+
before 0.17 are only there.
|