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
|
@@ -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.
|
|
@@ -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
|
|
@@ -505,11 +517,12 @@ The only way to move a session forward. See [Guarantees](/server/guarantees).
|
|
|
505
517
|
- A multi-event append is atomic: all-or-nothing, consecutive positions, one
|
|
506
518
|
transaction.
|
|
507
519
|
- An explicit `id` makes it idempotent across retries; re-sending an identical
|
|
508
|
-
batch returns the original rows.
|
|
520
|
+
batch returns the original rows. Ids are unique per session: the same `id`
|
|
521
|
+
in another session is another event.
|
|
509
522
|
- Events pushed through `server.fetch` arrive through this same append.
|
|
510
|
-
- Only
|
|
511
|
-
|
|
512
|
-
older
|
|
523
|
+
- Only events some consumer takes are dispatched; the append itself settles
|
|
524
|
+
the rest. An append no consumer takes still starts session healing when
|
|
525
|
+
older work is pending. The store reports that pending state in the
|
|
513
526
|
atomic append, so the decision needs no follow-up read.
|
|
514
527
|
|
|
515
528
|
### `session.append.dispatch(...events)`
|
|
@@ -525,7 +538,7 @@ scheduler instead of starting an inline drain. See
|
|
|
525
538
|
[Dispatch in a fresh invocation](/schedulers/overview#dispatch-in-a-fresh-invocation).
|
|
526
539
|
|
|
527
540
|
- Awaits acceptance of an immediate scheduler message. A batch that leaves no
|
|
528
|
-
pending
|
|
541
|
+
pending consumer work sends none.
|
|
529
542
|
- Throws before writing when the server has no scheduler.
|
|
530
543
|
- If the send fails, the events are already durable: give them explicit IDs
|
|
531
544
|
and retry the same dispatch.
|
|
@@ -546,7 +559,7 @@ session.schedule(
|
|
|
546
559
|
```
|
|
547
560
|
|
|
548
561
|
Appends the events later through the configured scheduler, from root and
|
|
549
|
-
|
|
562
|
+
consumer sessions. Resolves after provider acceptance, not after append. `name`
|
|
550
563
|
must be non-empty, with at least one event. `ScheduleDelay`, `ScheduleTiming`,
|
|
551
564
|
and `SessionSchedule` are exported from `experimental-a2/server`. See
|
|
552
565
|
[Timers](/server/timers) for stale-timer guards, provider limits, and
|
|
@@ -554,7 +567,7 @@ deployment compatibility.
|
|
|
554
567
|
|
|
555
568
|
| Timing | Meaning |
|
|
556
569
|
| --- | --- |
|
|
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
|
|
570
|
+
| `{ 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
571
|
| `{ at: Date }` | absolute delivery time; use it when a retried root call needs one target |
|
|
559
572
|
|
|
560
573
|
- The template type checks the unit. At runtime the number must be an
|
|
@@ -563,9 +576,10 @@ deployment compatibility.
|
|
|
563
576
|
once are rejected.
|
|
564
577
|
- The target is the earliest useful delivery time. Adapters encode it in
|
|
565
578
|
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
|
|
579
|
+
- The task id derives from the contract, session, and name. A consumer's
|
|
580
|
+
tasks add the trigger id, the consumer's name, and, once the session has
|
|
581
|
+
been cleared, the clear, so a trigger id reused in a new lifetime schedules
|
|
582
|
+
a new task. Omitted event ids
|
|
569
583
|
derive from the task id and position; explicit ids win.
|
|
570
584
|
- Reuse a name only for identical timing and events. Provider deduplication is
|
|
571
585
|
an optimization; stable event ids make later duplicate deliveries converge in
|
|
@@ -575,11 +589,11 @@ deployment compatibility.
|
|
|
575
589
|
functions, symbols, cycles, accessors, and non-plain prototypes are rejected;
|
|
576
590
|
shared references become independent values.
|
|
577
591
|
- 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
|
|
592
|
+
top-level append, outside any consumer's completion transaction, so a
|
|
593
|
+
schema transform produces the stored payload from the original input. The
|
|
594
|
+
events have `cause: null` and run through consumers, lanes, state, and
|
|
595
|
+
streams normally.
|
|
596
|
+
- A consumer's task belongs to its trigger's lifetime: if the session is cleared
|
|
583
597
|
before delivery, it is acknowledged without appending. A root task joins
|
|
584
598
|
whichever lifetime exists at delivery, and its name identifies one task
|
|
585
599
|
across lifetimes, so put something from the new lifetime in the name when
|
|
@@ -611,10 +625,10 @@ and `index` is always the head after the call:
|
|
|
611
625
|
| otherwise | `{ cleared: true, index }`, the new marker |
|
|
612
626
|
|
|
613
627
|
- Every event at or below the marker is superseded: never dispatched again,
|
|
614
|
-
never blocking its lane.
|
|
628
|
+
never blocking its lane. A run still working on one has its appends throw
|
|
615
629
|
`SUPERSEDED_ATTEMPT` and its state reads throw `SESSION_CLEARED` rather than
|
|
616
630
|
fold the new lifetime. Exception: after a clear from another instance, a
|
|
617
|
-
read bounded at the
|
|
631
|
+
read bounded at the run's own event may return that event's state from
|
|
618
632
|
memory until its `ctx.signal` aborts.
|
|
619
633
|
- Pushes and appends after the marker start the new lifetime.
|
|
620
634
|
- `clear()` then deletes the old events, their bookkeeping and event-id
|
|
@@ -624,8 +638,8 @@ and `index` is always the head after the call:
|
|
|
624
638
|
- If deletion fails, the call rejects with the session already cleared; any
|
|
625
639
|
later clear of the session, including a retry, finishes the deletion.
|
|
626
640
|
- Old event ids can be used again. Presence is not cleared.
|
|
627
|
-
- On `ctx.session`, the clear is fenced like the
|
|
628
|
-
|
|
641
|
+
- On `ctx.session`, the clear is fenced like the run's appends and ends the
|
|
642
|
+
run's own attempt: `ctx.signal` aborts with `SUPERSEDED_ATTEMPT` and
|
|
629
643
|
returned events are discarded. Its deletion keeps running under the
|
|
630
644
|
platform's `waitUntil`.
|
|
631
645
|
- All built-in stores support clearing.
|
|
@@ -647,19 +661,19 @@ works as in `session.schedule`, with the same provider limits. See
|
|
|
647
661
|
[Scheduling a clear](/contract/sessions#scheduling-a-clear).
|
|
648
662
|
|
|
649
663
|
- 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.
|
|
664
|
+
the trigger id, the consumer's name, and any earlier clear inside a
|
|
665
|
+
consumer. Reuse a name only for the same timing.
|
|
652
666
|
- Delivery runs `clear({ ifLastIndex })` and acknowledges after the deletion
|
|
653
667
|
finishes. No event is appended; a stale or duplicate delivery changes
|
|
654
668
|
nothing. Indexes never repeat, so a condition from an older lifetime never
|
|
655
669
|
matches a newer one.
|
|
656
670
|
- `ifLastIndex: ctx.event.index` matches only while the trigger is still the
|
|
657
|
-
head. The
|
|
658
|
-
|
|
671
|
+
head. The consumer's own appends and returned events move the head, so a
|
|
672
|
+
consumer that writes should use the index of its last `ctx.session.append`.
|
|
659
673
|
Actor handlers always commit state, so they can't use the trigger's index.
|
|
660
|
-
- A delay short enough to deliver while
|
|
661
|
-
|
|
662
|
-
|
|
674
|
+
- A delay short enough to deliver while `run` still works can match before its
|
|
675
|
+
writes land; the clear then ends that attempt and discards its returned
|
|
676
|
+
events.
|
|
663
677
|
- Each delivery attempt resumes the deletion. Vercel Queues redelivers until
|
|
664
678
|
retention ends. QStash retries up to its `retries` budget; a deletion that
|
|
665
679
|
outlasts them leaves the message in QStash's dead-letter queue, the rest of
|
|
@@ -672,6 +686,7 @@ works as in `session.schedule`, with the same provider limits. See
|
|
|
672
686
|
session.deadLetters(): Promise<DeadLetter[]>
|
|
673
687
|
|
|
674
688
|
type DeadLetter = Event & {
|
|
689
|
+
consumer: string
|
|
675
690
|
lane: string | null
|
|
676
691
|
attemptCount: number
|
|
677
692
|
failureCount: number
|
|
@@ -680,35 +695,37 @@ type DeadLetter = Event & {
|
|
|
680
695
|
}
|
|
681
696
|
```
|
|
682
697
|
|
|
683
|
-
The session's dead
|
|
684
|
-
|
|
698
|
+
The session's dead letters in event order, then by consumer, each with why it
|
|
699
|
+
failed. One event can be dead for one consumer and done for another. See
|
|
700
|
+
[Resolve a dead letter](/server/consumers#resolve-a-dead-letter).
|
|
685
701
|
|
|
686
702
|
- `failureCount` counts the attempts that spent its budget: caught failures,
|
|
687
703
|
and attempts that ended before they finished. `attemptCount` counts every
|
|
688
|
-
claim. `lastError` is the last error
|
|
704
|
+
claim. `lastError` is the last error `run` threw, stringified, or null
|
|
689
705
|
when every failed attempt ended before it finished.
|
|
690
706
|
- Events at or below the latest clear marker are never listed.
|
|
691
707
|
- Only on `server.session(id)`. `DeadLetter` is exported from
|
|
692
708
|
`experimental-a2/server`.
|
|
693
709
|
|
|
694
|
-
### `session.redrive(
|
|
710
|
+
### `session.redrive(target, options)`
|
|
695
711
|
|
|
696
712
|
```ts
|
|
697
713
|
session.redrive(
|
|
698
|
-
index: number,
|
|
714
|
+
target: { consumer: string; index: number },
|
|
699
715
|
options: { action: 'retry' | 'skip' },
|
|
700
716
|
): Promise<{ redriven: boolean }>
|
|
701
717
|
```
|
|
702
718
|
|
|
703
|
-
Resolves the dead-lettered event at `index` in one atomic store
|
|
719
|
+
Resolves the consumer's dead-lettered event at `index` in one atomic store
|
|
720
|
+
operation. Every dead letter names its consumer, A2's own included.
|
|
704
721
|
|
|
705
722
|
| Action | Effect |
|
|
706
723
|
| --- | --- |
|
|
707
|
-
| `retry` | The
|
|
708
|
-
| `skip` | The
|
|
724
|
+
| `retry` | The consumer runs the event again as a new attempt, with a fresh ten-failure budget. Once it completes, its lane continues. |
|
|
725
|
+
| `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
726
|
|
|
710
|
-
- `
|
|
711
|
-
throws a `TypeError` before any I/O.
|
|
727
|
+
- `consumer` is a consumer's name, `index` is a positive safe integer, and
|
|
728
|
+
`action` is required; anything else throws a `TypeError` before any I/O.
|
|
712
729
|
- `redriven` is `false`, with nothing written, when the event isn't
|
|
713
730
|
dead-lettered: still pending, already processed, already redriven, missing,
|
|
714
731
|
or at or below the latest clear marker. Calling it again is safe.
|
|
@@ -717,12 +734,50 @@ Resolves the dead-lettered event at `index` in one atomic store operation.
|
|
|
717
734
|
`schedulerArmTimeoutMs`. A failed send doesn't reject the call; it marks the
|
|
718
735
|
`a2.redrive` span and leaves later appends, `server.drain()`, or another
|
|
719
736
|
`redrive` to wake the session.
|
|
720
|
-
- A retried
|
|
721
|
-
`ctx.attempt` keeps counting. A
|
|
722
|
-
|
|
737
|
+
- A retried head keeps its `attemptCount` and last failure, so the next run's
|
|
738
|
+
`ctx.attempt` keeps counting. A skip moves the consumer's cursor on, and its
|
|
739
|
+
next head starts with fresh counts.
|
|
723
740
|
- Only on `server.session(id)`. `SessionRedriveOptions` and
|
|
724
741
|
`SessionRedriveResult` are exported from `experimental-a2/server`.
|
|
725
742
|
|
|
743
|
+
### `session.consume(consumer, options?)`
|
|
744
|
+
|
|
745
|
+
```ts
|
|
746
|
+
session.consume(
|
|
747
|
+
consumer: Consumer,
|
|
748
|
+
options?: { through?: number; signal?: AbortSignal },
|
|
749
|
+
): Promise<{ index: number }>
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
Runs a consumer with a string `lane` on this session, here, until it has
|
|
753
|
+
handled every event it takes through `through`: by default the session's head
|
|
754
|
+
when the call starts, and never more than it. Returns the index it has handled
|
|
755
|
+
every event through, at least `through`. See
|
|
756
|
+
[Run a consumer now](/server/consumers#run-a-consumer-now-with-consume).
|
|
757
|
+
|
|
758
|
+
- The consumer's cursor is created on the first call when it's missing: at the
|
|
759
|
+
start of the log with `backfill`, otherwise at the head. An idle cursor also
|
|
760
|
+
finds events appended by deployments that don't run the consumer. A session
|
|
761
|
+
with nothing after its latest clear, or never written, gets no cursor: the
|
|
762
|
+
call writes nothing.
|
|
763
|
+
- The server's own consumer shares its cursor with background drains: a head
|
|
764
|
+
another invocation holds, or one waiting out a failure's backoff, is waited
|
|
765
|
+
for. Nothing past `through` runs here; it carries on in the background. A different
|
|
766
|
+
definition under one of the server's consumer names throws a `TypeError`.
|
|
767
|
+
- A consumer the server doesn't run is claimed only by `consume`, and keeps
|
|
768
|
+
its place between calls.
|
|
769
|
+
- A dead-lettered head throws `CONSUMER_DEAD_LETTERED`. `signal` stops the
|
|
770
|
+
wait and throws its reason, even while an event runs here; that run
|
|
771
|
+
finishes on its own.
|
|
772
|
+
- `ctx.session.consume` of a consumer that is waiting on the running one
|
|
773
|
+
throws a `TypeError`, unless that consumer has handled everything through
|
|
774
|
+
`through` already.
|
|
775
|
+
- A consumer without a string `lane`, one of another contract, a `through`
|
|
776
|
+
that isn't a non-negative safe integer, or `ctx.session.consume` of the
|
|
777
|
+
running consumer itself throws a `TypeError` before any I/O.
|
|
778
|
+
- On `server.session(id)` and `ctx.session`. `SessionConsumeOptions` and
|
|
779
|
+
`SessionConsumeResult` are exported from `experimental-a2/server`.
|
|
780
|
+
|
|
726
781
|
### `session.history()`
|
|
727
782
|
|
|
728
783
|
```ts
|
|
@@ -747,18 +802,18 @@ session.state(
|
|
|
747
802
|
```
|
|
748
803
|
|
|
749
804
|
The log folded through one committed prefix. `index` is its exact boundary, an
|
|
750
|
-
append-order cursor rather than a
|
|
805
|
+
append-order cursor rather than a consumer-progress marker; hand it to the
|
|
751
806
|
client to resume there. See [State](/contract/state).
|
|
752
807
|
|
|
753
808
|
- Omitting `through` reads the latest committed frontier on a root session,
|
|
754
809
|
and through `ctx.event.index` on `ctx.session`, inclusive and stable across
|
|
755
|
-
concurrent appends and retries. In a
|
|
810
|
+
concurrent appends and retries. In a consumer, use `{ through: 'latest' }`
|
|
756
811
|
only when later committed events are intentionally part of the decision.
|
|
757
812
|
- A numeric `through` is an inclusive, non-negative safe integer;
|
|
758
813
|
`through: 0` returns the reducer's initial state.
|
|
759
814
|
- A latest read and a following append are not atomic. See
|
|
760
815
|
[State](/contract/state#reducers) for joins that converge.
|
|
761
|
-
- Never
|
|
816
|
+
- Never runs consumers or waits for pending work. Snapshot write-back
|
|
762
817
|
uses platform `waitUntil`, and its failures never fail the completed read:
|
|
763
818
|
they go to `a2.snapshot` telemetry or the default console sink, without
|
|
764
819
|
snapshot contents.
|
|
@@ -767,11 +822,12 @@ client to resume there. See [State](/contract/state).
|
|
|
767
822
|
positive `through` below `clearIndex` throws `SESSION_CLEARED`; `through: 0`
|
|
768
823
|
returns the seed without `clearIndex`.
|
|
769
824
|
- On `ctx.session`, every state read throws `SESSION_CLEARED` once the trigger
|
|
770
|
-
has been cleared, including `{ through: 'latest' }`.
|
|
825
|
+
has been cleared, including `{ through: 'latest' }`. Its `history()` and
|
|
771
826
|
`stream()` are not fenced.
|
|
772
|
-
- Default and numeric
|
|
773
|
-
the trigger completes or dead-letters and no other
|
|
774
|
-
it.
|
|
827
|
+
- Default and numeric reads on `ctx.session` retain their exact folded
|
|
828
|
+
checkpoint until the trigger completes or dead-letters and no other
|
|
829
|
+
unfinished run uses it. Its `{ through: 'latest' }` reads and root reads
|
|
830
|
+
retain nothing.
|
|
775
831
|
There are no retention options. A numeric read behind the latest snapshot
|
|
776
832
|
can use the greatest eligible retained checkpoint. See
|
|
777
833
|
[Snapshots are a cache](/contract/state#snapshots-are-a-cache).
|
|
@@ -791,7 +847,7 @@ session.stream(options: {
|
|
|
791
847
|
```
|
|
792
848
|
|
|
793
849
|
A live feed of the session's events. Server-side only; `server.fetch` exposes
|
|
794
|
-
it over SSE and WebSocket. Subscribing never
|
|
850
|
+
it over SSE and WebSocket. Subscribing never runs consumers.
|
|
795
851
|
|
|
796
852
|
- `startAfter` is a non-negative safe integer; `startAfter: 20` begins with
|
|
797
853
|
event 21.
|
|
@@ -828,6 +884,9 @@ broadcasts. No log row, no dispatch, no scheduler arm; `history()` and
|
|
|
828
884
|
epoch-ms stamp; the browser client stamps its sends automatically. Without
|
|
829
885
|
`at`, receipt time stands in: a stampless caller is a single writer, so
|
|
830
886
|
receipt order is its send order.
|
|
887
|
+
- `null` clears a field. The store keeps the clear's `at` until the field's
|
|
888
|
+
TTL runs out, so an older value that reaches it later, such as one sent
|
|
889
|
+
through another server instance, still loses.
|
|
831
890
|
- A bad payload throws `INVALID_PAYLOAD`, an unknown field throws
|
|
832
891
|
`UNKNOWN_PRESENCE_FIELD`, and a non-finite or negative `at` throws a
|
|
833
892
|
`TypeError`.
|
|
@@ -854,7 +913,7 @@ session.presence(): Promise<{
|
|
|
854
913
|
```
|
|
855
914
|
|
|
856
915
|
The current presence map, expired values pruned: a point-in-time read for
|
|
857
|
-
server renders. Live
|
|
916
|
+
server renders. Live views use `stream({ presence: true })`. Each entry's
|
|
858
917
|
`at` is the sender's stamp for when the value was set, the last-writer-wins
|
|
859
918
|
comparator; how long a value
|
|
860
919
|
stays painted is view logic, decided at render time.
|