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,323 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Production
|
|
3
|
-
description: Choose a durable store, add a scheduler for recovery and timers, and know what to do when an event dead-letters.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## 1. Choose a store
|
|
7
|
-
|
|
8
|
-
```ts server/orders.ts
|
|
9
|
-
import { createServer } from 'experimental-a2/server'
|
|
10
|
-
import { redis } from 'experimental-a2/store-redis'
|
|
11
|
-
import { orders } from '@/contracts'
|
|
12
|
-
|
|
13
|
-
export const ordersServer = createServer({
|
|
14
|
-
contract: orders,
|
|
15
|
-
store: redis({ url: process.env.REDIS_URL }),
|
|
16
|
-
handlers: {
|
|
17
|
-
/* ... */
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
| Entry point | Backend | Use it for |
|
|
23
|
-
| --- | --- | --- |
|
|
24
|
-
| `experimental-a2/store-redis` | Redis | Production (recommended) |
|
|
25
|
-
| `experimental-a2/store-redis-http` | Redis over a REST API | Production |
|
|
26
|
-
| `experimental-a2/store-postgres` | Postgres | Production |
|
|
27
|
-
| `experimental-a2/store-sqlite` | SQLite | Development: the default, at `.a2/dev.db` |
|
|
28
|
-
| `experimental-a2/store-memory` | In-memory | Tests: the default when `NODE_ENV=test` |
|
|
29
|
-
|
|
30
|
-
When you omit `store`, development gets SQLite (state survives `next dev`
|
|
31
|
-
restarts) and tests get memory. Production has no default, on purpose: the
|
|
32
|
-
server throws `STORE_NOT_CONFIGURED` at startup rather than write events to a
|
|
33
|
-
filesystem that evaporates.
|
|
34
|
-
|
|
35
|
-
The store holds A2's session histories. It does not connect A2 to your
|
|
36
|
-
application tables; see [Sessions and your database](/contract/sessions#sessions-and-your-database).
|
|
37
|
-
|
|
38
|
-
### Redis (recommended)
|
|
39
|
-
|
|
40
|
-
`redis({ url })` from `experimental-a2/store-redis` stores each session as a
|
|
41
|
-
Redis Stream and streams push-natively, so connections scale with your
|
|
42
|
-
processes, not your audience. `ioredis` is an optional peer dependency; pass
|
|
43
|
-
`url` or inject a client. It works on single instances and non-cluster
|
|
44
|
-
providers such as Upstash, where durability is on by default.
|
|
45
|
-
|
|
46
|
-
On providers that bill per command, such as Upstash, appending an event with a
|
|
47
|
-
handler and running it takes 24 commands, and an idle live stream takes 6 a
|
|
48
|
-
minute. See
|
|
49
|
-
[`experimental-a2/store-redis`](/reference/adapters#experimental-a2store-redis)
|
|
50
|
-
for how live streams recover a lost wake-up.
|
|
51
|
-
|
|
52
|
-
When only a REST API is available, `redisHttp({ url, token })` from
|
|
53
|
-
`experimental-a2/store-redis-http` speaks the same storage over `fetch`, with
|
|
54
|
-
no client dependency or extra setup. Live events and presence use Upstash's
|
|
55
|
-
HTTP `SUBSCRIBE` endpoint with the same URL and token. If that endpoint answers
|
|
56
|
-
404, 405, or 501, the store polls instead; other errors stay errors. See
|
|
57
|
-
[`experimental-a2/store-redis-http`](/reference/adapters#experimental-a2store-redis-http)
|
|
58
|
-
for its subscriptions, reconnects, and fallback.
|
|
59
|
-
|
|
60
|
-
:::warning
|
|
61
|
-
`redisHttp` live streams trust the messages on the store's `:notify`
|
|
62
|
-
channels. Browsers never hold Redis credentials, so they can't reach those
|
|
63
|
-
channels. The risk is a Redis credential that can publish but not write, such
|
|
64
|
-
as a token another service uses: it could add or hide events in live views.
|
|
65
|
-
Grant publish access only to credentials that can also write the store.
|
|
66
|
-
:::
|
|
67
|
-
|
|
68
|
-
### Postgres
|
|
69
|
-
|
|
70
|
-
Any Postgres works: Neon, Supabase, RDS, or your own box, including
|
|
71
|
-
transaction-mode poolers. `pg` is an optional peer dependency. Pass
|
|
72
|
-
`connectionString`, or inject your own pool as `client`; an injected pool's
|
|
73
|
-
platform lifecycle is yours to manage.
|
|
74
|
-
|
|
75
|
-
Live streams poll by default, backing off when a session goes quiet. For
|
|
76
|
-
native notifications, also pass `listenConnectionString`: a direct connection
|
|
77
|
-
to the same database. It installs notification triggers on A2's event and
|
|
78
|
-
presence tables, which stay installed and also cover writes from instances
|
|
79
|
-
without the option. Stores in one runtime share a listener, but every
|
|
80
|
-
serverless instance opens its own direct connection, so leave connection
|
|
81
|
-
headroom. See
|
|
82
|
-
[`experimental-a2/store-postgres`](/reference/adapters#experimental-a2store-postgres)
|
|
83
|
-
for live-stream latency and recovery.
|
|
84
|
-
|
|
85
|
-
## 2. Add a scheduler for handlers and timers
|
|
86
|
-
|
|
87
|
-
The scheduler puts a clock on recovery and on [timers](/server/timers).
|
|
88
|
-
Vercel Queues is the recommended default on Vercel. QStash delivers over HTTP
|
|
89
|
-
from separate infrastructure. Either adapter carries recovery and
|
|
90
|
-
`session.schedule` tasks through one route. Scheduled delays must fit the
|
|
91
|
-
[provider's limit](/server/timers#provider-limits).
|
|
92
|
-
|
|
93
|
-
### Vercel Queues (recommended)
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
// server/orders.ts, now with a scheduler:
|
|
97
|
-
import { createServer } from 'experimental-a2/server'
|
|
98
|
-
import { redis } from 'experimental-a2/store-redis'
|
|
99
|
-
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
100
|
-
import { orders } from '@/contracts'
|
|
101
|
-
|
|
102
|
-
export const scheduler = vercelQueues()
|
|
103
|
-
|
|
104
|
-
export const ordersServer = createServer({
|
|
105
|
-
contract: orders,
|
|
106
|
-
store: redis({ url: process.env.REDIS_URL }),
|
|
107
|
-
scheduler,
|
|
108
|
-
handlers: {
|
|
109
|
-
/* ... */
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
`@vercel/queue` is a peer dependency. Share one scheduler instance across
|
|
115
|
-
servers, and mount its handler once with every server it may deliver to:
|
|
116
|
-
|
|
117
|
-
```ts app/api/a2/scheduler/route.ts
|
|
118
|
-
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
119
|
-
|
|
120
|
-
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
`scheduler.handler(...)` checks its servers when the module loads: at least
|
|
124
|
-
one drainable server, with unique contract names. A bad route fails at
|
|
125
|
-
startup, or in a build that evaluates route modules.
|
|
126
|
-
|
|
127
|
-
```json vercel.json
|
|
128
|
-
{
|
|
129
|
-
"functions": {
|
|
130
|
-
"app/api/a2/scheduler/route.ts": {
|
|
131
|
-
"experimentalTriggers": [
|
|
132
|
-
{
|
|
133
|
-
"type": "queue/v2beta",
|
|
134
|
-
"topic": "a2"
|
|
135
|
-
}
|
|
136
|
-
]
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
The trigger is required. It connects the route to Vercel Queues and makes the
|
|
143
|
-
function private, so only queue infrastructure can invoke it. Don't expose the
|
|
144
|
-
callback as a public route or wrap it in a second authentication scheme.
|
|
145
|
-
|
|
146
|
-
The adapter keeps messages for 7 days (Vercel Queues' default is 24 hours), so
|
|
147
|
-
scheduled appends and clears can be up to 7 days out.
|
|
148
|
-
|
|
149
|
-
### QStash
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
// server/orders.ts, with QStash instead:
|
|
153
|
-
import { createServer } from 'experimental-a2/server'
|
|
154
|
-
import { qstash } from 'experimental-a2/scheduler-qstash'
|
|
155
|
-
import { redis } from 'experimental-a2/store-redis'
|
|
156
|
-
import { orders } from '@/contracts'
|
|
157
|
-
|
|
158
|
-
export const scheduler = qstash()
|
|
159
|
-
|
|
160
|
-
export const ordersServer = createServer({
|
|
161
|
-
contract: orders,
|
|
162
|
-
store: redis({ url: process.env.REDIS_URL }),
|
|
163
|
-
scheduler,
|
|
164
|
-
handlers: {
|
|
165
|
-
/* ... */
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
`@upstash/qstash` 2.11.0 or newer is a peer dependency. Mount
|
|
171
|
-
`scheduler.handler(...)` in the same `app/api/a2/scheduler/route.ts`, and set
|
|
172
|
-
`QSTASH_TOKEN`, `QSTASH_CURRENT_SIGNING_KEY`, and `QSTASH_NEXT_SIGNING_KEY`.
|
|
173
|
-
|
|
174
|
-
- **Callback URL:** inferred from `PORT` locally, and from
|
|
175
|
-
`VERCEL_PROJECT_PRODUCTION_URL` or `VERCEL_BRANCH_URL` on Vercel. Pass `url`
|
|
176
|
-
anywhere else.
|
|
177
|
-
- **Local development:** A2 starts QStash's development server on the first
|
|
178
|
-
publish or verification, with no QStash variables needed.
|
|
179
|
-
- **Security:** the route verifies `Upstash-Signature` before touching the
|
|
180
|
-
store and answers unsigned traffic with 401. Behind Deployment Protection,
|
|
181
|
-
pass an automation bypass as a callback header.
|
|
182
|
-
- **Outages:** delivery is at least once and A2 keeps recovery going, but when
|
|
183
|
-
QStash can't reach the route at all, redrive its DLQ.
|
|
184
|
-
|
|
185
|
-
The [adapter reference](/reference/adapters#experimental-a2scheduler-qstash)
|
|
186
|
-
covers the options, protected previews, Edge runtimes, and body limits.
|
|
187
|
-
|
|
188
|
-
### More than one scheduler
|
|
189
|
-
|
|
190
|
-
Servers with different scheduler instances need separate routes, even from the
|
|
191
|
-
same adapter. Each extra QStash instance passes its own `url`. Each Vercel
|
|
192
|
-
Queues instance needs its own `topic` and a matching `experimentalTriggers`
|
|
193
|
-
entry.
|
|
194
|
-
|
|
195
|
-
### Dispatch in a fresh invocation
|
|
196
|
-
|
|
197
|
-
Ordinary `append` starts handlers inline and uses the scheduler as a watchdog.
|
|
198
|
-
`append.dispatch` persists the input and hands the pending work straight to the
|
|
199
|
-
queue instead:
|
|
200
|
-
|
|
201
|
-
```ts app/api/imports/route.ts
|
|
202
|
-
import { ordersServer } from '@/server'
|
|
203
|
-
|
|
204
|
-
export async function POST(request: Request) {
|
|
205
|
-
const { orderId, shopId, items } = await request.json()
|
|
206
|
-
await ordersServer.session(orderId).append.dispatch({
|
|
207
|
-
id: `created:${orderId}`,
|
|
208
|
-
type: 'created',
|
|
209
|
-
payload: { shopId, items },
|
|
210
|
-
})
|
|
211
|
-
return Response.json({ accepted: true }, { status: 202 })
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
It requires a scheduler. The call awaits the queue send and starts no inline
|
|
216
|
-
drain. If the send fails, the event may already be durable; its explicit id
|
|
217
|
-
makes the retry idempotent. Dispatch controls the wakeup, not which worker
|
|
218
|
-
claims the event.
|
|
219
|
-
|
|
220
|
-
### Running without a scheduler
|
|
221
|
-
|
|
222
|
-
If no event type has a handler and nothing calls `session.schedule`, skip
|
|
223
|
-
`scheduler`. Events settle in their append transaction, with no drains, claims,
|
|
224
|
-
or queue messages. That's a complete production configuration: a durable event
|
|
225
|
-
log for history, reducers, and live sync.
|
|
226
|
-
|
|
227
|
-
With handlers, skipping it means the only wakeups are a top-level append or an
|
|
228
|
-
explicit `server.drain()`. Reads never wake a session, so one nobody wakes
|
|
229
|
-
stays stuck. That's fine for internal tools where "heals on the next write" is
|
|
230
|
-
acceptable. For production, configure a scheduler.
|
|
231
|
-
|
|
232
|
-
## 3. When an event dead-letters
|
|
233
|
-
|
|
234
|
-
A [dead-lettered](/server/guarantees#when-a-handler-keeps-failing) event waits
|
|
235
|
-
for you. List a session's dead letters, then resolve each one:
|
|
236
|
-
|
|
237
|
-
```ts
|
|
238
|
-
// anywhere on the server, behind your own operator checks:
|
|
239
|
-
import { ordersServer } from '@/server'
|
|
240
|
-
|
|
241
|
-
const order = ordersServer.session(orderId)
|
|
242
|
-
const [dead] = await order.deadLetters()
|
|
243
|
-
if (dead) {
|
|
244
|
-
// dead.lastError says why (null if every attempt timed out or crashed);
|
|
245
|
-
// deploy the handler fix first, then:
|
|
246
|
-
await order.redrive(dead.index, { action: 'retry' })
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
- **Fix and retry.** `{ action: 'retry' }` runs the handler again with a fresh
|
|
251
|
-
ten-failure budget. Once it completes, its lane continues.
|
|
252
|
-
- **Skip.** `{ action: 'skip' }` marks the event processed without running it,
|
|
253
|
-
accepting that its effects never happen. The next event in its lane runs.
|
|
254
|
-
|
|
255
|
-
Each redrive is one atomic store operation, and it wakes the session the way an
|
|
256
|
-
append does, so the lane resumes without another write. An event that isn't
|
|
257
|
-
dead-lettered anymore is left alone: `redrive` reports `{ redriven: false }`.
|
|
258
|
-
|
|
259
|
-
## 4. Deploy safely
|
|
260
|
-
|
|
261
|
-
The Vercel adapter's `deploymentId` applies to every task: recovery, scheduled
|
|
262
|
-
appends, and scheduled clears. Omit it to use the publishing deployment, pass
|
|
263
|
-
a deployment ID to target one, or pass `null` for deploymentless delivery.
|
|
264
|
-
Publishing and consuming clients must use the same scope.
|
|
265
|
-
|
|
266
|
-
- **Pinned tasks stay put.** Promoting a new deployment doesn't move them;
|
|
267
|
-
older deployments keep receiving their own work until it is acknowledged or
|
|
268
|
-
expires. Upgrading doesn't repair an older deployment's callback handler.
|
|
269
|
-
- **Deploymentless doesn't mean latest.** Vercel picks an eligible consumer,
|
|
270
|
-
not the latest production deployment. Every eligible consumer must use
|
|
271
|
-
`deploymentId: null` and stay compatible with pending tasks and session work.
|
|
272
|
-
- **Changing the setting doesn't move queued tasks.** Keep consumers for the
|
|
273
|
-
original scope until those tasks finish.
|
|
274
|
-
|
|
275
|
-
QStash's inferred production and branch URLs are moving aliases, so pending
|
|
276
|
-
tasks can reach a later deployment. Keep task envelopes and event schemas
|
|
277
|
-
compatible across deployments, and keep the same `maxBodyBytes` on every
|
|
278
|
-
deployment that can receive the callback: lowering it mid-rollout can reject
|
|
279
|
-
tasks the previous deployment accepted.
|
|
280
|
-
|
|
281
|
-
:::warning
|
|
282
|
-
Don't roll back to an A2 version without timer delivery while scheduled tasks
|
|
283
|
-
are outstanding.
|
|
284
|
-
An old route can acknowledge a new append task as a recovery message without
|
|
285
|
-
writing its events. Rolling back past scheduled clears fails too: an old
|
|
286
|
-
Vercel Queues route keeps failing a clear task until retention ends, and an
|
|
287
|
-
old QStash route rejects it as non-retryable, moving it to QStash's
|
|
288
|
-
dead-letter queue. Roll forward, or wait for those tasks to deliver or remove
|
|
289
|
-
them through the provider first.
|
|
290
|
-
:::
|
|
291
|
-
|
|
292
|
-
## Watch it in traces
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
// server/orders.ts, with telemetry:
|
|
296
|
-
import { createServer } from 'experimental-a2/server'
|
|
297
|
-
import { otel } from 'experimental-a2/otel'
|
|
298
|
-
import { orders } from '@/contracts'
|
|
299
|
-
|
|
300
|
-
export const ordersServer = createServer({
|
|
301
|
-
contract: orders,
|
|
302
|
-
// ...store, scheduler, and handlers as above
|
|
303
|
-
telemetry: otel(),
|
|
304
|
-
})
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
`telemetry` takes an instrumentation hook; `experimental-a2/otel` implements
|
|
308
|
-
it over OpenTelemetry (`@opentelemetry/api` is a peer dependency). With any
|
|
309
|
-
OTel setup registered (on Vercel, `@vercel/otel`), one trace shows the request,
|
|
310
|
-
`a2.append`, the inline `a2.drain`, an `a2.event` span per handler, and the
|
|
311
|
-
appends those handlers make. Failures mark their `a2.event` span,
|
|
312
|
-
`a2.event.attempt` reports `ctx.attempt`, and `a2.event.outcome =
|
|
313
|
-
dead_lettered` is the attribute to alert on. Agent servers add `a2.ai.*`
|
|
314
|
-
spans. See the [span catalogue](/reference/adapters#the-span-catalogue).
|
|
315
|
-
|
|
316
|
-
## Checklist
|
|
317
|
-
|
|
318
|
-
| Piece | Done when |
|
|
319
|
-
| ------------------- | -------------------------------------------------------------------- |
|
|
320
|
-
| Store | `store: redis(...)` (or `postgres(...)`) on every server |
|
|
321
|
-
| Scheduler | for servers with handlers or timers: one shared adapter, route, and provider config |
|
|
322
|
-
| Idempotent handlers | external side effects take `event.id` as an idempotency key |
|
|
323
|
-
| Client split | contracts/reducers isomorphic; only `experimental-a2/server` touches backends |
|