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
|
@@ -5,61 +5,51 @@ description: "`experimental-a2/server`: `createServer`, its route, the store and
|
|
|
5
5
|
|
|
6
6
|
## `experimental-a2/server`
|
|
7
7
|
|
|
8
|
-
### `
|
|
9
|
-
|
|
10
|
-
Normalizes a handler entry so you can compose an existing handler without
|
|
11
|
-
branching on its shape. For ordinary use, define handlers inline in
|
|
12
|
-
`createServer({ handlers })`.
|
|
13
|
-
|
|
14
|
-
- Accepts a function or `{ handler, lane?, abortOn? }` and returns the object
|
|
15
|
-
shape: a function becomes `{ handler: entry }`, an object is returned
|
|
16
|
-
unchanged, and both keep their original types.
|
|
17
|
-
- Does not bind a contract; `createServer({ handlers })` checks the match.
|
|
18
|
-
Annotate a new handler's context, as below, or pass an already typed
|
|
19
|
-
function.
|
|
20
|
-
- Spreading the result keeps `lane` and `abortOn`; set either after the spread
|
|
21
|
-
to override it. Changing a built-in AI handler's lane can separate it from
|
|
22
|
-
the handlers it coordinates with.
|
|
23
|
-
- A composition runs in one handler attempt. Work after the original resolves
|
|
24
|
-
still runs before its returned events commit, a throw fails the composed
|
|
25
|
-
attempt, and a retry can run both again.
|
|
26
|
-
|
|
27
|
-
```ts server/composed-orders.ts
|
|
28
|
-
import * as a2 from 'experimental-a2'
|
|
29
|
-
import { createServer, handler, type HandlerContext } from 'experimental-a2/server'
|
|
30
|
-
import { z } from 'zod'
|
|
31
|
-
|
|
32
|
-
const orders = a2.contract({
|
|
33
|
-
name: 'composed-orders',
|
|
34
|
-
events: {
|
|
35
|
-
created: z.object({ shopId: z.string() }),
|
|
36
|
-
notified: z.object({ shopId: z.string() }),
|
|
37
|
-
},
|
|
38
|
-
})
|
|
8
|
+
### `consumer(options)`
|
|
39
9
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
10
|
+
```ts
|
|
11
|
+
consumer(options: {
|
|
12
|
+
contract: Contract // the contract whose events it reads
|
|
13
|
+
name: string // its identity, unique per server
|
|
14
|
+
types?: readonly EventType[] // default: every type
|
|
15
|
+
lane?: string | ((ctx: LaneContext) => string | null) // default: concurrent
|
|
16
|
+
backfill?: boolean // default false; needs a string lane, and no abortOn
|
|
17
|
+
abortOn?: AbortSpec
|
|
18
|
+
}): { run(fn: (ctx: Context) => Promise<void | AppendInput | readonly AppendInput[]>): Consumer }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Defines a named reader of every session's log. `.run()` takes the function and
|
|
22
|
+
returns the `Consumer` you pass to `createServer({ consumers })` or
|
|
23
|
+
`session.consume()`. In `consumers`, the same options without `contract`, and
|
|
24
|
+
`run`, define one inline on the server's contract. See
|
|
25
|
+
[Consumers](/server/consumers).
|
|
26
|
+
|
|
27
|
+
| Option | Behavior |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `name` | 1-128 letters, digits, and `. _ : -`, starting with a letter or digit. Cursors, failures, dead letters, and child event ids key on it, so a new name is a new consumer. Names starting with `a2.` are reserved. |
|
|
30
|
+
| `types` | The event types it runs on, at least one; `ctx.event` narrows to them. Other events start no work for it. Leave it out to take every type. |
|
|
31
|
+
| `lane` | Omitted: every event runs concurrently on its own cursor. A string: one cursor runs every event in log order. A function of `{ sessionId, event }`: the event's lane, or `null` to run it concurrently. Resolved and stored at append. |
|
|
32
|
+
| `backfill` | `true` starts a new consumer at the beginning of the log instead of at the first event appended after it exists. Needs a string `lane` and no `abortOn`; anything else throws a `TypeError`. |
|
|
33
|
+
| `abortOn` | Events that fire `ctx.signal` while `run` works: an array matches by type; an object maps types to predicates `(event, trigger, { attempt })`, whose `attempt` is the trigger's durable attempt, so a cancellation can fence one recovered run. See [Cancellation](/server/cancellation). Not with `backfill`: cancellation is for work in progress, and checking it while replaying history would read the rest of the log for every event. |
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// consumer options: a cancel that names one recovered run
|
|
37
|
+
abortOn: {
|
|
38
|
+
cancelled: (event, trigger, { attempt }) =>
|
|
39
|
+
event.payload.of === `${trigger.id}:${attempt}`,
|
|
40
|
+
},
|
|
61
41
|
```
|
|
62
42
|
|
|
43
|
+
- `run` receives the context below, with `ctx.attempt` counting claims of this
|
|
44
|
+
event for this consumer. Returned events commit with the cursor's advance;
|
|
45
|
+
`ctx.session.append` commits immediately. Both derive ids of events without
|
|
46
|
+
an `id` from the trigger and the consumer's name.
|
|
47
|
+
- An invalid name, an empty or unknown type list, or `backfill` with a non-string lane or with `abortOn` throws
|
|
48
|
+
a `TypeError` before any I/O. `createServer` throws on two consumers with
|
|
49
|
+
one name or a consumer of another contract.
|
|
50
|
+
- `Consumer`, `ConsumerOptions`, `ConsumerLane`, `ConsumerResult`, and
|
|
51
|
+
`AbortSpec` are exported from `experimental-a2/server`.
|
|
52
|
+
|
|
63
53
|
### `createServer(options)`
|
|
64
54
|
|
|
65
55
|
```ts
|
|
@@ -69,65 +59,42 @@ createServer(options: {
|
|
|
69
59
|
scheduler?: A2Scheduler
|
|
70
60
|
telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
|
|
71
61
|
presence?: { ttlMs?: number } // presence expiry policy; default 60s
|
|
72
|
-
|
|
73
|
-
[type]:
|
|
74
|
-
| Handler
|
|
75
|
-
| {
|
|
76
|
-
abortOn?: AbortSpec
|
|
77
|
-
lane?: string | ((ctx: LaneContext) => string)
|
|
78
|
-
handler: Handler
|
|
79
|
-
}
|
|
80
|
-
}
|
|
62
|
+
consumers?: readonly (Consumer | InlineConsumer)[] // see consumer(options)
|
|
81
63
|
}): A2Server
|
|
82
|
-
|
|
83
|
-
type Handler = (
|
|
84
|
-
ctx: Context,
|
|
85
|
-
) => Promise<void | AppendInput | readonly AppendInput[]>
|
|
86
64
|
```
|
|
87
65
|
|
|
88
66
|
Implements a contract: binds its vocabulary to storage and reactions.
|
|
89
67
|
`experimental-a2/server` is the only entry point that can reach a store
|
|
90
68
|
backend; under the browser condition its exports resolve to a loud error. See
|
|
91
|
-
[
|
|
69
|
+
[Consumers](/server/consumers). The options type is `ServerOptions`, exported
|
|
70
|
+
from `experimental-a2/server`.
|
|
92
71
|
|
|
93
72
|
| Option | Behavior |
|
|
94
73
|
| --- | --- |
|
|
95
|
-
| `
|
|
96
|
-
| `lane` | Within one session, same-lane events run one at a time in log order; all others run concurrently. A resolver receives `{ sessionId, event }`. The value is resolved and stored at append, so a later deployment can't reinterpret pending work. See [Lanes](/server/handlers#lanes). |
|
|
97
|
-
| `abortOn` | Events that fire `ctx.signal` while the handler runs: an array matches by type; an object maps types to predicates `(event, trigger, { attempt })`, whose `attempt` is the trigger's durable attempt, so a cancellation can fence one recovered run. See [Cancellation](/server/cancellation). |
|
|
74
|
+
| `consumers` | The consumers this server runs, each with its own cursors in every session: a `Consumer` from `consumer()`, or `{ name, types?, lane?, backfill?, abortOn?, run }` inline, whose `run` narrows to its `types`. Names must be unique, and every consumer must read this server's contract; otherwise construction throws a `TypeError`. Omit it for a history-only server. |
|
|
98
75
|
| `presence.ttlMs` | How long a presence value survives without a refreshing set: a positive integer of milliseconds, default 60 seconds, counted on the storage's clock, never the sender's stamp. Only for contracts that declare `presence`; elsewhere construction throws a `TypeError`. |
|
|
99
76
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- Handlers are concurrent by default. A2 starts every eligible event once its
|
|
111
|
-
append commits.
|
|
112
|
-
- A handler returns nothing, one event, or an array. A2 marks the trigger
|
|
113
|
-
processed and appends the returned batch in one atomic store operation; when
|
|
114
|
-
the handler throws, the returned events never exist.
|
|
115
|
-
- Handlers are optional per event type. An event type without one settles in
|
|
116
|
-
its append transaction with no dispatch attempt, and starts no drain or
|
|
117
|
-
scheduler arm when the session has no older pending handler work. See
|
|
118
|
-
[Events without handlers](/server/handlers#events-without-handlers).
|
|
77
|
+
- A2 starts every eligible event once its append commits. A consumer without
|
|
78
|
+
a lane runs its events concurrently.
|
|
79
|
+
- `run` returns nothing, one event, or an array. A2 moves the consumer's
|
|
80
|
+
cursor past the trigger and appends the returned batch in one atomic store
|
|
81
|
+
operation; when `run` throws, the returned events never exist.
|
|
82
|
+
- An event no consumer takes starts no work: no dispatch attempt, and no drain
|
|
83
|
+
or scheduler arm when the session has no older pending work. See
|
|
84
|
+
[Which events it takes](/server/consumers#which-events-it-takes).
|
|
119
85
|
|
|
120
|
-
The context every
|
|
86
|
+
The context every consumer's `run` receives, `ConsumerContext`:
|
|
121
87
|
|
|
122
88
|
| Property | Meaning |
|
|
123
89
|
| --- | --- |
|
|
124
90
|
| `ctx.event` | `Event`: the triggering event |
|
|
125
|
-
| `ctx.attempt` | durable 1-based
|
|
126
|
-
| `ctx.session` | this session: `id` (equal to `ctx.event.sessionId`), `append`, `schedule`, `clear`, `history`, `state`, `stream`, `setPresence` |
|
|
91
|
+
| `ctx.attempt` | durable 1-based claim ordinal of this event for this consumer; increments on every durable claim and may skip when a process dies before `run` |
|
|
92
|
+
| `ctx.session` | this session: `id` (equal to `ctx.event.sessionId`), `append`, `schedule`, `clear`, `history`, `state`, `stream`, `consume`, `setPresence` |
|
|
127
93
|
| `ctx.signal` | `AbortSignal`: fires on lease lapse (`CLAIM_EXPIRED`), proven supersession (`SUPERSEDED_ATTEMPT`), and matching `abortOn` events |
|
|
128
94
|
|
|
129
|
-
`
|
|
130
|
-
|
|
95
|
+
`ConsumerContext`, `ConsumerAppend`, and `LaneContext` are exported from
|
|
96
|
+
`experimental-a2/server`. `ctx.session` shares the `server.session(id)`
|
|
97
|
+
surface; consumer-specific behavior is noted under each method. Its `state(reducer)` folds through the
|
|
131
98
|
trigger's index by default (see
|
|
132
99
|
[`session.state`](#sessionstatereducer-options)), and its `append` takes a
|
|
133
100
|
name:
|
|
@@ -136,12 +103,12 @@ name:
|
|
|
136
103
|
ctx.session.append(name: string, ...events: AppendInput[]): Promise<Event[]>
|
|
137
104
|
```
|
|
138
105
|
|
|
139
|
-
- It commits immediately, so its events may run while
|
|
140
|
-
|
|
141
|
-
[Append what should happen now](/server/
|
|
106
|
+
- It commits immediately, so its events may run while `run` is still active
|
|
107
|
+
unless a lane orders them. See
|
|
108
|
+
[Append what should happen now](/server/consumers#append-what-should-happen-now).
|
|
142
109
|
- The name is the idempotency key, scoped to the trigger, for events whose
|
|
143
|
-
`id` is omitted: each such id derives from the trigger id, the
|
|
144
|
-
event's position. The same name and generated-id batch returns the existing
|
|
110
|
+
`id` is omitted: each such id derives from the trigger id, the consumer's
|
|
111
|
+
name, the append's name, and the event's position. The same name and generated-id batch returns the existing
|
|
145
112
|
rows on retry; a changed batch conflicts with them.
|
|
146
113
|
- An explicit `id` wins, so different triggering events can converge on one
|
|
147
114
|
fact. Root `server.session(id).append(...events)` takes no name.
|
|
@@ -214,7 +181,7 @@ automatically.
|
|
|
214
181
|
- Authentication stays in the route, where headers, cookies, and framework
|
|
215
182
|
context are available. Capture the authenticated principal in `authorize`.
|
|
216
183
|
- `authorize` is an ingress policy for this `fetch` call, not a global server
|
|
217
|
-
policy: direct `session.append`,
|
|
184
|
+
policy: direct `session.append`, consumer appends, scheduled appends, and
|
|
218
185
|
server-side `setPresence` bypass it.
|
|
219
186
|
- By default the `sessionId` and `index` query parameters bind one session to
|
|
220
187
|
the socket. A2 authorizes the stream before upgrading, and frames can't
|
|
@@ -243,7 +210,7 @@ you append, read, or stream. `session.id` is the `id` passed in. See
|
|
|
243
210
|
|
|
244
211
|
It has every [session method](#session), plus
|
|
245
212
|
[`deadLetters()`](#sessiondeadletters) and
|
|
246
|
-
[`redrive()`](#
|
|
213
|
+
[`redrive()`](#sessionredrivetarget-options), which `ctx.session` doesn't.
|
|
247
214
|
|
|
248
215
|
### `server.drain(sessionId)`
|
|
249
216
|
|
|
@@ -251,43 +218,48 @@ It has every [session method](#session), plus
|
|
|
251
218
|
server.drain(sessionId: string): Promise<{ settled: boolean }>
|
|
252
219
|
```
|
|
253
220
|
|
|
254
|
-
Claims every eligible event and runs
|
|
221
|
+
Claims every eligible event for each consumer and runs them concurrently.
|
|
255
222
|
`settled` means no actionable or live-claimed work remains. Past the
|
|
256
223
|
invocation's deadline it claims nothing and returns `settled: false`. A dead-lettered
|
|
257
224
|
event can block later work in its lane while other lanes continue, until
|
|
258
|
-
[`session.redrive()`](#
|
|
225
|
+
[`session.redrive()`](#sessionredrivetarget-options) resolves it. Scheduler
|
|
259
226
|
callbacks use it; you'll rarely call it yourself. The public result stays this
|
|
260
227
|
simple boolean.
|
|
261
228
|
|
|
262
229
|
### `A2Store`
|
|
263
230
|
|
|
264
|
-
Custom store adapters
|
|
231
|
+
Custom store adapters keep the log and one cursor per (session, consumer,
|
|
232
|
+
lane). These are the dispatch methods:
|
|
265
233
|
|
|
266
234
|
```ts
|
|
267
235
|
// Custom server store adapter:
|
|
268
236
|
type EventCause = {
|
|
237
|
+
consumer: string
|
|
238
|
+
lane: string | null // null: a concurrent consumer's cursor of one event
|
|
269
239
|
index: number
|
|
270
240
|
attempt: number
|
|
271
|
-
batchSize?: number // present on named
|
|
241
|
+
batchSize?: number // present on named consumer appends
|
|
272
242
|
}
|
|
273
243
|
|
|
244
|
+
type EventTarget = { consumer: string; lane: string | null }
|
|
245
|
+
|
|
274
246
|
type AppendEvent = {
|
|
275
247
|
type: string
|
|
276
248
|
payload: unknown
|
|
277
249
|
id?: string
|
|
278
250
|
cause?: EventCause
|
|
279
|
-
|
|
280
|
-
settled?: true // internal: no handler is registered for this event type
|
|
251
|
+
targets?: readonly EventTarget[] // lane-function and concurrent consumers
|
|
281
252
|
}
|
|
282
253
|
|
|
283
254
|
type ReturnedEvent = AppendEvent & { id: string }
|
|
284
255
|
|
|
285
|
-
type StoredEvent = Event & {
|
|
286
|
-
|
|
256
|
+
type StoredEvent = Event & { cause: EventCause | null }
|
|
257
|
+
|
|
258
|
+
type HeadRef = { consumer: string; lane: string | null; index: number }
|
|
259
|
+
|
|
260
|
+
type HeadDispatch = {
|
|
261
|
+
consumer: string
|
|
287
262
|
lane: string | null
|
|
288
|
-
processedAt: Date | null
|
|
289
|
-
processedByAttempt: number | null
|
|
290
|
-
returnedEventIds: string[] | null
|
|
291
263
|
firstClaimedAt: Date | null
|
|
292
264
|
lastClaimedAt: Date | null
|
|
293
265
|
attemptCount: number
|
|
@@ -298,16 +270,25 @@ type StoredEvent = Event & {
|
|
|
298
270
|
lastFailedAttempt: number | null
|
|
299
271
|
lastError: string | null
|
|
300
272
|
failedAt: Date | null
|
|
301
|
-
clearIndex?: number // on claimed events, once the session has been cleared
|
|
302
273
|
}
|
|
303
274
|
|
|
304
|
-
type
|
|
305
|
-
|
|
306
|
-
|
|
275
|
+
type HeadEvent = StoredEvent &
|
|
276
|
+
HeadDispatch & {
|
|
277
|
+
clearIndex?: number // on claimed heads, once the session has been cleared
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type StoredCursor = HeadDispatch & { position: number; head: number | null }
|
|
281
|
+
|
|
282
|
+
type StoreConsumer = {
|
|
283
|
+
name: string
|
|
284
|
+
// a consumer with one string lane: its cursor reads the log by type
|
|
285
|
+
log?: { lane: string; types: readonly string[] | null; backfill: boolean }
|
|
307
286
|
}
|
|
308
287
|
|
|
288
|
+
type StoreAppendResult = { events: StoredEvent[]; hasPending: boolean }
|
|
289
|
+
|
|
309
290
|
type StoreClaimAvailableResult =
|
|
310
|
-
| { outcome: 'claimed'; events:
|
|
291
|
+
| { outcome: 'claimed'; events: HeadEvent[]; head?: number } // head: the session's head when the claim was made
|
|
311
292
|
| { outcome: 'busy'; retryAt: Date }
|
|
312
293
|
| { outcome: 'settled' }
|
|
313
294
|
|
|
@@ -319,9 +300,19 @@ interface A2Store {
|
|
|
319
300
|
append(
|
|
320
301
|
sessionId: string,
|
|
321
302
|
events: AppendEvent[],
|
|
322
|
-
options?: { originIndex?: number },
|
|
303
|
+
options?: { originIndex?: number; consumers?: readonly StoreConsumer[] },
|
|
323
304
|
): Promise<StoreAppendResult>
|
|
324
305
|
|
|
306
|
+
cursors(sessionId: string): Promise<StoredCursor[]>
|
|
307
|
+
|
|
308
|
+
// for session.consume(): create a missing log cursor (at the clear marker
|
|
309
|
+
// with backfill, else at the head), give an idle one its next event,
|
|
310
|
+
// leave one with a head alone; returns it and the session's head
|
|
311
|
+
wakeLog(options: {
|
|
312
|
+
sessionId: string
|
|
313
|
+
consumer: StoreConsumer & { log: NonNullable<StoreConsumer['log']> }
|
|
314
|
+
}): Promise<{ cursor: StoredCursor; head: number }>
|
|
315
|
+
|
|
325
316
|
// optional together; required for session.clear()
|
|
326
317
|
clear?(options: {
|
|
327
318
|
sessionId: string
|
|
@@ -331,11 +322,13 @@ interface A2Store {
|
|
|
331
322
|
reclaim?(sessionId: string): Promise<{ deleted: number; done: boolean }>
|
|
332
323
|
|
|
333
324
|
// optional together; required for session.deadLetters() and redrive()
|
|
334
|
-
deadLetters?(sessionId: string): Promise<
|
|
325
|
+
deadLetters?(sessionId: string): Promise<HeadEvent[]>
|
|
335
326
|
redrive?(options: {
|
|
336
327
|
sessionId: string
|
|
328
|
+
consumer: string
|
|
337
329
|
index: number
|
|
338
330
|
action: 'retry' | 'skip'
|
|
331
|
+
consumers?: readonly StoreConsumer[]
|
|
339
332
|
}): Promise<{ redriven: boolean; hasPending: boolean }>
|
|
340
333
|
|
|
341
334
|
claimAvailable(options: {
|
|
@@ -343,27 +336,33 @@ interface A2Store {
|
|
|
343
336
|
holder: string
|
|
344
337
|
ttlMs: number
|
|
345
338
|
expiresAtMs?: number
|
|
346
|
-
|
|
339
|
+
exclude?: readonly HeadRef[]
|
|
340
|
+
consumers?: readonly StoreConsumer[]
|
|
347
341
|
maxFailures?: number
|
|
348
342
|
}): Promise<StoreClaimAvailableResult>
|
|
349
343
|
|
|
350
344
|
renewClaims(options: {
|
|
351
345
|
sessionId: string
|
|
352
346
|
holder: string
|
|
353
|
-
claims: readonly
|
|
347
|
+
claims: readonly (HeadRef & { attempt: number })[]
|
|
354
348
|
ttlMs: number
|
|
355
349
|
expiresAtMs?: number
|
|
356
|
-
}): Promise<{ renewed:
|
|
350
|
+
}): Promise<{ renewed: HeadRef[]; superseded: HeadRef[] }>
|
|
357
351
|
|
|
358
352
|
completeAttempt(options: {
|
|
359
353
|
sessionId: string
|
|
354
|
+
consumer: string
|
|
355
|
+
lane: string | null
|
|
360
356
|
index: number
|
|
361
357
|
attempt: number
|
|
362
358
|
events: ReturnedEvent[]
|
|
359
|
+
consumers?: readonly StoreConsumer[]
|
|
363
360
|
}): Promise<CompleteAttemptResult>
|
|
364
361
|
|
|
365
362
|
failAttempt(options: {
|
|
366
363
|
sessionId: string
|
|
364
|
+
consumer: string
|
|
365
|
+
lane: string | null
|
|
367
366
|
index: number
|
|
368
367
|
attempt: number
|
|
369
368
|
error: string
|
|
@@ -376,17 +375,27 @@ interface A2Store {
|
|
|
376
375
|
}
|
|
377
376
|
```
|
|
378
377
|
|
|
378
|
+
A cursor's **head** is the next event it runs: the lowest unfinished event of
|
|
379
|
+
its lane, the event itself for a concurrent cursor, or for a log cursor the
|
|
380
|
+
next event after its position whose type it takes. The clear marker is never a
|
|
381
|
+
head. A head is eligible when no live claim or retry hold owns it, it is not
|
|
382
|
+
dead-lettered, and it lies above the latest clear marker. Moving a cursor past
|
|
383
|
+
its head resets the head's attempt and failure fields and releases its
|
|
384
|
+
snapshot pins. `consumers` names the consumers the caller serves: work for any
|
|
385
|
+
other consumer is neither pending nor claimed. Omitted, every cursor counts.
|
|
386
|
+
|
|
379
387
|
| Method | Atomic effect |
|
|
380
388
|
| --- | --- |
|
|
381
|
-
| `append` | Write a consecutive batch
|
|
382
|
-
| `
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
389
|
+
| `append` | Write a consecutive batch. Each event's `targets` start work: a lane target queues the event in that consumer's lane cursor, a null lane creates a cursor for that event alone. Each log consumer in `consumers` whose types the batch carries gets its cursor created (at the start of the log with `backfill`, otherwise just before the batch) or, when idle, a head. Report pending work for the served consumers as `hasPending`. An event with a `cause` commits only while that cursor's head is `cause.index`, `cause.attempt` is its latest attempt, and it isn't dead-lettered; otherwise throw `SUPERSEDED_ATTEMPT`. The check serializes against a concurrent claim, after the idempotent-replay path. |
|
|
390
|
+
| `cursors` | List every log and lane cursor, and every concurrent cursor that still has a head, by consumer, then lane, with concurrent cursors last by head. Lane and concurrent cursors whose work lies at or below the latest clear marker are not listed. |
|
|
391
|
+
| `claimAvailable` | Claim every eligible head of the served consumers, in event order, then by consumer. A claim records its holder, expiry, timestamps, and next `attemptCount`. Taking over a lapsed claim, one its holder never completed or failed, adds one to `failureCount`; when that reaches `maxFailures`, dead-letter the head instead of claiming it: advance `attemptCount` to fence the lapsed attempt, leave `lastFailedAttempt` unchanged, and return it with `failedAt` set. Excluded heads stay unclaimed and keep blocking their lane. A claim may report `head`, the session's head when the claim was made. |
|
|
392
|
+
| `renewClaims` | Extend the listed live claims the holder still owns. `renewed` lists those; `superseded` lists claims whose head a newer attempt took, or whose cursor moved past it. An expired claim no successor took is in neither and may still complete; renewal never revives it. |
|
|
393
|
+
| `completeAttempt` | In one transaction: fence on the current attempt, append the returned batch with its own targets, record the completion with its exact ordered child ids, and move the cursor past its head. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
|
|
394
|
+
| `failAttempt` | Record one current caught failure on the cursor, clear its claim, and dead-letter at `maxFailures`. Short of that, with `retryAfterMs` the head stays unclaimable for that long: set its claim expiry with no holder, so `claimAvailable` reports it `busy` until then. A repeated acknowledgment is idempotent; a stale attempt returns `superseded`. |
|
|
395
|
+
| `clear` | Serialized with `append`. Check `ifLastIndex` against the head and the latest marker, then append a clear marker at the next index. Log cursors count from the marker; lane and concurrent cursors at or below it are superseded. A `cause` is fenced like a consumer append. |
|
|
396
|
+
| `reclaim` | Delete one bounded batch of rows below the latest clear marker, with their cursor records, event-id records, and cached state. Report how many rows went, and `done` when none are left. Never wait on a lock other methods hold. |
|
|
397
|
+
| `deadLetters` | Return the dead-lettered heads above the latest clear marker, in event order, then by consumer. |
|
|
398
|
+
| `redrive` | Only for the consumer's dead-lettered head at `index` above the latest clear marker. `retry` clears `failedAt` and `failureCount`, leaving claims and the last failure. `skip` moves the cursor past it with no completion. Anything else reports `redriven: false` and writes nothing. Both serialize with claims and consumer appends; report `hasPending` like `append`. |
|
|
390
399
|
|
|
391
400
|
`readHistory(sessionId, options)` is an optional capability with the same
|
|
392
401
|
exclusive `afterIndex` and inclusive `throughIndex` bounds as `read()`. Without
|
|
@@ -396,14 +405,14 @@ A store that supports clearing treats every event at or below the latest
|
|
|
396
405
|
marker as superseded, whether or not its row still exists. Each check belongs
|
|
397
406
|
inside the operation's existing atomic step:
|
|
398
407
|
|
|
399
|
-
- `claimAvailable` never claims it
|
|
400
|
-
|
|
408
|
+
- `claimAvailable` never claims it, so a dead-lettered lane head stops
|
|
409
|
+
blocking; a new event in that lane becomes its head.
|
|
401
410
|
- `renewClaims` reports it in `superseded`; `completeAttempt` and
|
|
402
411
|
`failAttempt` return `superseded`.
|
|
403
412
|
- An `append` whose `cause` names it throws `SUPERSEDED_ATTEMPT`; one whose
|
|
404
413
|
`originIndex` is at or below the marker throws `SESSION_CLEARED`.
|
|
405
414
|
- Reads start at the marker; `readState` returns its index as `clearIndex`,
|
|
406
|
-
and claimed
|
|
415
|
+
and claimed heads carry it as `clearIndex`.
|
|
407
416
|
- In `append` and `completeAttempt`, an event id whose row is below this
|
|
408
417
|
session's marker is free to reuse.
|
|
409
418
|
|
|
@@ -414,15 +423,18 @@ stores support both.
|
|
|
414
423
|
|
|
415
424
|
Stored fields:
|
|
416
425
|
|
|
417
|
-
- `attemptCount` counts claims, including abandoned ones.
|
|
418
|
-
counts failed attempts: caught failures, and lapsed
|
|
419
|
-
took over. Ten dead-letter the
|
|
426
|
+
- `attemptCount` counts claims of the current head, including abandoned ones.
|
|
427
|
+
`failureCount` counts its failed attempts: caught failures, and lapsed
|
|
428
|
+
claims a later claim took over. Ten dead-letter the head. Both start over
|
|
429
|
+
when the cursor moves on. See
|
|
420
430
|
[Guarantees](/server/guarantees#one-event-many-attempts).
|
|
421
|
-
- `cause` names the event and
|
|
422
|
-
return value first persisted the child; `null`
|
|
423
|
-
|
|
424
|
-
- `
|
|
425
|
-
|
|
431
|
+
- `cause` names the consumer, lane, event, and attempt whose
|
|
432
|
+
`ctx.session.append` or return value first persisted the child; `null`
|
|
433
|
+
means a top-level event.
|
|
434
|
+
- `lane` is the cursor's lane, resolved before append; `null` is a concurrent
|
|
435
|
+
cursor of one event.
|
|
436
|
+
- A completion's recorded child ids make a lost acknowledgment recoverable
|
|
437
|
+
without accepting a partial child batch.
|
|
426
438
|
- Lifecycle timestamps are adapter clock values for their atomic operations,
|
|
427
439
|
not exact database commit times. Built-in adapters persist all these fields
|
|
428
440
|
inside their existing atomic operations, with no extra round trip.
|
|
@@ -438,7 +450,7 @@ Custom scheduler adapters implement two methods:
|
|
|
438
450
|
|
|
439
451
|
Application routes call `scheduler.handler(...servers)` on the configured
|
|
440
452
|
adapter. See
|
|
441
|
-
[
|
|
453
|
+
[Schedulers](/schedulers/overview).
|
|
442
454
|
|
|
443
455
|
`SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask |
|
|
444
456
|
SchedulerClearTask`. These task types, plus `ScheduledEvent`, are exported
|
|
@@ -453,7 +465,7 @@ from `experimental-a2/server`.
|
|
|
453
465
|
|
|
454
466
|
Drain tasks wake pending work and carry no application payload. Append tasks
|
|
455
467
|
carry the events for `session.schedule`; `originIndex` is the scheduling
|
|
456
|
-
|
|
468
|
+
consumer's trigger, absent for root schedules. Clear tasks carry the condition for
|
|
457
469
|
`session.schedule.clear`.
|
|
458
470
|
|
|
459
471
|
Deliver an authenticated, normalized append or clear task through the server
|
|
@@ -482,7 +494,7 @@ verifies that the task and server contract match.
|
|
|
482
494
|
|
|
483
495
|
- `deliverSchedulerAppend` revalidates the carried event inputs and uses the
|
|
484
496
|
ordinary top-level append. The target server must have a scheduler. When
|
|
485
|
-
the append leaves pending
|
|
497
|
+
the append leaves pending consumer work, it resolves only after a recovery
|
|
486
498
|
watchdog is durable, so a send failure keeps the delivery retryable. A task
|
|
487
499
|
whose `originIndex` was cleared resolves without appending.
|
|
488
500
|
- `deliverSchedulerClear` runs `clear({ ifLastIndex })` and resolves after its
|
|
@@ -507,9 +519,9 @@ The only way to move a session forward. See [Guarantees](/server/guarantees).
|
|
|
507
519
|
- An explicit `id` makes it idempotent across retries; re-sending an identical
|
|
508
520
|
batch returns the original rows.
|
|
509
521
|
- Events pushed through `server.fetch` arrive through this same append.
|
|
510
|
-
- Only
|
|
511
|
-
|
|
512
|
-
older
|
|
522
|
+
- Only events some consumer takes are dispatched; the append itself settles
|
|
523
|
+
the rest. An append no consumer takes still starts session healing when
|
|
524
|
+
older work is pending. The store reports that pending state in the
|
|
513
525
|
atomic append, so the decision needs no follow-up read.
|
|
514
526
|
|
|
515
527
|
### `session.append.dispatch(...events)`
|
|
@@ -522,10 +534,10 @@ session.append.dispatch(
|
|
|
522
534
|
|
|
523
535
|
Commits the same atomic batch, then sends pending work to the configured
|
|
524
536
|
scheduler instead of starting an inline drain. See
|
|
525
|
-
[Dispatch in a fresh invocation](/
|
|
537
|
+
[Dispatch in a fresh invocation](/schedulers/overview#dispatch-in-a-fresh-invocation).
|
|
526
538
|
|
|
527
539
|
- Awaits acceptance of an immediate scheduler message. A batch that leaves no
|
|
528
|
-
pending
|
|
540
|
+
pending consumer work sends none.
|
|
529
541
|
- Throws before writing when the server has no scheduler.
|
|
530
542
|
- If the send fails, the events are already durable: give them explicit IDs
|
|
531
543
|
and retry the same dispatch.
|
|
@@ -546,7 +558,7 @@ session.schedule(
|
|
|
546
558
|
```
|
|
547
559
|
|
|
548
560
|
Appends the events later through the configured scheduler, from root and
|
|
549
|
-
|
|
561
|
+
consumer sessions. Resolves after provider acceptance, not after append. `name`
|
|
550
562
|
must be non-empty, with at least one event. `ScheduleDelay`, `ScheduleTiming`,
|
|
551
563
|
and `SessionSchedule` are exported from `experimental-a2/server`. See
|
|
552
564
|
[Timers](/server/timers) for stale-timer guards, provider limits, and
|
|
@@ -554,7 +566,7 @@ deployment compatibility.
|
|
|
554
566
|
|
|
555
567
|
| Timing | Meaning |
|
|
556
568
|
| --- | --- |
|
|
557
|
-
| `{ delay: '30s' }` | relative delay with unit `ms`, `s`, `m`, `h`, or `d`; from the call's wall clock on a root session, from the trigger's durable `createdAt` in a
|
|
569
|
+
| `{ delay: '30s' }` | relative delay with unit `ms`, `s`, `m`, `h`, or `d`; from the call's wall clock on a root session, from the trigger's durable `createdAt` in a consumer, so retries keep the delivery time |
|
|
558
570
|
| `{ at: Date }` | absolute delivery time; use it when a retried root call needs one target |
|
|
559
571
|
|
|
560
572
|
- The template type checks the unit. At runtime the number must be an
|
|
@@ -563,9 +575,10 @@ deployment compatibility.
|
|
|
563
575
|
once are rejected.
|
|
564
576
|
- The target is the earliest useful delivery time. Adapters encode it in
|
|
565
577
|
whole-second slots; an already-due target is sent immediately.
|
|
566
|
-
- The task id derives from the contract, session, and name.
|
|
567
|
-
the trigger id and, once the session has
|
|
568
|
-
trigger id reused in a new lifetime schedules
|
|
578
|
+
- The task id derives from the contract, session, and name. A consumer's
|
|
579
|
+
tasks add the trigger id, the consumer's name, and, once the session has
|
|
580
|
+
been cleared, the clear, so a trigger id reused in a new lifetime schedules
|
|
581
|
+
a new task. Omitted event ids
|
|
569
582
|
derive from the task id and position; explicit ids win.
|
|
570
583
|
- Reuse a name only for identical timing and events. Provider deduplication is
|
|
571
584
|
an optimization; stable event ids make later duplicate deliveries converge in
|
|
@@ -575,11 +588,11 @@ deployment compatibility.
|
|
|
575
588
|
functions, symbols, cycles, accessors, and non-plain prototypes are rejected;
|
|
576
589
|
shared references become independent values.
|
|
577
590
|
- Delivery validates the snapshotted schema input through an ordinary
|
|
578
|
-
top-level append, outside any
|
|
579
|
-
transform produces the stored payload from the original input. The
|
|
580
|
-
have `cause: null` and run through
|
|
581
|
-
normally.
|
|
582
|
-
- A
|
|
591
|
+
top-level append, outside any consumer's completion transaction, so a
|
|
592
|
+
schema transform produces the stored payload from the original input. The
|
|
593
|
+
events have `cause: null` and run through consumers, lanes, state, and
|
|
594
|
+
streams normally.
|
|
595
|
+
- A consumer's task belongs to its trigger's lifetime: if the session is cleared
|
|
583
596
|
before delivery, it is acknowledged without appending. A root task joins
|
|
584
597
|
whichever lifetime exists at delivery, and its name identifies one task
|
|
585
598
|
across lifetimes, so put something from the new lifetime in the name when
|
|
@@ -611,10 +624,10 @@ and `index` is always the head after the call:
|
|
|
611
624
|
| otherwise | `{ cleared: true, index }`, the new marker |
|
|
612
625
|
|
|
613
626
|
- Every event at or below the marker is superseded: never dispatched again,
|
|
614
|
-
never blocking its lane.
|
|
627
|
+
never blocking its lane. A run still working on one has its appends throw
|
|
615
628
|
`SUPERSEDED_ATTEMPT` and its state reads throw `SESSION_CLEARED` rather than
|
|
616
629
|
fold the new lifetime. Exception: after a clear from another instance, a
|
|
617
|
-
read bounded at the
|
|
630
|
+
read bounded at the run's own event may return that event's state from
|
|
618
631
|
memory until its `ctx.signal` aborts.
|
|
619
632
|
- Pushes and appends after the marker start the new lifetime.
|
|
620
633
|
- `clear()` then deletes the old events, their bookkeeping and event-id
|
|
@@ -624,8 +637,8 @@ and `index` is always the head after the call:
|
|
|
624
637
|
- If deletion fails, the call rejects with the session already cleared; any
|
|
625
638
|
later clear of the session, including a retry, finishes the deletion.
|
|
626
639
|
- Old event ids can be used again. Presence is not cleared.
|
|
627
|
-
- On `ctx.session`, the clear is fenced like the
|
|
628
|
-
|
|
640
|
+
- On `ctx.session`, the clear is fenced like the run's appends and ends the
|
|
641
|
+
run's own attempt: `ctx.signal` aborts with `SUPERSEDED_ATTEMPT` and
|
|
629
642
|
returned events are discarded. Its deletion keeps running under the
|
|
630
643
|
platform's `waitUntil`.
|
|
631
644
|
- All built-in stores support clearing.
|
|
@@ -647,19 +660,19 @@ works as in `session.schedule`, with the same provider limits. See
|
|
|
647
660
|
[Scheduling a clear](/contract/sessions#scheduling-a-clear).
|
|
648
661
|
|
|
649
662
|
- The task id derives from the contract, session, name, and condition, plus
|
|
650
|
-
the trigger id and any earlier clear inside a
|
|
651
|
-
the same timing.
|
|
663
|
+
the trigger id, the consumer's name, and any earlier clear inside a
|
|
664
|
+
consumer. Reuse a name only for the same timing.
|
|
652
665
|
- Delivery runs `clear({ ifLastIndex })` and acknowledges after the deletion
|
|
653
666
|
finishes. No event is appended; a stale or duplicate delivery changes
|
|
654
667
|
nothing. Indexes never repeat, so a condition from an older lifetime never
|
|
655
668
|
matches a newer one.
|
|
656
669
|
- `ifLastIndex: ctx.event.index` matches only while the trigger is still the
|
|
657
|
-
head. The
|
|
658
|
-
|
|
670
|
+
head. The consumer's own appends and returned events move the head, so a
|
|
671
|
+
consumer that writes should use the index of its last `ctx.session.append`.
|
|
659
672
|
Actor handlers always commit state, so they can't use the trigger's index.
|
|
660
|
-
- A delay short enough to deliver while
|
|
661
|
-
|
|
662
|
-
|
|
673
|
+
- A delay short enough to deliver while `run` still works can match before its
|
|
674
|
+
writes land; the clear then ends that attempt and discards its returned
|
|
675
|
+
events.
|
|
663
676
|
- Each delivery attempt resumes the deletion. Vercel Queues redelivers until
|
|
664
677
|
retention ends. QStash retries up to its `retries` budget; a deletion that
|
|
665
678
|
outlasts them leaves the message in QStash's dead-letter queue, the rest of
|
|
@@ -672,6 +685,7 @@ works as in `session.schedule`, with the same provider limits. See
|
|
|
672
685
|
session.deadLetters(): Promise<DeadLetter[]>
|
|
673
686
|
|
|
674
687
|
type DeadLetter = Event & {
|
|
688
|
+
consumer: string
|
|
675
689
|
lane: string | null
|
|
676
690
|
attemptCount: number
|
|
677
691
|
failureCount: number
|
|
@@ -680,35 +694,37 @@ type DeadLetter = Event & {
|
|
|
680
694
|
}
|
|
681
695
|
```
|
|
682
696
|
|
|
683
|
-
The session's dead
|
|
684
|
-
|
|
697
|
+
The session's dead letters in event order, then by consumer, each with why it
|
|
698
|
+
failed. One event can be dead for one consumer and done for another. See
|
|
699
|
+
[Resolve a dead letter](/server/consumers#resolve-a-dead-letter).
|
|
685
700
|
|
|
686
701
|
- `failureCount` counts the attempts that spent its budget: caught failures,
|
|
687
702
|
and attempts that ended before they finished. `attemptCount` counts every
|
|
688
|
-
claim. `lastError` is the last error
|
|
703
|
+
claim. `lastError` is the last error `run` threw, stringified, or null
|
|
689
704
|
when every failed attempt ended before it finished.
|
|
690
705
|
- Events at or below the latest clear marker are never listed.
|
|
691
706
|
- Only on `server.session(id)`. `DeadLetter` is exported from
|
|
692
707
|
`experimental-a2/server`.
|
|
693
708
|
|
|
694
|
-
### `session.redrive(
|
|
709
|
+
### `session.redrive(target, options)`
|
|
695
710
|
|
|
696
711
|
```ts
|
|
697
712
|
session.redrive(
|
|
698
|
-
index: number,
|
|
713
|
+
target: { consumer: string; index: number },
|
|
699
714
|
options: { action: 'retry' | 'skip' },
|
|
700
715
|
): Promise<{ redriven: boolean }>
|
|
701
716
|
```
|
|
702
717
|
|
|
703
|
-
Resolves the dead-lettered event at `index` in one atomic store
|
|
718
|
+
Resolves the consumer's dead-lettered event at `index` in one atomic store
|
|
719
|
+
operation. Every dead letter names its consumer, A2's own included.
|
|
704
720
|
|
|
705
721
|
| Action | Effect |
|
|
706
722
|
| --- | --- |
|
|
707
|
-
| `retry` | The
|
|
708
|
-
| `skip` | The
|
|
723
|
+
| `retry` | The consumer runs the event again as a new attempt, with a fresh ten-failure budget. Once it completes, its lane continues. |
|
|
724
|
+
| `skip` | The consumer's cursor moves past the event without running it, and the next event in its lane runs. Its effects never happen for that consumer. |
|
|
709
725
|
|
|
710
|
-
- `
|
|
711
|
-
throws a `TypeError` before any I/O.
|
|
726
|
+
- `consumer` is a consumer's name, `index` is a positive safe integer, and
|
|
727
|
+
`action` is required; anything else throws a `TypeError` before any I/O.
|
|
712
728
|
- `redriven` is `false`, with nothing written, when the event isn't
|
|
713
729
|
dead-lettered: still pending, already processed, already redriven, missing,
|
|
714
730
|
or at or below the latest clear marker. Calling it again is safe.
|
|
@@ -717,12 +733,50 @@ Resolves the dead-lettered event at `index` in one atomic store operation.
|
|
|
717
733
|
`schedulerArmTimeoutMs`. A failed send doesn't reject the call; it marks the
|
|
718
734
|
`a2.redrive` span and leaves later appends, `server.drain()`, or another
|
|
719
735
|
`redrive` to wake the session.
|
|
720
|
-
- A retried
|
|
721
|
-
`ctx.attempt` keeps counting. A
|
|
722
|
-
|
|
736
|
+
- A retried head keeps its `attemptCount` and last failure, so the next run's
|
|
737
|
+
`ctx.attempt` keeps counting. A skip moves the consumer's cursor on, and its
|
|
738
|
+
next head starts with fresh counts.
|
|
723
739
|
- Only on `server.session(id)`. `SessionRedriveOptions` and
|
|
724
740
|
`SessionRedriveResult` are exported from `experimental-a2/server`.
|
|
725
741
|
|
|
742
|
+
### `session.consume(consumer, options?)`
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
session.consume(
|
|
746
|
+
consumer: Consumer,
|
|
747
|
+
options?: { through?: number; signal?: AbortSignal },
|
|
748
|
+
): Promise<{ index: number }>
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Runs a consumer with a string `lane` on this session, here, until it has
|
|
752
|
+
handled every event it takes through `through`: by default the session's head
|
|
753
|
+
when the call starts, and never more than it. Returns the index it has handled
|
|
754
|
+
every event through, at least `through`. See
|
|
755
|
+
[Run a consumer now](/server/consumers#run-a-consumer-now-with-consume).
|
|
756
|
+
|
|
757
|
+
- The consumer's cursor is created on the first call when it's missing: at the
|
|
758
|
+
start of the log with `backfill`, otherwise at the head. An idle cursor also
|
|
759
|
+
finds events appended by deployments that don't run the consumer. A session
|
|
760
|
+
with nothing after its latest clear, or never written, gets no cursor: the
|
|
761
|
+
call writes nothing.
|
|
762
|
+
- The server's own consumer shares its cursor with background drains: a head
|
|
763
|
+
another invocation holds, or one waiting out a failure's backoff, is waited
|
|
764
|
+
for. Nothing past `through` runs here; it carries on in the background. A different
|
|
765
|
+
definition under one of the server's consumer names throws a `TypeError`.
|
|
766
|
+
- A consumer the server doesn't run is claimed only by `consume`, and keeps
|
|
767
|
+
its place between calls.
|
|
768
|
+
- A dead-lettered head throws `CONSUMER_DEAD_LETTERED`. `signal` stops the
|
|
769
|
+
wait and throws its reason, even while an event runs here; that run
|
|
770
|
+
finishes on its own.
|
|
771
|
+
- `ctx.session.consume` of a consumer that is waiting on the running one
|
|
772
|
+
throws a `TypeError`, unless that consumer has handled everything through
|
|
773
|
+
`through` already.
|
|
774
|
+
- A consumer without a string `lane`, one of another contract, a `through`
|
|
775
|
+
that isn't a non-negative safe integer, or `ctx.session.consume` of the
|
|
776
|
+
running consumer itself throws a `TypeError` before any I/O.
|
|
777
|
+
- On `server.session(id)` and `ctx.session`. `SessionConsumeOptions` and
|
|
778
|
+
`SessionConsumeResult` are exported from `experimental-a2/server`.
|
|
779
|
+
|
|
726
780
|
### `session.history()`
|
|
727
781
|
|
|
728
782
|
```ts
|
|
@@ -747,18 +801,18 @@ session.state(
|
|
|
747
801
|
```
|
|
748
802
|
|
|
749
803
|
The log folded through one committed prefix. `index` is its exact boundary, an
|
|
750
|
-
append-order cursor rather than a
|
|
804
|
+
append-order cursor rather than a consumer-progress marker; hand it to the
|
|
751
805
|
client to resume there. See [State](/contract/state).
|
|
752
806
|
|
|
753
807
|
- Omitting `through` reads the latest committed frontier on a root session,
|
|
754
808
|
and through `ctx.event.index` on `ctx.session`, inclusive and stable across
|
|
755
|
-
concurrent appends and retries. In a
|
|
809
|
+
concurrent appends and retries. In a consumer, use `{ through: 'latest' }`
|
|
756
810
|
only when later committed events are intentionally part of the decision.
|
|
757
811
|
- A numeric `through` is an inclusive, non-negative safe integer;
|
|
758
812
|
`through: 0` returns the reducer's initial state.
|
|
759
813
|
- A latest read and a following append are not atomic. See
|
|
760
814
|
[State](/contract/state#reducers) for joins that converge.
|
|
761
|
-
- Never
|
|
815
|
+
- Never runs consumers or waits for pending work. Snapshot write-back
|
|
762
816
|
uses platform `waitUntil`, and its failures never fail the completed read:
|
|
763
817
|
they go to `a2.snapshot` telemetry or the default console sink, without
|
|
764
818
|
snapshot contents.
|
|
@@ -767,11 +821,12 @@ client to resume there. See [State](/contract/state).
|
|
|
767
821
|
positive `through` below `clearIndex` throws `SESSION_CLEARED`; `through: 0`
|
|
768
822
|
returns the seed without `clearIndex`.
|
|
769
823
|
- On `ctx.session`, every state read throws `SESSION_CLEARED` once the trigger
|
|
770
|
-
has been cleared, including `{ through: 'latest' }`.
|
|
824
|
+
has been cleared, including `{ through: 'latest' }`. Its `history()` and
|
|
771
825
|
`stream()` are not fenced.
|
|
772
|
-
- Default and numeric
|
|
773
|
-
the trigger completes or dead-letters and no other
|
|
774
|
-
it.
|
|
826
|
+
- Default and numeric reads on `ctx.session` retain their exact folded
|
|
827
|
+
checkpoint until the trigger completes or dead-letters and no other
|
|
828
|
+
unfinished run uses it. Its `{ through: 'latest' }` reads and root reads
|
|
829
|
+
retain nothing.
|
|
775
830
|
There are no retention options. A numeric read behind the latest snapshot
|
|
776
831
|
can use the greatest eligible retained checkpoint. See
|
|
777
832
|
[Snapshots are a cache](/contract/state#snapshots-are-a-cache).
|
|
@@ -791,7 +846,7 @@ session.stream(options: {
|
|
|
791
846
|
```
|
|
792
847
|
|
|
793
848
|
A live feed of the session's events. Server-side only; `server.fetch` exposes
|
|
794
|
-
it over SSE and WebSocket. Subscribing never
|
|
849
|
+
it over SSE and WebSocket. Subscribing never runs consumers.
|
|
795
850
|
|
|
796
851
|
- `startAfter` is a non-negative safe integer; `startAfter: 20` begins with
|
|
797
852
|
event 21.
|
|
@@ -854,7 +909,7 @@ session.presence(): Promise<{
|
|
|
854
909
|
```
|
|
855
910
|
|
|
856
911
|
The current presence map, expired values pruned: a point-in-time read for
|
|
857
|
-
server renders. Live
|
|
912
|
+
server renders. Live views use `stream({ presence: true })`. Each entry's
|
|
858
913
|
`at` is the sender's stamp for when the value was set, the last-writer-wins
|
|
859
914
|
comparator; how long a value
|
|
860
915
|
stays painted is view logic, decided at render time.
|