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
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Handlers
|
|
3
|
-
description: Plain async functions that react to events. Concurrent by default, serialized by lanes, retried after failures.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
```ts server/orders.ts
|
|
7
|
-
import { createServer } from 'experimental-a2/server'
|
|
8
|
-
import { orders } from '@/contracts'
|
|
9
|
-
|
|
10
|
-
export const ordersServer = createServer({
|
|
11
|
-
contract: orders,
|
|
12
|
-
handlers: {
|
|
13
|
-
created: async ({ event }) => {
|
|
14
|
-
// your side effect, e.g. email the shop, idempotent via event.id:
|
|
15
|
-
// await sendEmailToShop(event.payload, { idempotencyKey: event.id })
|
|
16
|
-
return { type: 'shop.notified', payload: {} }
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
A handler runs when its event lands: a plain async function that can call a
|
|
23
|
-
database, hit an API, or stream a model response. A2 claims the event while it
|
|
24
|
-
runs and retries it after a crash.
|
|
25
|
-
|
|
26
|
-
`createServer` from `experimental-a2/server` binds the contract to a
|
|
27
|
-
[store](/stores/overview) and one complete handler table,
|
|
28
|
-
so a handler can't go missing because its module was never imported. Compose
|
|
29
|
-
the table across files by spreading objects into `handlers`. It is the only
|
|
30
|
-
entry point that touches a store, and it fails loudly in a browser bundle.
|
|
31
|
-
|
|
32
|
-
## Events without handlers
|
|
33
|
-
|
|
34
|
-
```ts server/order-history.ts
|
|
35
|
-
import { createServer } from 'experimental-a2/server'
|
|
36
|
-
import { postgres } from 'experimental-a2/store-postgres'
|
|
37
|
-
import { orders } from '@/contracts'
|
|
38
|
-
|
|
39
|
-
export const orderHistory = createServer({
|
|
40
|
-
contract: orders,
|
|
41
|
-
store: postgres({ connectionString: process.env.DATABASE_URL }),
|
|
42
|
-
})
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Handlers are optional per event type. An event without one is still logged,
|
|
46
|
-
folded into state, and streamed; it settles in its append transaction, with no
|
|
47
|
-
claim or scheduler message. Adding a handler later doesn't replay settled
|
|
48
|
-
events. Append a new event or run a migration when you want that.
|
|
49
|
-
|
|
50
|
-
## The context
|
|
51
|
-
|
|
52
|
-
| Property | What it is |
|
|
53
|
-
| ------------- | ---------- |
|
|
54
|
-
| `ctx.event` | The triggering event: `{ id, type, payload, index, sessionId, createdAt }`. |
|
|
55
|
-
| `ctx.attempt` | The durable 1-based dispatch claim for this event. |
|
|
56
|
-
| `ctx.session` | This session's handle: `id`, `append`, `schedule`, `clear`, `history`, `state`, `stream`, and `setPresence` (presence contracts only). |
|
|
57
|
-
| `ctx.signal` | An `AbortSignal`. Fires on matching `abortOn` events and when the attempt loses its claim (`CLAIM_EXPIRED`, `SUPERSEDED_ATTEMPT`). See [Cancellation](/server/cancellation). |
|
|
58
|
-
|
|
59
|
-
`ctx.attempt` increments on every durable claim. It can skip a number when a
|
|
60
|
-
process dies between the claim and handler entry.
|
|
61
|
-
|
|
62
|
-
[`ctx.session.state(reducer)`](/contract/state) folds through
|
|
63
|
-
`ctx.event.index`, so later concurrent appends don't change what this event
|
|
64
|
-
sees. Pass `{ through: 'latest' }` to read the current committed frontier
|
|
65
|
-
instead.
|
|
66
|
-
|
|
67
|
-
## Return what happens after success
|
|
68
|
-
|
|
69
|
-
```ts server/return-many.ts
|
|
70
|
-
import { createServer } from 'experimental-a2/server'
|
|
71
|
-
import { orders } from '@/contracts'
|
|
72
|
-
|
|
73
|
-
export const fanout = createServer({
|
|
74
|
-
contract: orders,
|
|
75
|
-
handlers: {
|
|
76
|
-
'shop.started': async () => {
|
|
77
|
-
return orders.batch(
|
|
78
|
-
{ type: 'shop.notified', payload: {} },
|
|
79
|
-
{ type: 'driver.notified', payload: { driverId: 'driver-7' } },
|
|
80
|
-
)
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Return one event or an array when it should exist only if the handler
|
|
87
|
-
succeeds. A2 marks the trigger processed and appends the batch in one atomic
|
|
88
|
-
operation, at consecutive positions. If the handler throws or the process dies
|
|
89
|
-
first, none of it exists. Returned events get deterministic ids, so a retry
|
|
90
|
-
after a lost acknowledgement doesn't duplicate them.
|
|
91
|
-
|
|
92
|
-
`contract.batch(...)` is a type helper that keeps each event's literal type;
|
|
93
|
-
A2 still validates at commit.
|
|
94
|
-
|
|
95
|
-
## Append what should happen now
|
|
96
|
-
|
|
97
|
-
```ts server/append-now.ts
|
|
98
|
-
import { createServer } from 'experimental-a2/server'
|
|
99
|
-
import { orders } from '@/contracts'
|
|
100
|
-
|
|
101
|
-
export const immediate = createServer({
|
|
102
|
-
contract: orders,
|
|
103
|
-
handlers: {
|
|
104
|
-
created: async ({ session }) => {
|
|
105
|
-
await session.append('notify-shop', {
|
|
106
|
-
type: 'shop.notified',
|
|
107
|
-
payload: {},
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// The shop.notified handler may already be running here.
|
|
111
|
-
// await continueCurrentWork()
|
|
112
|
-
},
|
|
113
|
-
'shop.notified': async () => {},
|
|
114
|
-
},
|
|
115
|
-
})
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`ctx.session.append()` commits immediately, so the appended event can start
|
|
119
|
-
while its parent is still running. Use it for streaming progress, independent
|
|
120
|
-
fan-out, or any fact that stays true even if the handler later fails. Each
|
|
121
|
-
call is atomic.
|
|
122
|
-
|
|
123
|
-
The first argument names the append within the triggering event. A2 derives
|
|
124
|
-
the ids of events without an `id` from the trigger, the name, and each
|
|
125
|
-
event's position, so a retry gets the existing events back, whatever order
|
|
126
|
-
parallel branches finish in. Keep the name stable; reusing it with a different
|
|
127
|
-
batch conflicts. An explicit `id` wins, so different triggers can converge on
|
|
128
|
-
one fact. Top-level `server.session(id).append(...events)` takes no name.
|
|
129
|
-
|
|
130
|
-
## Concurrent by default
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
// anywhere on the server:
|
|
134
|
-
await ordersServer.session('order-42').append(
|
|
135
|
-
{ type: 'created', payload: { shopId: 'shop-7', items: [] } },
|
|
136
|
-
{ type: 'shop.started', payload: {} },
|
|
137
|
-
)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Every eligible event has its own claim, so both handlers start without
|
|
141
|
-
waiting for each other. Log order stays stable; handler order doesn't. A
|
|
142
|
-
failure retries that event without holding up unlaned events or other lanes.
|
|
143
|
-
|
|
144
|
-
## Lanes
|
|
145
|
-
|
|
146
|
-
```ts server/serial-orders.ts
|
|
147
|
-
import { createServer } from 'experimental-a2/server'
|
|
148
|
-
import { orders } from '@/contracts'
|
|
149
|
-
|
|
150
|
-
export const serialOrders = createServer({
|
|
151
|
-
contract: orders,
|
|
152
|
-
handlers: {
|
|
153
|
-
created: {
|
|
154
|
-
lane: 'order-work',
|
|
155
|
-
handler: async () => {},
|
|
156
|
-
},
|
|
157
|
-
'shop.started': {
|
|
158
|
-
lane: 'order-work',
|
|
159
|
-
handler: async () => {},
|
|
160
|
-
},
|
|
161
|
-
'driver.notified': {
|
|
162
|
-
lane: ({ event }) => `driver:${event.payload.driverId}`,
|
|
163
|
-
handler: async ({ event }) => {
|
|
164
|
-
// your work for this driver, using event.payload.driverId
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
})
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Use a lane when events share a resource and must not overlap. Within one
|
|
172
|
-
session, same-lane events run one at a time in log order; different lanes and
|
|
173
|
-
unlaned events stay concurrent. A lane is a constant (a serial queue) or a
|
|
174
|
-
function of the validated event.
|
|
175
|
-
|
|
176
|
-
A2 stores the resolved lane at append, so a deployment can't reinterpret
|
|
177
|
-
pending work. Lanes are per session, not distributed locks across sessions. If
|
|
178
|
-
a lane's first unfinished event fails, later events in that lane wait; other
|
|
179
|
-
lanes continue.
|
|
180
|
-
|
|
181
|
-
## Handlers can run twice
|
|
182
|
-
|
|
183
|
-
Handlers are at-least-once. A crash after your side effect but before A2
|
|
184
|
-
records completion runs the handler again.
|
|
185
|
-
|
|
186
|
-
- **External side effects take an idempotency key.** `event.id` is stable
|
|
187
|
-
across attempts.
|
|
188
|
-
- **Named appends are deduplicated.** A retry gets the existing events back.
|
|
189
|
-
- **Returned events commit with completion.** They all exist with the
|
|
190
|
-
processed marker, or none do.
|
|
191
|
-
|
|
192
|
-
## When handlers throw
|
|
193
|
-
|
|
194
|
-
Throwing means "retry me." A2 records the failure and retries with backoff.
|
|
195
|
-
The failed event blocks only its lane. After ten failed attempts it
|
|
196
|
-
dead-letters and keeps blocking its lane until
|
|
197
|
-
[you retry or skip it](#resolve-a-dead-letter).
|
|
198
|
-
|
|
199
|
-
When someone presses stop, catch the abort and return normally instead. See
|
|
200
|
-
[Cancellation](/server/cancellation).
|
|
201
|
-
|
|
202
|
-
## Resolve a dead letter
|
|
203
|
-
|
|
204
|
-
A [dead-lettered](/server/guarantees#when-a-handler-keeps-failing) event waits
|
|
205
|
-
for you. List a session's dead letters, then retry or skip each one:
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
// anywhere on the server, behind your own operator checks:
|
|
209
|
-
import { ordersServer } from '@/server'
|
|
210
|
-
|
|
211
|
-
const order = ordersServer.session(orderId)
|
|
212
|
-
const [dead] = await order.deadLetters()
|
|
213
|
-
if (dead) {
|
|
214
|
-
// dead.lastError says why (null if every attempt timed out or crashed);
|
|
215
|
-
// deploy the handler fix first, then:
|
|
216
|
-
await order.redrive(dead.index, { action: 'retry' })
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
- **Retry.** `{ action: 'retry' }` runs the handler again with a fresh
|
|
221
|
-
ten-failure budget. Once it completes, its lane continues.
|
|
222
|
-
- **Skip.** `{ action: 'skip' }` marks the event processed without running it,
|
|
223
|
-
accepting that its effects never happen. The next event in its lane runs.
|
|
224
|
-
|
|
225
|
-
Each redrive is one atomic store operation, and it wakes the session like an
|
|
226
|
-
append, so the lane resumes without another write. An event that isn't
|
|
227
|
-
dead-lettered anymore is left alone: `redrive` reports `{ redriven: false }`.
|