experimental-a2 0.17.0 → 0.18.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 +32 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
- package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +692 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
- package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +950 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +113 -40
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
- package/dist/internal-CscSY7K_.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +9 -10
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
- package/dist/store-CyLUy86v.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +56 -11
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.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 +106 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
- package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +66 -21
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +109 -145
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +860 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +731 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +142 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +323 -0
- package/docs/server/06-guarantees.mdx +132 -0
- package/examples/playground/DEPLOY.md +3 -5
- package/examples/playground/app/a0/[appId]/page.tsx +12 -13
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +98 -54
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +271 -90
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +912 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1322 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/ai-tool-input.ts +45 -0
- package/src/internal.ts +13 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +14 -15
- package/src/server.ts +233 -29
- package/src/store-memory.ts +92 -16
- package/src/store-postgres.ts +156 -25
- package/src/store-redis-core.ts +322 -110
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +121 -28
- package/src/store.ts +55 -3
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
package/docs/01-quickstart.mdx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Quickstart
|
|
3
|
-
description: Define
|
|
3
|
+
description: Define a contract, react to its events, and make your first durable append in five minutes.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## What you'll build
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
can read back.
|
|
8
|
+
An order that notifies a shop when it arrives: one contract, one handler, two
|
|
9
|
+
routes, and a log you can read back.
|
|
11
10
|
|
|
12
11
|
## 1. Install
|
|
13
12
|
|
|
@@ -17,10 +16,7 @@ npm i experimental-a2 zod
|
|
|
17
16
|
|
|
18
17
|
## 2. Define a contract
|
|
19
18
|
|
|
20
|
-
A contract is a name plus the events it understands
|
|
21
|
-
name, each value a schema. Zod here, though any
|
|
22
|
-
[Standard Schema](https://standardschema.dev) validator works, and
|
|
23
|
-
that's the contract's complete declaration.
|
|
19
|
+
A contract is a name plus the events it understands, each with a schema:
|
|
24
20
|
|
|
25
21
|
```ts server/orders.ts
|
|
26
22
|
import { z } from 'zod'
|
|
@@ -35,21 +31,10 @@ export const orders = a2.contract({
|
|
|
35
31
|
})
|
|
36
32
|
```
|
|
37
33
|
|
|
38
|
-
The contract is a plain, importable, isomorphic value. The server
|
|
39
|
-
implements it below, the reducer derives from it in step 6, and when a
|
|
40
|
-
browser enters the picture it types the client too.
|
|
41
|
-
|
|
42
|
-
There's no database setup in development: A2 stores events in SQLite at
|
|
43
|
-
`.a2/dev.db`, so state survives restarts. Tests get an in-memory store.
|
|
44
|
-
Production wants an explicit backend; see
|
|
45
|
-
[Going to production](/guides/production).
|
|
46
|
-
|
|
47
34
|
## 3. Implement it
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
event and can append what happens next. Same file, below the contract
|
|
52
|
-
(they can split later, when a browser needs the contract alone):
|
|
36
|
+
`createServer` binds the contract to storage and handlers. Same file, below
|
|
37
|
+
the contract:
|
|
53
38
|
|
|
54
39
|
```ts server/orders.ts
|
|
55
40
|
import { createServer } from 'experimental-a2/server'
|
|
@@ -66,24 +51,19 @@ export const ordersServer = createServer({
|
|
|
66
51
|
})
|
|
67
52
|
```
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
54
|
+
- Handlers can run more than once (at-least-once delivery), so make external
|
|
55
|
+
calls safe to repeat. `event.id` is stable across re-runs: use it as the
|
|
56
|
+
idempotency key.
|
|
57
|
+
- The returned event commits atomically with handler success. If the handler
|
|
58
|
+
throws or the process dies first, `shop.notified` doesn't appear. Return one
|
|
59
|
+
event or an array.
|
|
74
60
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- The returned event is committed atomically with successful handler
|
|
79
|
-
completion. If the handler throws or the process dies first,
|
|
80
|
-
`shop.notified` does not appear. A handler can return one event or an array.
|
|
61
|
+
Development needs no database setup: events go to SQLite at `.a2/dev.db` and
|
|
62
|
+
survive restarts. Tests get an in-memory store. Production needs an explicit
|
|
63
|
+
store; see [Production](/server/production).
|
|
81
64
|
|
|
82
65
|
## 4. Append the first event
|
|
83
66
|
|
|
84
|
-
`append` is the entire public surface for moving a session forward. Call it
|
|
85
|
-
from a route:
|
|
86
|
-
|
|
87
67
|
```ts app/api/orders/route.ts
|
|
88
68
|
import { ordersServer } from '@/server/orders'
|
|
89
69
|
|
|
@@ -99,11 +79,8 @@ export async function POST(req: Request) {
|
|
|
99
79
|
}
|
|
100
80
|
```
|
|
101
81
|
|
|
102
|
-
|
|
103
|
-
is
|
|
104
|
-
Creating the handle does no I/O, and the id is yours to choose.
|
|
105
|
-
|
|
106
|
-
Try it:
|
|
82
|
+
`session(orderId)` is one [session](/contract/sessions), one instance of the
|
|
83
|
+
contract. The id is yours, and creating the handle does no I/O.
|
|
107
84
|
|
|
108
85
|
```bash
|
|
109
86
|
curl -X POST localhost:3000/api/orders \
|
|
@@ -111,18 +88,15 @@ curl -X POST localhost:3000/api/orders \
|
|
|
111
88
|
-d '{ "orderId": "order-42", "shopId": "shop-7", "items": ["flat white"] }'
|
|
112
89
|
```
|
|
113
90
|
|
|
114
|
-
The payload is validated
|
|
115
|
-
|
|
91
|
+
The payload is validated before anything is written; a bad one fails with
|
|
92
|
+
`INVALID_PAYLOAD` and the issues.
|
|
116
93
|
|
|
117
|
-
When
|
|
94
|
+
When `append` returns, the event is durably in the log and A2 has started the
|
|
118
95
|
`created` handler inline. In production, a queue-backed scheduler wakes pending
|
|
119
|
-
work after a failure or process death. See [
|
|
120
|
-
for the exact contract.
|
|
96
|
+
work after a failure or process death. See [Guarantees](/server/guarantees).
|
|
121
97
|
|
|
122
98
|
## 5. Read the log back
|
|
123
99
|
|
|
124
|
-
Add a route that returns the session's history:
|
|
125
|
-
|
|
126
100
|
```ts app/api/orders/[orderId]/route.ts
|
|
127
101
|
import { ordersServer } from '@/server/orders'
|
|
128
102
|
|
|
@@ -147,41 +121,35 @@ curl localhost:3000/api/orders/order-42
|
|
|
147
121
|
]
|
|
148
122
|
```
|
|
149
123
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
That second event is the payoff. You appended one event; the handler
|
|
154
|
-
returned the next; the log shows the whole story, in order. This is what
|
|
155
|
-
debugging looks like in A2: you read.
|
|
124
|
+
Each event also carries `id`, `sessionId`, and `createdAt` (trimmed here). You
|
|
125
|
+
appended one event and the handler returned the second. Debugging in A2 is
|
|
126
|
+
reading the log.
|
|
156
127
|
|
|
157
128
|
## 6. Fold state out of the log
|
|
158
129
|
|
|
159
|
-
|
|
160
|
-
once:
|
|
130
|
+
UIs usually want a view, not raw history. Define a reducer once:
|
|
161
131
|
|
|
162
132
|
```ts reducer.ts
|
|
163
133
|
import { reducer } from 'experimental-a2'
|
|
164
134
|
import { orders } from '@/server/orders'
|
|
165
135
|
|
|
166
|
-
export const ordersReducer = reducer({
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
136
|
+
export const ordersReducer = reducer({
|
|
137
|
+
contract: orders,
|
|
138
|
+
name: 'order-status',
|
|
139
|
+
initialState: { status: 'new' },
|
|
140
|
+
}).fold((state, event) => {
|
|
141
|
+
switch (event.type) {
|
|
142
|
+
case 'created':
|
|
143
|
+
return { ...state, status: 'created' }
|
|
144
|
+
case 'shop.notified':
|
|
145
|
+
return { ...state, status: 'notified' }
|
|
146
|
+
default:
|
|
147
|
+
return state
|
|
148
|
+
}
|
|
149
|
+
})
|
|
177
150
|
```
|
|
178
151
|
|
|
179
|
-
|
|
180
|
-
first call fixes the state and event types, so `.fold()` receives fully
|
|
181
|
-
concrete ones. No type arguments, no annotations, and literal unions in
|
|
182
|
-
your state survive the fold.
|
|
183
|
-
|
|
184
|
-
And fold anywhere you need the current picture:
|
|
152
|
+
Then fold wherever you need the current picture:
|
|
185
153
|
|
|
186
154
|
```ts
|
|
187
155
|
// anywhere on the server:
|
|
@@ -192,22 +160,20 @@ const { state } = await ordersServer.session('order-42').state(ordersReducer)
|
|
|
192
160
|
// state → { status: 'notified' }
|
|
193
161
|
```
|
|
194
162
|
|
|
195
|
-
|
|
196
|
-
|
|
163
|
+
State isn't a second record to update. It's a fold over the log; see
|
|
164
|
+
[State](/contract/state).
|
|
197
165
|
|
|
198
166
|
## That's it
|
|
199
167
|
|
|
200
|
-
You have a
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
react to it, applied to timers, cancellation, live UIs, and production
|
|
204
|
-
infrastructure.
|
|
168
|
+
You have a validated write path, a handler, a log you can read, and a view
|
|
169
|
+
folded from it. The rest of these docs apply the same two moves, append an
|
|
170
|
+
event and react to it, to timers, cancellation, live UIs, and production.
|
|
205
171
|
|
|
206
172
|
<CardGroup cols={2}>
|
|
207
|
-
<Card title="Handlers" href="/
|
|
208
|
-
|
|
173
|
+
<Card title="Handlers" href="/server/handlers" icon="file">
|
|
174
|
+
Context, chaining, and re-runs.
|
|
209
175
|
</Card>
|
|
210
|
-
<Card title="
|
|
211
|
-
|
|
176
|
+
<Card title="Production" href="/server/production" icon="rocket">
|
|
177
|
+
A store, a scheduler, dead letters.
|
|
212
178
|
</Card>
|
|
213
179
|
</CardGroup>
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Complete examples
|
|
3
|
-
description:
|
|
3
|
+
description: Every experimental-a2 install includes a complete Next.js playground to copy, run, and read.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Shipped with the package
|
|
7
7
|
|
|
8
|
-
Every `experimental-a2` installation includes a complete Next.js playground at
|
|
9
|
-
`node_modules/experimental-a2/examples/playground`.
|
|
10
|
-
|
|
11
|
-
The playground is not a collection of isolated snippets. Each feature owns its
|
|
12
|
-
contract, reducer, server, route handlers, client session, and scheduler when it
|
|
13
|
-
needs one. It uses only public package entry points.
|
|
14
|
-
|
|
15
|
-
Copy it into a new directory to run or modify it:
|
|
16
|
-
|
|
17
8
|
```sh
|
|
18
9
|
cp -R node_modules/experimental-a2/examples/playground ./a2-playground
|
|
19
10
|
cd a2-playground
|
|
@@ -21,12 +12,13 @@ pnpm install
|
|
|
21
12
|
pnpm dev
|
|
22
13
|
```
|
|
23
14
|
|
|
24
|
-
The
|
|
25
|
-
|
|
15
|
+
The playground lives at `node_modules/experimental-a2/examples/playground`.
|
|
16
|
+
Each feature owns its contract, reducer, server, route handlers, client
|
|
17
|
+
session, and scheduler when it needs one, using only public entry points.
|
|
26
18
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
inside the
|
|
19
|
+
The copy is standalone: its `package.json` pins A2 to the version that
|
|
20
|
+
shipped it, and its `AGENTS.md` describes the structure to coding agents that
|
|
21
|
+
work inside the directory.
|
|
30
22
|
|
|
31
23
|
## Find the right example
|
|
32
24
|
|
|
@@ -49,8 +41,6 @@ When A2 is installed locally, read these package directories in this order:
|
|
|
49
41
|
3. `node_modules/experimental-a2/src` only when implementation details are
|
|
50
42
|
relevant.
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
The npm snapshot is generated from `apps/playground` in the A2 repository when
|
|
56
|
-
the package is published, so the runnable app remains the canonical source.
|
|
44
|
+
Don't infer application patterns from `dist`. The playground, generated from
|
|
45
|
+
`apps/playground` in the A2 repository at publish time, is the reference for
|
|
46
|
+
how the public pieces fit together.
|
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Introduction
|
|
3
|
-
description: "
|
|
3
|
+
description: "Typed state behind a mailbox. Each instance processes one message at a time, and each handler ends in one atomic commit."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## The model
|
|
7
7
|
|
|
8
|
-
An actor is state behind a mailbox
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
An actor is state behind a mailbox: you send an instance typed events,
|
|
9
|
+
and its handlers process them one at a time. Between messages an
|
|
10
|
+
instance is rows in your store, not a process. Every instance is an
|
|
11
|
+
ordinary A2 session, so durability, retries, history, and devtools hold
|
|
12
|
+
for actors too.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
actors too. The module is three entry points: `experimental-a2/actor`
|
|
17
|
-
on the server, `experimental-a2/actor/client` anywhere, and
|
|
14
|
+
Entry points: `experimental-a2/actor` on the server,
|
|
15
|
+
`experimental-a2/actor/client` anywhere, and
|
|
18
16
|
`experimental-a2/actor/react` for the hook.
|
|
19
17
|
|
|
20
18
|
## Define one
|
|
21
19
|
|
|
22
|
-
A protocol declares the state shape and the events with their inputs.
|
|
23
|
-
Types, not schemas: handlers are trusted server code, and the compiler
|
|
24
|
-
holds them to the protocol. Pass it explicitly.
|
|
25
|
-
|
|
26
20
|
```ts vault.ts
|
|
27
21
|
import { NonRetriableError } from 'experimental-a2'
|
|
28
22
|
import { actor } from 'experimental-a2/actor'
|
|
@@ -52,28 +46,22 @@ export const vault = actor<Vault>({
|
|
|
52
46
|
})
|
|
53
47
|
```
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
committed atomically when the handler returns. `name` is
|
|
59
|
-
prefixes storage keys), and `state` seeds new instances.
|
|
49
|
+
Pass the protocol explicitly as the type argument. It declares types,
|
|
50
|
+
not schemas: handlers are trusted server code, and a missing handler,
|
|
51
|
+
an extra one, or a wrong input shape fails to compile. `ctx.state` is a
|
|
52
|
+
mutable draft, committed atomically when the handler returns. `name` is
|
|
53
|
+
identity (it prefixes storage keys), and `state` seeds new instances.
|
|
60
54
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`NonRetriableError` on bad shapes (bring a schema library and `.parse`
|
|
64
|
-
if you prefer; it is userland either way).
|
|
55
|
+
When an event can arrive from the wire, guard its input in the
|
|
56
|
+
handler's first lines and throw `NonRetriableError` on bad shapes.
|
|
65
57
|
|
|
66
|
-
`actor` also accepts
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
[Presence](/actors/routes#presence).
|
|
58
|
+
`actor` also accepts `createServer`'s `store`, `scheduler`, and
|
|
59
|
+
`telemetry` options, with the same development default store; see
|
|
60
|
+
[Production](/server/production). A protocol can also declare
|
|
61
|
+
[`presence`](/actors/routes#presence).
|
|
71
62
|
|
|
72
63
|
## Call it
|
|
73
64
|
|
|
74
|
-
`call` sends the event and awaits its answer: the state after the
|
|
75
|
-
handler ran, and the log index of that commit.
|
|
76
|
-
|
|
77
65
|
```ts
|
|
78
66
|
// anywhere on the server:
|
|
79
67
|
import { vault } from '@/vault'
|
|
@@ -84,18 +72,14 @@ const { state, index } = await savings.call.deposit({ amount: 100 })
|
|
|
84
72
|
// state reflects this deposit, and every message before it
|
|
85
73
|
```
|
|
86
74
|
|
|
87
|
-
`
|
|
88
|
-
|
|
75
|
+
`call` awaits the answer: the state after the handler ran and the log
|
|
76
|
+
index of that commit. `vault.actor(id)` is a handle; nothing exists
|
|
77
|
+
until the first message. Concurrent calls from any number of
|
|
89
78
|
invocations serialize on the instance's lane, and each answer reflects
|
|
90
|
-
its own message
|
|
91
|
-
[Concurrency](/actors/concurrency) is the full story.
|
|
79
|
+
its own message; see [Concurrency](/actors/concurrency).
|
|
92
80
|
|
|
93
81
|
### Refusals
|
|
94
82
|
|
|
95
|
-
A handler says no by throwing `NonRetriableError`. That refusal is an
|
|
96
|
-
answer, not a failure: nothing commits, nothing retries, and the caller
|
|
97
|
-
gets the message.
|
|
98
|
-
|
|
99
83
|
```ts
|
|
100
84
|
// anywhere on the server:
|
|
101
85
|
import { ActorRefusedError } from 'experimental-a2/actor'
|
|
@@ -110,28 +94,23 @@ try {
|
|
|
110
94
|
}
|
|
111
95
|
```
|
|
112
96
|
|
|
113
|
-
|
|
97
|
+
Throwing `NonRetriableError` refuses the message. A refusal is an
|
|
98
|
+
answer, and it is all or nothing: no state change, no buffered sends,
|
|
99
|
+
no retry. The message settles, and the caller receives
|
|
100
|
+
`ActorRefusedError`.
|
|
114
101
|
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
and the caller receives `ActorRefusedError`.
|
|
118
|
-
- **Any other throw** is an infrastructure failure. A2 retries the
|
|
119
|
-
message with the usual budget and backoff; the instance's lane waits,
|
|
120
|
-
which is deliberate (correctness over liveness). See
|
|
102
|
+
- **Any other throw** retries the message with the usual budget and
|
|
103
|
+
backoff while the instance's lane waits; see
|
|
121
104
|
[Errors](/reference/errors).
|
|
122
105
|
- **A crash** re-runs the handler from the pre-message state. `ctx.id`
|
|
123
106
|
is the message id, stable across re-runs: use it as the idempotency
|
|
124
107
|
key for external I/O.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
already have run. Over HTTP it's a `410`; see [Routes](/actors/routes).
|
|
108
|
+
- **A clear** by `handle.session.clear()` before the answer rejects the
|
|
109
|
+
call with an `A2Error` whose code is `SESSION_CLEARED`. The handler
|
|
110
|
+
may already have run. Over HTTP it's a `410`.
|
|
129
111
|
|
|
130
112
|
## Send, don't wait
|
|
131
113
|
|
|
132
|
-
`send` validates, appends the event durably, and returns without
|
|
133
|
-
waiting for the handler.
|
|
134
|
-
|
|
135
114
|
```ts
|
|
136
115
|
// anywhere on the server:
|
|
137
116
|
import { vault } from '@/vault'
|
|
@@ -140,9 +119,9 @@ const { id } = await vault.actor('savings').send.deposit({ amount: 25 })
|
|
|
140
119
|
// durably queued; the handler runs on the instance's lane
|
|
141
120
|
```
|
|
142
121
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
idempotent.
|
|
122
|
+
`send` validates and appends the event durably without waiting for the
|
|
123
|
+
handler: same mailbox, same ordering. Pass `{ id }` in the options to
|
|
124
|
+
make a retried send idempotent.
|
|
146
125
|
|
|
147
126
|
## Read without queueing
|
|
148
127
|
|
|
@@ -153,41 +132,24 @@ import { vault } from '@/vault'
|
|
|
153
132
|
const { state } = await vault.actor('savings').state()
|
|
154
133
|
```
|
|
155
134
|
|
|
156
|
-
A snapshot read
|
|
135
|
+
A snapshot read: it never queues behind pending messages, and a new
|
|
157
136
|
instance reads as its seed state.
|
|
158
137
|
|
|
159
138
|
## State is remembered, not recomputed
|
|
160
139
|
|
|
161
|
-
Every commit appends a state event
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
recompute views from events; actor state is the recorded decision
|
|
166
|
-
itself.)
|
|
167
|
-
|
|
168
|
-
Because state lives in the log, it holds to the same floor as any
|
|
169
|
-
payload: plain JSON trees. A `Date`, a `Map`, or `NaN` fails the commit
|
|
170
|
-
loudly instead of being silently coerced.
|
|
140
|
+
Every commit appends a state event, and reads fold them by last write
|
|
141
|
+
wins. Unlike [reducers](/contract/state), nothing is recomputed, so
|
|
142
|
+
rewriting a handler never changes what a past commit means. State must
|
|
143
|
+
be plain JSON: a `Date`, a `Map`, or `NaN` fails the commit loudly.
|
|
171
144
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
- **
|
|
175
|
-
|
|
176
|
-
defensively (`ctx.state.pending ??= {}` as a first line) instead of
|
|
177
|
-
assuming the latest shape.
|
|
178
|
-
- **The seed applies to new instances only.** Changing `state` in the
|
|
179
|
-
definition does not migrate existing instances; they keep their
|
|
180
|
-
remembered state.
|
|
145
|
+
- **Old instances meet new code.** After a deploy, default new fields
|
|
146
|
+
defensively (`ctx.state.pending ??= {}` as a first line).
|
|
147
|
+
- **The seed applies to new instances only.** Changing `state` does not
|
|
148
|
+
migrate existing instances.
|
|
181
149
|
|
|
182
150
|
## The log is still there
|
|
183
151
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
the
|
|
187
|
-
|
|
188
|
-
mailbox is visible in devtools. Event names starting with `a2.` are
|
|
189
|
-
reserved for the actor's own bookkeeping.
|
|
190
|
-
|
|
191
|
-
Next: [Concurrency](/actors/concurrency) for the execution
|
|
192
|
-
model, [Deadlines and timers](/actors/timers) for scheduled events, and
|
|
193
|
-
[Routes, clients, and React](/actors/routes) for the wire.
|
|
152
|
+
`handle.session` is the instance's underlying session (history,
|
|
153
|
+
streams, scheduling). The definition exposes its assembled `contract`,
|
|
154
|
+
`server`, and `reducer`, and devtools show the full mailbox. Event
|
|
155
|
+
names starting with `a2.` are reserved for the actor's own bookkeeping.
|
|
@@ -1,50 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Concurrency
|
|
3
|
-
description: "Each instance processes one message at a time. Slow work goes in concurrent handlers
|
|
3
|
+
description: "Each instance processes one message at a time. Slow work goes in concurrent handlers, which run in parallel and report back by sending events."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## One message at a time
|
|
7
7
|
|
|
8
|
-
Every message
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
checks cannot be raced by another message, and its changes commit
|
|
13
|
-
before the next message runs.
|
|
8
|
+
Every message runs on the instance's [lane](/server/handlers#lanes):
|
|
9
|
+
one handler at a time, in arrival order. A handler sees the state every
|
|
10
|
+
previous message produced, no other message can race its checks, and
|
|
11
|
+
its changes commit before the next message runs.
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
make one instance concurrent.
|
|
20
|
-
|
|
21
|
-
## The problem with slow work
|
|
22
|
-
|
|
23
|
-
A serial handler holds the lane while it runs. If `withdraw` awaits a
|
|
24
|
-
two-second bank API, every deposit waits behind it. The instance stays
|
|
25
|
-
correct, but it stops responding.
|
|
26
|
-
|
|
27
|
-
The fix is not to weaken the lane. It is to split the work: serial
|
|
28
|
-
handlers make the decisions, concurrent handlers do the slow parts,
|
|
29
|
-
and the results come back as events.
|
|
13
|
+
Lanes are per instance: a thousand vaults process in parallel, each in
|
|
14
|
+
order. If one instance gets more traffic than one queue can absorb,
|
|
15
|
+
split the identity (per user, per order, per document) instead of
|
|
16
|
+
making one instance concurrent.
|
|
30
17
|
|
|
31
18
|
## Concurrent handlers
|
|
32
19
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
A serial handler holds the lane while it runs: if `withdraw` awaits a
|
|
21
|
+
two-second bank API, every deposit waits behind it. Keep decisions
|
|
22
|
+
serial and move the slow part into a handler wrapped in
|
|
23
|
+
`concurrent(...)`. It leaves the lane and runs in parallel with serial
|
|
24
|
+
handlers and other concurrent ones. Inside it:
|
|
36
25
|
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
the lane and decide against current state.
|
|
26
|
+
- `ctx.state()` returns a snapshot, which can go stale while the
|
|
27
|
+
handler runs. There is no draft to mutate.
|
|
28
|
+
- State changes happen by sending events, whose serial handlers decide
|
|
29
|
+
against current state.
|
|
42
30
|
|
|
43
31
|
## Reserve, transfer, settle
|
|
44
32
|
|
|
45
|
-
A withdrawal that talks to a slow bank, without ever blocking the
|
|
46
|
-
vault: reserve the money, run the transfer, then settle or refund.
|
|
47
|
-
|
|
48
33
|
```ts vault.ts
|
|
49
34
|
import { NonRetriableError } from 'experimental-a2'
|
|
50
35
|
import { actor, concurrent } from 'experimental-a2/actor'
|
|
@@ -109,46 +94,34 @@ export const vault = actor<Vault>({
|
|
|
109
94
|
})
|
|
110
95
|
```
|
|
111
96
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
message does nothing. The refund amount comes from state, not from
|
|
126
|
-
recomputing it.
|
|
127
|
-
|
|
128
|
-
A crash at any step re-runs that one message from the state it started
|
|
129
|
-
with. Wherever the process dies, the vault lands in a named state:
|
|
130
|
-
reserved, settled, or refunded. Money is never half-transferred.
|
|
97
|
+
1. **`withdraw` reserves** on the lane, so no other withdrawal can race
|
|
98
|
+
the balance check. The balance drop, the `pending` entry, and the
|
|
99
|
+
`transfer` event commit atomically: the withdrawal cannot
|
|
100
|
+
half-start.
|
|
101
|
+
2. **`transfer` does the slow part** off the lane, so deposits keep
|
|
102
|
+
flowing. It never changes state, and it reports failure by sending
|
|
103
|
+
`refund`, not by throwing.
|
|
104
|
+
3. **`settle` and `refund` finish.** Both return when `pending[ref]` is
|
|
105
|
+
gone, so a duplicate or late message does nothing. The refund amount
|
|
106
|
+
comes from state.
|
|
107
|
+
|
|
108
|
+
A crash re-runs only that message from the state it started with, so
|
|
109
|
+
the vault always lands reserved, settled, or refunded.
|
|
131
110
|
|
|
132
111
|
## Buffered sends
|
|
133
112
|
|
|
134
|
-
`ctx.send`
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
twice.
|
|
113
|
+
`ctx.send` buffers. A serial handler's sends commit with its state
|
|
114
|
+
change; a concurrent handler's commit when it finishes. A refusal
|
|
115
|
+
discards the buffer with the state changes, so a refused message leaves
|
|
116
|
+
no trace. Re-runs produce the same message ids, so a retried handler
|
|
117
|
+
never appends its sends twice. `ctx.schedule` is
|
|
118
|
+
[not buffered](/actors/timers#scheduling-is-immediate).
|
|
141
119
|
|
|
142
120
|
## When handlers fail
|
|
143
121
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- A **concurrent** handler that throws is retried off the lane;
|
|
151
|
-
nothing else waits for it.
|
|
152
|
-
- `call` works on serial events only. A concurrent handler produces no
|
|
153
|
-
answer to wait for, so calling one is rejected: `send` those events
|
|
154
|
-
instead.
|
|
122
|
+
A concurrent handler that throws retries off the lane, and nothing
|
|
123
|
+
waits for it. Serial handlers fail as described in
|
|
124
|
+
[Refusals](/actors/introduction#refusals).
|
|
125
|
+
|
|
126
|
+
`call` works on serial events only. A concurrent handler produces no
|
|
127
|
+
answer, so calling one is rejected: `send` those events instead.
|