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.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- 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-DFET5q16.js.map +1 -0
- 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-DAu77CDr.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
- package/dist/ai-EEfKyc-z.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 +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +214 -67
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- 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 +86 -73
- 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-BQS4i_V9.js} +29 -6
- package/dist/client-BQS4i_V9.js.map +1 -0
- 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-CvsrprnU.js} +40 -16
- package/dist/internal-CvsrprnU.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-CIeL33wj.js +12 -0
- package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
- package/dist/server-qgez0Xny.d.ts.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-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.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-D0MhBvg6.d.ts +532 -0
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- 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 +406 -225
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.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 +1598 -587
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
- package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +527 -299
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- 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-CV-Z7wz-.js} +6 -5
- package/dist/wire-CV-Z7wz-.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +13 -2
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +36 -25
- package/docs/agents/04-harnesses.mdx +14 -7
- package/docs/client/03-presence.mdx +18 -14
- 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 +29 -24
- package/docs/reference/01-core.mdx +7 -6
- package/docs/reference/02-server.mdx +250 -191
- package/docs/reference/04-ai.mdx +33 -23
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +25 -19
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +418 -0
- package/docs/server/02-timers.mdx +31 -21
- 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 +61 -51
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +24 -10
- package/docs/stores/03-redis-http.mdx +8 -6
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +31 -0
- package/docs/upgrades/78-0-22.mdx +115 -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 +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a0/server.ts +4 -4
- 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 +209 -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/0004_a2_session_event_ids.sql +2 -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/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -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/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- 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/lib/store.ts +1 -1
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +209 -0
- package/sql/postgres/schema.sql +227 -67
- package/src/actor-shared.ts +10 -2
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +162 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +324 -82
- package/src/ai-harness.ts +17 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/client.ts +19 -2
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +27 -4
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +57 -16
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +819 -218
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +604 -286
- package/src/store-postgres.ts +2006 -857
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +1961 -1333
- package/src/store-redis-notify.ts +76 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +915 -516
- package/src/store.ts +241 -102
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/actor-C7b-1arn.d.ts.map +0 -1
- package/dist/actor-shared-t7w8xIKP.js.map +0 -1
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/client-BhHLoNKF.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-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.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-presence-polling-B1ixeqIS.js +0 -94
- package/dist/store-presence-polling-B1ixeqIS.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/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
package/docs/01-quickstart.mdx
CHANGED
|
@@ -5,7 +5,7 @@ description: Define a contract, react to its events, and make your first durable
|
|
|
5
5
|
|
|
6
6
|
## What you'll build
|
|
7
7
|
|
|
8
|
-
An order that notifies a shop when it arrives: one contract, one
|
|
8
|
+
An order that notifies a shop when it arrives: one contract, one consumer, two
|
|
9
9
|
routes, and a log you can read back.
|
|
10
10
|
|
|
11
11
|
## 1. Install
|
|
@@ -33,28 +33,34 @@ export const orders = a2.contract({
|
|
|
33
33
|
|
|
34
34
|
## 3. Implement it
|
|
35
35
|
|
|
36
|
-
`createServer` binds the contract to storage
|
|
37
|
-
the contract:
|
|
36
|
+
A consumer reacts to events, and `createServer` binds the contract to storage
|
|
37
|
+
and its consumers. Same file, below the contract:
|
|
38
38
|
|
|
39
39
|
```ts server/orders.ts
|
|
40
40
|
import { createServer } from 'experimental-a2/server'
|
|
41
41
|
|
|
42
42
|
export const ordersServer = createServer({
|
|
43
43
|
contract: orders,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
consumers: [
|
|
45
|
+
{
|
|
46
|
+
name: 'notify-shop',
|
|
47
|
+
types: ['created'],
|
|
48
|
+
run: async ({ event }) => {
|
|
49
|
+
// your side effect, e.g. email the shop, idempotent via event.id:
|
|
50
|
+
// await sendEmailToShop(event.payload, { idempotencyKey: event.id })
|
|
51
|
+
return { type: 'shop.notified', payload: {} }
|
|
52
|
+
},
|
|
49
53
|
},
|
|
50
|
-
|
|
54
|
+
],
|
|
51
55
|
})
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
-
|
|
58
|
+
- `name` is the consumer's identity: A2 keeps its place in every session under
|
|
59
|
+
it. `types` lists the events it runs on.
|
|
60
|
+
- Consumers can run more than once (at-least-once delivery), so make external
|
|
55
61
|
calls safe to repeat. `event.id` is stable across re-runs: use it as the
|
|
56
62
|
idempotency key.
|
|
57
|
-
- The returned event commits atomically with
|
|
63
|
+
- The returned event commits atomically with the consumer's success. If `run`
|
|
58
64
|
throws or the process dies first, `shop.notified` doesn't appear. Return one
|
|
59
65
|
event or an array.
|
|
60
66
|
|
|
@@ -92,7 +98,7 @@ The payload is validated before anything is written; a bad one fails with
|
|
|
92
98
|
`INVALID_PAYLOAD` and the issues.
|
|
93
99
|
|
|
94
100
|
When `append` returns, the event is durably in the log and A2 has started the
|
|
95
|
-
`
|
|
101
|
+
`notify-shop` consumer inline. In production, a queue-backed scheduler wakes pending
|
|
96
102
|
work after a failure or process death. See [Guarantees](/server/guarantees).
|
|
97
103
|
|
|
98
104
|
## 5. Read the log back
|
|
@@ -122,7 +128,7 @@ curl localhost:3000/api/orders/order-42
|
|
|
122
128
|
```
|
|
123
129
|
|
|
124
130
|
Each event also carries `id`, `sessionId`, and `createdAt` (trimmed here). You
|
|
125
|
-
appended one event and the
|
|
131
|
+
appended one event and the consumer returned the second. Debugging in A2 is
|
|
126
132
|
reading the log.
|
|
127
133
|
|
|
128
134
|
## 6. Fold state out of the log
|
|
@@ -165,12 +171,12 @@ State isn't a second record to update. It's a fold over the log; see
|
|
|
165
171
|
|
|
166
172
|
## That's it
|
|
167
173
|
|
|
168
|
-
You have a validated write path, a
|
|
174
|
+
You have a validated write path, a consumer, a log you can read, and a view
|
|
169
175
|
folded from it. The rest of these docs apply the same two moves, append an
|
|
170
176
|
event and react to it, to timers, cancellation, live UIs, and production.
|
|
171
177
|
|
|
172
178
|
<CardGroup cols={3}>
|
|
173
|
-
<Card title="
|
|
179
|
+
<Card title="Consumers" href="/server/consumers" icon="file">
|
|
174
180
|
Context, chaining, and re-runs.
|
|
175
181
|
</Card>
|
|
176
182
|
<Card title="Stores" href="/stores/overview" icon="rocket">
|
package/docs/02-examples.mdx
CHANGED
|
@@ -24,7 +24,7 @@ work inside the directory.
|
|
|
24
24
|
|
|
25
25
|
- `app/counter` is the smallest complete contract, reducer, server, and React
|
|
26
26
|
client.
|
|
27
|
-
- `app/orders` adds
|
|
27
|
+
- `app/orders` adds consumers and a feature-owned scheduler.
|
|
28
28
|
- `app/chat` shows request-scoped authorization and durable AI generation.
|
|
29
29
|
- `app/agent` shows tools, approvals, reminders, and sandboxed commands.
|
|
30
30
|
- `app/canvas` adds optimistic state, presence, and WebSocket upgrades.
|
|
@@ -103,8 +103,11 @@ no retry. The message settles, and the caller receives
|
|
|
103
103
|
backoff while the instance's lane waits; see
|
|
104
104
|
[Errors](/reference/errors).
|
|
105
105
|
- **A crash** re-runs the handler from the pre-message state. `ctx.id`
|
|
106
|
-
is the message id, stable across re-runs
|
|
107
|
-
key for external I/O.
|
|
106
|
+
is the message id, stable across re-runs and unique within the
|
|
107
|
+
instance: use it as the idempotency key for external I/O. An explicit
|
|
108
|
+
message `id` that repeats in another instance repeats there too, so
|
|
109
|
+
derive explicit ids per instance, for example with
|
|
110
|
+
[`idempotentId()`](/reference/core#idempotentidparts).
|
|
108
111
|
- **A clear** by `handle.session.clear()` before the answer rejects the
|
|
109
112
|
call with an `A2Error` whose code is `SESSION_CLEARED`. The handler
|
|
110
113
|
may already have run. Over HTTP it's a `410`.
|
|
@@ -153,3 +156,11 @@ be plain JSON: a `Date`, a `Map`, or `NaN` fails the commit loudly.
|
|
|
153
156
|
streams, scheduling). The definition exposes its assembled `contract`,
|
|
154
157
|
`server`, and `reducer`, and devtools show the full mailbox. Event
|
|
155
158
|
names starting with `a2.` are reserved for the actor's own bookkeeping.
|
|
159
|
+
|
|
160
|
+
The server runs the handlers on two of A2's own
|
|
161
|
+
[consumers](/server/consumers): `a2.actor` for serial messages, on one
|
|
162
|
+
lane per instance, and `a2.actor.concurrent` for
|
|
163
|
+
[concurrent](/actors/concurrency#concurrent-handlers) ones. A message
|
|
164
|
+
that keeps failing dead-letters for one of them; list and redrive it
|
|
165
|
+
through `server.session(id)` like any
|
|
166
|
+
[dead letter](/server/consumers#resolve-a-dead-letter).
|
|
@@ -5,7 +5,7 @@ description: "Each instance processes one message at a time. Slow work goes in c
|
|
|
5
5
|
|
|
6
6
|
## One message at a time
|
|
7
7
|
|
|
8
|
-
Every message runs on the instance's [lane](/server/
|
|
8
|
+
Every message runs on the instance's [lane](/server/consumers#lanes):
|
|
9
9
|
one handler at a time, in arrival order. A handler sees the state every
|
|
10
10
|
previous message produced, no other message can race its checks, and
|
|
11
11
|
its changes commit before the next message runs.
|
|
@@ -22,7 +22,7 @@ export const assistant = agent({
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
`agent()` is the whole server: the AI protocol, the agent's loop, and A2's
|
|
25
|
-
store, scheduler, and
|
|
25
|
+
store, scheduler, and consumers. `name` is its identity. The loop is the
|
|
26
26
|
agent's `harness`. By default it's `{ type: 'ai-sdk' }`: each response runs as
|
|
27
27
|
durable one-step `streamText()` generations, tuned with `harness.generation`
|
|
28
28
|
and `maxSteps`, listed under [`agent()`](/reference/ai#agentoptions). The same
|
package/docs/agents/02-tools.mdx
CHANGED
|
@@ -42,11 +42,11 @@ Approval policy lives in `harness.generation.toolApproval`, beside the other
|
|
|
42
42
|
per-step settings. Later examples that import `assistant` use the agent from
|
|
43
43
|
[Agents](/agents/introduction#define-the-agent).
|
|
44
44
|
|
|
45
|
-
### Read the
|
|
45
|
+
### Read the tool's context
|
|
46
46
|
|
|
47
47
|
```ts server/set-reminder.ts
|
|
48
48
|
import { tool } from 'ai'
|
|
49
|
-
import {
|
|
49
|
+
import { consumerContext, inputs } from 'experimental-a2/ai'
|
|
50
50
|
import { z } from 'zod'
|
|
51
51
|
|
|
52
52
|
export const setReminder = tool({
|
|
@@ -56,7 +56,7 @@ export const setReminder = tool({
|
|
|
56
56
|
reminder: z.string(),
|
|
57
57
|
}),
|
|
58
58
|
execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
|
|
59
|
-
const ctx =
|
|
59
|
+
const ctx = consumerContext()
|
|
60
60
|
await ctx.session.schedule(
|
|
61
61
|
`reminder:${toolCallId}`,
|
|
62
62
|
{ delay: `${delaySeconds}s` },
|
|
@@ -72,11 +72,12 @@ export const setReminder = tool({
|
|
|
72
72
|
})
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
`
|
|
76
|
-
|
|
75
|
+
`consumerContext()` returns the running tool's
|
|
76
|
+
[consumer context](/server/consumers#the-context): `event`, `attempt`,
|
|
77
|
+
`session`, and `signal`. It works anywhere below `execute`, including
|
|
77
78
|
awaited helpers, and throws outside a tool execution. Pass a shared
|
|
78
79
|
`agentContract()` to type application events; A2 checks it against the
|
|
79
|
-
executing agent. See [`
|
|
80
|
+
executing agent. See [`consumerContext()`](/reference/ai#consumercontextcontract)
|
|
80
81
|
for the event payload.
|
|
81
82
|
|
|
82
83
|
This tool schedules a reminder as a new user message. `ctx.session` belongs to
|
|
@@ -86,12 +87,12 @@ Naming the timer after `toolCallId` and the message after the triggering
|
|
|
86
87
|
event's ID makes a retry converge on the same timer and message, even if a
|
|
87
88
|
provider reuses tool-call IDs. The six-day maximum is Vercel Queues'
|
|
88
89
|
[delay limit](/server/timers#provider-limits); adjust it for your scheduler. A
|
|
89
|
-
transient or ambiguous scheduler failure retries the
|
|
90
|
+
transient or ambiguous scheduler failure retries the tool's run with the same
|
|
90
91
|
identity and target time; a deterministic rejection or a missing scheduler is
|
|
91
92
|
an ordinary tool error.
|
|
92
93
|
|
|
93
|
-
To test a context-reading tool, call it inside `
|
|
94
|
-
from `experimental-a2/ai/server`; within `fn`, `
|
|
94
|
+
To test a context-reading tool, call it inside `runWithConsumerContext(ctx, fn)`
|
|
95
|
+
from `experimental-a2/ai/server`; within `fn`, `consumerContext()` returns `ctx`.
|
|
95
96
|
|
|
96
97
|
### Answer approvals
|
|
97
98
|
|
|
@@ -125,8 +126,8 @@ request as `ai.approval.requested`, and the reducer adds it to
|
|
|
125
126
|
`state.pendingApprovals`. `approve()` and `deny()` copy
|
|
126
127
|
the request, message, generation, and approval IDs from it, so a delayed click
|
|
127
128
|
cannot authorize a later model step. The decision updates the approval part in
|
|
128
|
-
the message, and
|
|
129
|
-
|
|
129
|
+
the message, and A2 then runs the approved tool or records the denied
|
|
130
|
+
result.
|
|
130
131
|
|
|
131
132
|
A policy can also be a function that returns `'user-approval'` or
|
|
132
133
|
`'approved'` from the validated input. It runs during the model step and again
|
|
@@ -50,12 +50,12 @@ Provider-executed tools, structured output, and forced tool choices skip
|
|
|
50
50
|
automatic compaction, and configuring it for them throws; use custom callbacks.
|
|
51
51
|
|
|
52
52
|
The first generation with each Gateway model in a session records
|
|
53
|
-
`ai.model.metadata.requested`;
|
|
54
|
-
records `ai.model.metadata.resolved`, without delaying or failing
|
|
55
|
-
generation. `state.modelMetadata` holds each model's limits and status:
|
|
53
|
+
`ai.model.metadata.requested`; A2's `a2.ai.metadata` consumer fetches the
|
|
54
|
+
catalog and records `ai.model.metadata.resolved`, without delaying or failing
|
|
55
|
+
the generation. `state.modelMetadata` holds each model's limits and status:
|
|
56
56
|
pending, resolved, or unavailable (absent from the catalog). A lookup that
|
|
57
|
-
exhausted its retries stays pending without a new lookup each turn, so
|
|
58
|
-
|
|
57
|
+
exhausted its retries stays pending without a new lookup each turn, so check
|
|
58
|
+
the session's dead letters for `a2.ai.metadata` or set a threshold. The estimate is serialized UTF-8
|
|
59
59
|
bytes divided by four, calibrated by the previous same-model call's usage. It is
|
|
60
60
|
not a tokenizer, makes no remote calls, and does not precisely measure new image
|
|
61
61
|
or audio tokens.
|
|
@@ -191,11 +191,13 @@ transcript. The session's state is `AIState` merged with yours, so a key that
|
|
|
191
191
|
shadows one of `AIState`'s is a type error. The reducer's `name` is its snapshot
|
|
192
192
|
identity: rename it when `fold` changes.
|
|
193
193
|
|
|
194
|
-
Serve it with `agent({ contract })
|
|
194
|
+
Serve it with `agent({ contract })`, and react to your own events with
|
|
195
|
+
`consumers`, next to the agent's. Every resolver receives your state as
|
|
195
196
|
`state`:
|
|
196
197
|
|
|
197
198
|
```ts server/support-desk.ts
|
|
198
199
|
import { agent } from 'experimental-a2/ai/server'
|
|
200
|
+
import { consumer } from 'experimental-a2/server'
|
|
199
201
|
import { supportDesk } from '../support-desk'
|
|
200
202
|
|
|
201
203
|
export const supportDeskAgent = agent({
|
|
@@ -204,12 +206,16 @@ export const supportDeskAgent = agent({
|
|
|
204
206
|
state.plan === 'pro'
|
|
205
207
|
? { model: 'openai/gpt-5.6-terra', reasoning: 'high' }
|
|
206
208
|
: { model: 'openai/gpt-5.6-luna', speed: 'fast' },
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
consumers: [
|
|
210
|
+
{
|
|
211
|
+
name: 'page-on-call',
|
|
212
|
+
types: ['ticket.flagged'],
|
|
213
|
+
run: async ({ event }) => {
|
|
214
|
+
// your side effect; event.id makes a stable idempotency key:
|
|
215
|
+
// await notifyOnCall(event.payload.ticketId, { idempotencyKey: event.id })
|
|
216
|
+
},
|
|
211
217
|
},
|
|
212
|
-
|
|
218
|
+
],
|
|
213
219
|
})
|
|
214
220
|
```
|
|
215
221
|
|
|
@@ -277,7 +283,7 @@ export const POST = handle
|
|
|
277
283
|
Server appends, such as
|
|
278
284
|
`supportDeskAgent.session(id).append({ type: 'plan.changed', payload })`, are
|
|
279
285
|
trusted and skip `authorize`. `push()` also accepts any `inputs` builder, and
|
|
280
|
-
`
|
|
286
|
+
`consumerContext(supportDesk)` types application events inside tools.
|
|
281
287
|
|
|
282
288
|
The contract-free client, `createAgentClient()` or `useAgent({ api | client })`
|
|
283
289
|
with a type-only import, folds only the built-in reducer. It accepts
|
|
@@ -285,10 +291,11 @@ with a type-only import, folds only the built-in reducer. It accepts
|
|
|
285
291
|
no `reducer`; the types enforce this through `AI_STATE_REDUCER`.
|
|
286
292
|
|
|
287
293
|
When the agent belongs inside something larger, such as a shared chat room,
|
|
288
|
-
keep two sessions.
|
|
289
|
-
to the agent's session with `inputs.message()`.
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
keep two sessions. A consumer of the room appends each message the agent should
|
|
295
|
+
see to the agent's session with `inputs.message()`. Derive the message ID from
|
|
296
|
+
the room event's ID with `idempotentId()`, so a retried run converges on the
|
|
297
|
+
same message. Event IDs are unique per session, so when several rooms feed one
|
|
298
|
+
agent session, include the room's session ID too. A tool
|
|
292
299
|
the agent calls to speak appends to the room with an `idempotentId()` of its
|
|
293
300
|
`toolCallId`. Neither fold reads the other's state.
|
|
294
301
|
|
|
@@ -369,8 +376,12 @@ completed input.
|
|
|
369
376
|
|
|
370
377
|
## Delivery semantics
|
|
371
378
|
|
|
372
|
-
|
|
373
|
-
|
|
379
|
+
The agent runs on four of A2's own at-least-once
|
|
380
|
+
[consumers](/server/consumers): `a2.ai.control` applies commands and work
|
|
381
|
+
reports one at a time, `a2.ai.model` runs model steps on one lane, `a2.ai.tools` runs local
|
|
382
|
+
tools, and `a2.ai.metadata` looks up model limits. A dead letter names the one
|
|
383
|
+
that failed. A successful completion commits atomically with its terminal
|
|
384
|
+
events, and deterministic IDs
|
|
374
385
|
make scheduling and lifecycle appends effectively once. An incomplete model
|
|
375
386
|
attempt is marked `superseded` before its replacement starts. An expired claim
|
|
376
387
|
or superseded attempt keeps its abort reason through model, resolver,
|
|
@@ -379,7 +390,7 @@ attempt; an explicit interruption stays terminal. A retried tool can stream
|
|
|
379
390
|
different preliminary output under the same tool-call ID.
|
|
380
391
|
|
|
381
392
|
Provider calls and external side effects sit outside that transaction, so
|
|
382
|
-
recovery may repeat a call whose effect landed before its
|
|
393
|
+
recovery may repeat a call whose effect landed before its run committed.
|
|
383
394
|
Give side-effecting tools their own idempotency, normally keyed by the AI SDK
|
|
384
395
|
`toolCallId`. A fast tool can also start before its model step fails; the
|
|
385
396
|
failure blocks the continuation but cannot undo the effect.
|
|
@@ -499,11 +510,11 @@ full handoff rules.
|
|
|
499
510
|
New-input commands from `inputs.message()`, `inputs.seed()`, and
|
|
500
511
|
`inputs.steer()` use the message ID as their event ID, and hand-written commands
|
|
501
512
|
must too. Caller IDs cannot start with the reserved `a2.ai:` prefix and must be
|
|
502
|
-
unique
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
513
|
+
unique within the session; the same ID in another session is another message.
|
|
514
|
+
An identical submission replays without new work, even after removal or
|
|
515
|
+
compaction. Reusing an ID in the session with different content or action
|
|
516
|
+
rejects with `PARTIAL_DUPLICATE_BATCH`: use `queue.edit()` to change a pending
|
|
517
|
+
input and a fresh ID for a new one.
|
|
507
518
|
|
|
508
519
|
### Optimistic conversation display
|
|
509
520
|
|
|
@@ -530,7 +541,7 @@ application, with `commandId` (the submitted event's ID) and an `outcome` of
|
|
|
530
541
|
in flight, each action resolves to its persisted command events, whose `id` is
|
|
531
542
|
the receipt's `commandId`, or to an empty array when there is nothing to
|
|
532
543
|
address, such as Stop with no active turn. A rejection is a normal result and
|
|
533
|
-
spends no
|
|
544
|
+
spends no retry.
|
|
534
545
|
|
|
535
546
|
### Pause and resume
|
|
536
547
|
|
|
@@ -31,7 +31,7 @@ everything it can call:
|
|
|
31
31
|
import { tool } from 'ai'
|
|
32
32
|
import { Sandbox } from '@vercel/sandbox'
|
|
33
33
|
import { z } from 'zod'
|
|
34
|
-
import {
|
|
34
|
+
import { consumerContext } from 'experimental-a2/ai'
|
|
35
35
|
import { agent } from 'experimental-a2/ai/server'
|
|
36
36
|
import { fx } from 'experimental-a2/ai/fx'
|
|
37
37
|
|
|
@@ -45,7 +45,7 @@ export const coder = agent({
|
|
|
45
45
|
description: 'Run a shell command in the repository.',
|
|
46
46
|
inputSchema: z.object({ command: z.string() }),
|
|
47
47
|
execute: async ({ command }) => {
|
|
48
|
-
const { session, signal } =
|
|
48
|
+
const { session, signal } = consumerContext()
|
|
49
49
|
// One Vercel Sandbox per session, created on first use.
|
|
50
50
|
const sandbox = await Sandbox.getOrCreate({
|
|
51
51
|
name: session.id,
|
|
@@ -125,7 +125,8 @@ turn, so a process keeps the images it fetched instead of fetching them again.
|
|
|
125
125
|
|
|
126
126
|
A file that isn't an image, or that A2 can't fetch, fails the turn with an
|
|
127
127
|
error that names it. So does a steer with files under FX, which steers with
|
|
128
|
-
text only.
|
|
128
|
+
text only. A steer that joins the turn's prompt, as described below, sends its
|
|
129
|
+
files with the prompt. The harness applies its own rules to the images it gets: FX fails a
|
|
129
130
|
turn whose model doesn't take images, and Pi resizes them. Every step sends the
|
|
130
131
|
conversation so far, files and all, so a file that fails one turn fails the
|
|
131
132
|
turns after it too.
|
|
@@ -169,14 +170,20 @@ sets `needsApproval` is rejected. It hands the engine only tools A2 runs, so a
|
|
|
169
170
|
provider-defined tool, such as a provider's web search, is rejected too. `maxSteps` limits the model calls in one
|
|
170
171
|
response under every harness. Steering enters at the next step, as with any
|
|
171
172
|
agent: the harness sees the steered message before its next model call.
|
|
173
|
+
Steering that hands off before any of the turn's model calls finished, as after
|
|
174
|
+
a pause during the first one, joins the turn's prompt instead, so the turn's
|
|
175
|
+
first model call sees it.
|
|
172
176
|
|
|
173
177
|
## Recovery
|
|
174
178
|
|
|
175
179
|
A2 records each model call and each tool result as the harness works. When a
|
|
176
180
|
process dies in the middle of a response, the next attempt resumes at the
|
|
177
181
|
interrupted step: finished model calls are not made again and finished tools
|
|
178
|
-
do not run again. A2
|
|
179
|
-
last response resumes the harness from its state at the end of that
|
|
180
|
-
FX's checkpoint or Pi's session, kept in memory, and any other
|
|
181
|
-
the session from the log.
|
|
182
|
+
do not run again. A2 keeps nothing durable outside the log: a process that ran
|
|
183
|
+
the last response resumes the harness from its state at the end of that
|
|
184
|
+
response, FX's checkpoint or Pi's session, kept in memory, and any other
|
|
185
|
+
process replays the session from the log. Under `fx()`, the process that ran a
|
|
186
|
+
step also keeps FX's kernel waiting on that step's tool calls until the session
|
|
187
|
+
has nothing left to run there, so the response's next step, or the first step
|
|
188
|
+
of a response that steers it, continues it without a replay. A tool call that a crash cut short reruns unless its
|
|
182
189
|
`toolRecovery` is `'report'`; see [Delivery semantics](/agents/conversations#delivery-semantics).
|
|
@@ -13,7 +13,7 @@ replay value, and a lost update is repainted by the next one. The test:
|
|
|
13
13
|
> passes? Yes: event, in the log. No: presence.
|
|
14
14
|
|
|
15
15
|
Messages, document updates, cancellations: events. Cursors, selections,
|
|
16
|
-
typing, "who's online", a
|
|
16
|
+
typing, "who's online", a consumer's live progress: presence.
|
|
17
17
|
|
|
18
18
|
Presence rides the session you already have: same contract, route, auth, and
|
|
19
19
|
stream. A2 replicates each participant's latest field values to every
|
|
@@ -167,30 +167,34 @@ render-time rule:
|
|
|
167
167
|
The durable event at index 214 beats any presence stamped 213 or lower. A2
|
|
168
168
|
carries the stamp; the comparison is yours, in view code.
|
|
169
169
|
|
|
170
|
-
##
|
|
170
|
+
## Consumers broadcast too
|
|
171
171
|
|
|
172
|
-
`ctx.session.setPresence` is the same operation, so a
|
|
172
|
+
`ctx.session.setPresence` is the same operation, so a consumer can publish live
|
|
173
173
|
progress without writing non-facts to the log:
|
|
174
174
|
|
|
175
175
|
```ts server/canvas.ts
|
|
176
|
-
import { createServer } from 'experimental-a2/server'
|
|
176
|
+
import { consumer, createServer } from 'experimental-a2/server'
|
|
177
177
|
import { canvas } from '@/contracts'
|
|
178
178
|
|
|
179
|
+
const thumbnails = consumer({
|
|
180
|
+
contract: canvas,
|
|
181
|
+
name: 'thumbnails',
|
|
182
|
+
types: ['shape.created'],
|
|
183
|
+
}).run(async (ctx) => {
|
|
184
|
+
// long work; broadcast progress as it goes:
|
|
185
|
+
// await ctx.session.setPresence({
|
|
186
|
+
// participant: 'system:thumbnails',
|
|
187
|
+
// values: { progress: { step: 2, of: 5 } },
|
|
188
|
+
// })
|
|
189
|
+
})
|
|
190
|
+
|
|
179
191
|
export const canvasServer = createServer({
|
|
180
192
|
contract: canvas,
|
|
181
|
-
|
|
182
|
-
'shape.created': async (ctx) => {
|
|
183
|
-
// long work; broadcast progress as it goes:
|
|
184
|
-
// await ctx.session.setPresence({
|
|
185
|
-
// participant: 'system:thumbnails',
|
|
186
|
-
// values: { progress: { step: 2, of: 5 } },
|
|
187
|
-
// })
|
|
188
|
-
},
|
|
189
|
-
},
|
|
193
|
+
consumers: [thumbnails],
|
|
190
194
|
})
|
|
191
195
|
```
|
|
192
196
|
|
|
193
|
-
The participant id is the
|
|
197
|
+
The participant id is the consumer's choice; a `system:` prefix keeps robots
|
|
194
198
|
apart from humans in the map. When the work completes, append the outcome as
|
|
195
199
|
an event: the progress was never a fact, the result is.
|
|
196
200
|
|
|
@@ -33,7 +33,7 @@ schema for its payload. These docs use Zod, but any
|
|
|
33
33
|
your own.
|
|
34
34
|
|
|
35
35
|
The contract is a plain, importable, **isomorphic** value. The server
|
|
36
|
-
implements it with [
|
|
36
|
+
implements it with [consumers](/server/consumers), [reducers](/contract/state)
|
|
37
37
|
derive the current view from it, and the browser types its pushes off it.
|
|
38
38
|
|
|
39
39
|
## Validation
|
|
@@ -52,6 +52,6 @@ typing, live progress) that is replicated but never stored in the log. See
|
|
|
52
52
|
:::tip
|
|
53
53
|
Events are facts, so name them in past tense: `created`, `expired`,
|
|
54
54
|
`shop.notified`. A name that reads like a command (`notifyShop`) describes
|
|
55
|
-
what a
|
|
55
|
+
what a consumer should do, not what happened. Dots are a naming convention, not
|
|
56
56
|
a namespace.
|
|
57
57
|
:::
|
|
@@ -11,8 +11,8 @@ const events = await ordersServer.session(orderId).history()
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Every event in the session, oldest first: always the raw log, never a summary
|
|
14
|
-
or snapshot. Use it for audits, debugging, and
|
|
15
|
-
shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
|
|
14
|
+
or snapshot. Use it for audits, debugging, and consumer questions like "did
|
|
15
|
+
the shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
|
|
16
16
|
|
|
17
17
|
## Reducers
|
|
18
18
|
|
|
@@ -72,7 +72,7 @@ server-only app can keep the contract next to `createServer` instead.
|
|
|
72
72
|
|
|
73
73
|
## Read boundaries
|
|
74
74
|
|
|
75
|
-
A root session reads the latest committed frontier. A
|
|
75
|
+
A root session reads the latest committed frontier. A consumer's session stops
|
|
76
76
|
at its triggering event by default, stable across concurrent appends and
|
|
77
77
|
retries:
|
|
78
78
|
|
|
@@ -83,23 +83,27 @@ import { ordersReducer } from './reducer'
|
|
|
83
83
|
|
|
84
84
|
export const ordersServer = createServer({
|
|
85
85
|
contract: orders,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
consumers: [
|
|
87
|
+
{
|
|
88
|
+
name: 'read-status',
|
|
89
|
+
types: ['created'],
|
|
90
|
+
run: async ({ event, session }) => {
|
|
91
|
+
const atTrigger = await session.state(ordersReducer)
|
|
92
|
+
// atTrigger.index === event.index
|
|
93
|
+
|
|
94
|
+
const latest = await session.state(ordersReducer, { through: 'latest' })
|
|
95
|
+
// latest also folds events appended after this one
|
|
96
|
+
},
|
|
93
97
|
},
|
|
94
|
-
|
|
98
|
+
],
|
|
95
99
|
})
|
|
96
100
|
```
|
|
97
101
|
|
|
98
102
|
Use `{ through: 'latest' }` for joins or other logic that intentionally
|
|
99
103
|
observes later events. A numeric `{ through: index }` reads any inclusive log
|
|
100
104
|
boundary; `through: 0` returns `initialState`. `state()` is observational: it
|
|
101
|
-
never runs
|
|
102
|
-
history, not
|
|
105
|
+
never runs consumers or waits for pending work, and its index marks committed
|
|
106
|
+
history, not consumer progress.
|
|
103
107
|
|
|
104
108
|
A latest read and a following `ctx.session.append(name, ...events)` are
|
|
105
109
|
separate operations, and concurrent appends and retries can move the frontier
|
|
@@ -125,7 +129,7 @@ telemetry span (or `console.error` by default) without snapshot contents, and
|
|
|
125
129
|
the next read folds the missing tail again.
|
|
126
130
|
|
|
127
131
|
Each session and reducer has one latest snapshot. Older ones exist only while
|
|
128
|
-
|
|
132
|
+
consumer runs that read at their boundary are unfinished (not yet completed
|
|
129
133
|
or dead-lettered). This is automatic: there is no TTL, history limit, or
|
|
130
134
|
retention setting.
|
|
131
135
|
|
|
@@ -4,7 +4,7 @@ description: "A session is one instance of a contract, with its own ordered log,
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
```ts
|
|
7
|
-
// anywhere on the server: a route, a
|
|
7
|
+
// anywhere on the server: a route, a consumer, a script
|
|
8
8
|
const order = ordersServer.session('order-42')
|
|
9
9
|
```
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ Put events in one session when they form one history, fold, recovery unit, or
|
|
|
19
19
|
live view. Split what should be read and recovered independently.
|
|
20
20
|
|
|
21
21
|
A session is not a table or a general query boundary: one `orders` contract can
|
|
22
|
-
serve millions of order sessions through the same server and
|
|
22
|
+
serve millions of order sessions through the same server and consumers.
|
|
23
23
|
Collections, joins, search, and constraints across many records belong in
|
|
24
24
|
[your database](#sessions-and-your-database).
|
|
25
25
|
|
|
@@ -48,7 +48,7 @@ same atomic step that appends the marker; if another event landed first, the
|
|
|
48
48
|
clear skips and returns `{ cleared: false, index }` with the current head. A
|
|
49
49
|
conditional clear is safe to retry.
|
|
50
50
|
|
|
51
|
-
The old lifetime's pending events are never dispatched, and a
|
|
51
|
+
The old lifetime's pending events are never dispatched, and a consumer still
|
|
52
52
|
running for one can't append, return events, or read the new lifetime's state.
|
|
53
53
|
Clearing can't stop code that is already running or undo its side effects.
|
|
54
54
|
|
|
@@ -70,7 +70,7 @@ authorize your own route before calling it.
|
|
|
70
70
|
|
|
71
71
|
### Scheduling a clear
|
|
72
72
|
|
|
73
|
-
With a [scheduler](/schedulers/overview) configured, a
|
|
73
|
+
With a [scheduler](/schedulers/overview) configured, a consumer can schedule a
|
|
74
74
|
clear. This one clears an idle cart a day after its last activity:
|
|
75
75
|
|
|
76
76
|
```ts server/carts.ts
|
|
@@ -89,20 +89,24 @@ export const carts = a2.contract({
|
|
|
89
89
|
export const cartsServer = createServer({
|
|
90
90
|
contract: carts,
|
|
91
91
|
scheduler: vercelQueues(),
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
consumers: [
|
|
93
|
+
{
|
|
94
|
+
name: 'clear-idle',
|
|
95
|
+
types: ['item.added'],
|
|
96
|
+
run: async ({ event, session }) => {
|
|
97
|
+
await session.schedule.clear('idle', {
|
|
98
|
+
delay: '1d',
|
|
99
|
+
ifLastIndex: event.index,
|
|
100
|
+
})
|
|
101
|
+
},
|
|
98
102
|
},
|
|
99
|
-
|
|
103
|
+
],
|
|
100
104
|
})
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
The delay counts from the event's stored time. A later event moves the head
|
|
104
|
-
past `event.index`, so this delivery skips and that event's
|
|
105
|
-
|
|
108
|
+
past `event.index`, so this delivery skips and that event's run schedules the
|
|
109
|
+
next check; only events the consumer takes extend the policy. A consumer's own
|
|
106
110
|
writes move the head too, so one that appends should pass the index its last
|
|
107
111
|
append returned.
|
|
108
112
|
|
|
@@ -148,27 +152,30 @@ attached to it:
|
|
|
148
152
|
The investigation's session refers to the issue id without becoming a second
|
|
149
153
|
owner of the issue. A2 can own individual issues when their history and live
|
|
150
154
|
state are the center of the product, but boards and search then span many
|
|
151
|
-
sessions and need another query model.
|
|
152
|
-
session
|
|
153
|
-
client
|
|
155
|
+
sessions and need another query model. A whole workspace can live in one
|
|
156
|
+
session when its pages read from [your tables](/server/sql-projections) instead
|
|
157
|
+
of folding the log, but every client following that session's live stream
|
|
158
|
+
receives every event, so weigh that traffic before many people share one.
|
|
154
159
|
|
|
155
|
-
### Database writes from
|
|
160
|
+
### Database writes from consumers
|
|
156
161
|
|
|
157
|
-
|
|
162
|
+
Consumers may call your database like any other side effect. They are
|
|
158
163
|
at-least-once, so make the write idempotent with `event.id`; it runs after the
|
|
159
164
|
event is durable and may be retried. That is not one atomic write across A2
|
|
160
165
|
and your tables: if a relational change must commit before the request
|
|
161
166
|
succeeds, make it through the database's write path.
|
|
162
167
|
|
|
163
|
-
###
|
|
168
|
+
### Tables kept from the logs
|
|
164
169
|
|
|
165
|
-
A
|
|
166
|
-
list of active imports.
|
|
170
|
+
A consumer can keep a table in your database from A2-owned sessions, such as a
|
|
171
|
+
list of active imports. Committing its position in the same transaction as the
|
|
172
|
+
rows applies each event exactly once; [SQL projections](/server/sql-projections)
|
|
173
|
+
shows how. The table:
|
|
167
174
|
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
|
|
175
|
+
- is eventually consistent with the session logs;
|
|
176
|
+
- has each session's events in order, not an order across sessions;
|
|
177
|
+
- fills from history with `backfill`, one session at a time, as each next
|
|
178
|
+
runs: nothing enumerates sessions, so to fill it for idle ones, consume each.
|
|
172
179
|
|
|
173
180
|
Back up and migrate it like application data. If search, joins, and
|
|
174
181
|
cross-session reporting are central, the database should probably own that
|