experimental-a2 0.6.0 → 0.8.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 +41 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +20 -17
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
- package/dist/server-DjPhHnbI.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
- package/dist/store-RJO35BMj.d.ts.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 +80 -78
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.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 +231 -182
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
- package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -4
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +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 +104 -91
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +10 -15
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +182 -293
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +36 -15
- package/src/client.ts +2 -2
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +315 -312
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +138 -101
- package/src/store-postgres.ts +355 -238
- package/src/store-redis-core.ts +247 -237
- package/src/store-redis-http.ts +1 -2
- package/src/store-redis.ts +1 -2
- package/src/store-sqlite.ts +191 -153
- package/src/store.ts +24 -66
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-CdMqi7mC.d.ts +0 -202
- package/dist/client-CdMqi7mC.d.ts.map +0 -1
- package/dist/client-Dj5d3SP_.js +0 -1026
- package/dist/client-Dj5d3SP_.js.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
|
@@ -72,7 +72,6 @@ createServer(options: {
|
|
|
72
72
|
store?: A2Store // default: sqlite in dev, memory in tests, required in prod
|
|
73
73
|
scheduler?: A2Scheduler
|
|
74
74
|
telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
|
|
75
|
-
validatePush?: (context: PushValidationContext) => void | PromiseLike<void>
|
|
76
75
|
presence?: { ttlMs?: number } // presence expiry policy; default 60s
|
|
77
76
|
handlers?: {
|
|
78
77
|
[type]:
|
|
@@ -123,19 +122,6 @@ Server-only by construction: `experimental-a2/server` is the only entry point th
|
|
|
123
122
|
can reach a store backend, and its exports map resolves to a loud error
|
|
124
123
|
under the browser condition.
|
|
125
124
|
|
|
126
|
-
`validatePush(context)` runs only for input that arrived over the wire
|
|
127
|
-
through `handle`'s push lane, once per plane. The events plane invokes it with `{ sessionId, events }` before
|
|
128
|
-
contract schema validation and before the store append, so throwing rejects the
|
|
129
|
-
complete push without writing anything. The presence plane invokes it with
|
|
130
|
-
`{ sessionId, events: [], presence }`, the whole pushed patch with its
|
|
131
|
-
participant, before field validation and the broadcast, so authorizing the
|
|
132
|
-
participant id (and applying any size or cardinality policy) happens at the
|
|
133
|
-
same seam. The patch arrives frozen: authorize, don't rewrite (a mutation
|
|
134
|
-
attempt throws and fails the push). Direct trusted server appends, handler appends, and server-side
|
|
135
|
-
`setPresence` bypass it. The envelope parser inside `handle` creates
|
|
136
|
-
the runtime provenance brand after reading the envelope; a caller-supplied
|
|
137
|
-
field with the same name is ignored, and the brand is not stored in the log.
|
|
138
|
-
|
|
139
125
|
`presence.ttlMs` sets how long a presence value survives without a
|
|
140
126
|
refreshing set (default 60 seconds, as a positive integer of
|
|
141
127
|
milliseconds). Expiry counts on the storage's own clock, never on the
|
|
@@ -172,12 +158,11 @@ The context every handler receives:
|
|
|
172
158
|
when a process dies before handler entry.
|
|
173
159
|
|
|
174
160
|
`ctx.session.id` equals `ctx.event.sessionId`. Its `history`, `state`, and
|
|
175
|
-
`stream` methods
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
is active.
|
|
161
|
+
`stream` methods share the session surface returned by `server.session(id)`,
|
|
162
|
+
with one causal default: `ctx.session.state(reducer)` folds through the
|
|
163
|
+
triggering event's index. The same event sees the same state across concurrent
|
|
164
|
+
appends and retries. Ask for `{ through: 'latest' }` when a handler intentionally
|
|
165
|
+
needs the committed frontier captured when that read runs.
|
|
181
166
|
|
|
182
167
|
The handler-local append is specialized:
|
|
183
168
|
|
|
@@ -198,10 +183,141 @@ in its stable task identity. A relative delay is anchored to that event's
|
|
|
198
183
|
durable `createdAt`, so a handler retry keeps the same delivery time. The call
|
|
199
184
|
returns `Promise<void>` after the configured scheduler accepts the task.
|
|
200
185
|
|
|
201
|
-
A state read and following append are not atomic. Concurrent appends and
|
|
202
|
-
retries may move the frontier between them. A generic join should
|
|
203
|
-
|
|
204
|
-
eligible attempt converges on the same
|
|
186
|
+
A latest state read and following append are not atomic. Concurrent appends and
|
|
187
|
+
retries may move the frontier between them. A generic join should read with
|
|
188
|
+
`{ through: 'latest' }`, use a monotone readiness predicate, and give its output
|
|
189
|
+
a stable explicit event `id`, so every eligible attempt converges on the same
|
|
190
|
+
append.
|
|
191
|
+
|
|
192
|
+
### `server.fetch(request, options?)`
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
server.fetch(
|
|
196
|
+
request: Request,
|
|
197
|
+
options?: {
|
|
198
|
+
authorize?: (operation: A2Operation) => boolean | Promise<boolean>
|
|
199
|
+
upgradeWebSocket?: (
|
|
200
|
+
attach: (socket: A2Socket) => void,
|
|
201
|
+
) => Response | Promise<Response>
|
|
202
|
+
},
|
|
203
|
+
): Promise<Response>
|
|
204
|
+
|
|
205
|
+
type A2Operation =
|
|
206
|
+
| {
|
|
207
|
+
type: 'stream'
|
|
208
|
+
sessionId: string
|
|
209
|
+
startAfter: number
|
|
210
|
+
transport: 'http' | 'websocket'
|
|
211
|
+
}
|
|
212
|
+
| {
|
|
213
|
+
type: 'history'
|
|
214
|
+
sessionId: string
|
|
215
|
+
gte: number
|
|
216
|
+
lte: number
|
|
217
|
+
transport: 'http'
|
|
218
|
+
}
|
|
219
|
+
| {
|
|
220
|
+
type: 'push'
|
|
221
|
+
sessionId: string
|
|
222
|
+
events: readonly {
|
|
223
|
+
type: ContractEventType | string
|
|
224
|
+
payload: unknown
|
|
225
|
+
id?: string
|
|
226
|
+
}[]
|
|
227
|
+
presence?: {
|
|
228
|
+
participant: string
|
|
229
|
+
values: Readonly<Record<string, unknown>>
|
|
230
|
+
seen?: number
|
|
231
|
+
at?: number
|
|
232
|
+
}
|
|
233
|
+
transport: 'http' | 'websocket'
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The bound Fetch API route for this server. An unprotected route can export it
|
|
238
|
+
directly:
|
|
239
|
+
|
|
240
|
+
```ts app/api/public-order-events/route.ts
|
|
241
|
+
import { ordersServer } from '@/server/orders'
|
|
242
|
+
|
|
243
|
+
export const GET = ordersServer.fetch
|
|
244
|
+
export const POST = ordersServer.fetch
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A plain GET streams one session over SSE, resumed after the `index` query
|
|
248
|
+
parameter. A GET with `gte` and `lte` returns the closed history range as JSON
|
|
249
|
+
and sets `a2-history-covered` to `true` when every requested index was returned.
|
|
250
|
+
POST accepts `{ sessionId, events, presence? }` and returns the appended events.
|
|
251
|
+
Presence is inferred from the contract. A contract that declares presence
|
|
252
|
+
streams it and accepts presence pushes automatically.
|
|
253
|
+
|
|
254
|
+
`authorize` runs after the request or socket frame has been parsed, but before
|
|
255
|
+
contract schema validation, presence validation, or I/O. Event names and
|
|
256
|
+
presence field names are typed from the contract. Their unvalidated values stay
|
|
257
|
+
`unknown`. Return `false` to deny the operation with `FORBIDDEN`. A thrown error
|
|
258
|
+
becomes `STORE_UNAVAILABLE`, because authorization infrastructure failed rather
|
|
259
|
+
than denied access. Operation objects, push events, event arrays, and presence
|
|
260
|
+
patches are frozen. Authorize them, do not rewrite them.
|
|
261
|
+
|
|
262
|
+
Authentication stays outside A2, where the route can read headers, cookies, and
|
|
263
|
+
framework context. Capture the authenticated principal in `authorize`:
|
|
264
|
+
|
|
265
|
+
```ts app/api/protected-order-events/route.ts
|
|
266
|
+
import { ordersServer } from '@/server/orders'
|
|
267
|
+
|
|
268
|
+
async function fetchOrders(request: Request): Promise<Response> {
|
|
269
|
+
// your authentication:
|
|
270
|
+
const user = { id: 'user-1' }
|
|
271
|
+
if (!user) return new Response(null, { status: 401 })
|
|
272
|
+
|
|
273
|
+
return ordersServer.fetch(request, {
|
|
274
|
+
authorize(operation) {
|
|
275
|
+
// your per-session and per-operation authorization:
|
|
276
|
+
// return canAccess(user, operation)
|
|
277
|
+
return true
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const GET = fetchOrders
|
|
283
|
+
export const POST = fetchOrders
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Direct `session.append`, handler appends, scheduled appends, and server-side
|
|
287
|
+
`setPresence` bypass `authorize`. It is an ingress policy for this fetch call,
|
|
288
|
+
not a global server policy.
|
|
289
|
+
|
|
290
|
+
A GET with `Upgrade: websocket` uses `upgradeWebSocket`. Without that option it
|
|
291
|
+
returns 426. The upgrade function receives an `attach` callback for the
|
|
292
|
+
platform socket:
|
|
293
|
+
|
|
294
|
+
```ts app/api/socket-order-events/route.ts
|
|
295
|
+
import { experimental_upgradeWebSocket } from '@vercel/functions'
|
|
296
|
+
import { ordersServer } from '@/server/orders'
|
|
297
|
+
|
|
298
|
+
export const POST = ordersServer.fetch
|
|
299
|
+
|
|
300
|
+
export function GET(request: Request): Promise<Response> {
|
|
301
|
+
return ordersServer.fetch(request, {
|
|
302
|
+
upgradeWebSocket: (attach) =>
|
|
303
|
+
experimental_upgradeWebSocket(attach, {
|
|
304
|
+
maxPayload: 4 * 1024 * 1024,
|
|
305
|
+
}),
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Authenticate the physical WebSocket GET before calling `server.fetch`. Then
|
|
311
|
+
use `authorize` for each session subscribe and each event or presence push.
|
|
312
|
+
There is no WebSocket-upgrade operation in `A2Operation`. A denied subscribe
|
|
313
|
+
receives `unsubscribed` and does not affect other sessions. A denied event push
|
|
314
|
+
receives a `FORBIDDEN` ack. Denied presence is silently dropped because it is
|
|
315
|
+
fire-and-forget. Failed presence authorization is dropped the same way and does
|
|
316
|
+
not disconnect unrelated sessions on the multiplexed socket.
|
|
317
|
+
|
|
318
|
+
SSE and WebSocket lifetimes use the platform's ambient invocation deadline.
|
|
319
|
+
A2 also uses the platform's ambient `waitUntil` capability for background work.
|
|
320
|
+
Neither capability is reordered or passed through `server.fetch` options.
|
|
205
321
|
|
|
206
322
|
### `server.session(id)`
|
|
207
323
|
|
|
@@ -372,9 +488,9 @@ Custom scheduler adapters implement two methods:
|
|
|
372
488
|
| `schedule(task: SchedulerTask): Promise<void>` | put one versioned task on durable delivery infrastructure |
|
|
373
489
|
| `handler(...servers)` | return the route that receives drain and append tasks |
|
|
374
490
|
|
|
375
|
-
`handler`
|
|
376
|
-
|
|
377
|
-
|
|
491
|
+
Application routes call `scheduler.handler(...servers)` on the configured
|
|
492
|
+
adapter. The handler requires at least one drainable server and rejects
|
|
493
|
+
duplicate contract names synchronously.
|
|
378
494
|
|
|
379
495
|
`SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask`. These task types,
|
|
380
496
|
plus `ScheduledEvent`, are exported from `experimental-a2/server`.
|
|
@@ -429,7 +545,7 @@ the contract's schemas before anything is written. A multi-event append
|
|
|
429
545
|
is atomic: all-or-nothing, consecutive positions, one transaction. Pass
|
|
430
546
|
`id` to make an append idempotent across retries; re-sending an
|
|
431
547
|
identical batch returns the original rows. (Events that arrived through
|
|
432
|
-
`
|
|
548
|
+
`server.fetch`'s push lane are accepted directly.) See
|
|
433
549
|
[Durability](/concepts/durability).
|
|
434
550
|
|
|
435
551
|
After the write, A2 dispatches only event types with registered handlers.
|
|
@@ -514,10 +630,13 @@ Raw events from the session, oldest first. `gte` and `lte` are inclusive event
|
|
|
514
630
|
indexes; omit a bound to leave that end of the log open.
|
|
515
631
|
Bounds are non-negative safe integers. `lte: 0` returns `[]`; `gte > lte` throws.
|
|
516
632
|
|
|
517
|
-
### `session.state(reducer)`
|
|
633
|
+
### `session.state(reducer, options?)`
|
|
518
634
|
|
|
519
635
|
```ts
|
|
520
|
-
session.state(
|
|
636
|
+
session.state(
|
|
637
|
+
reducer: Reducer<S>,
|
|
638
|
+
options?: { through?: number | 'latest' },
|
|
639
|
+
): Promise<{ state: S; index: number }>
|
|
521
640
|
```
|
|
522
641
|
|
|
523
642
|
The log folded through one committed prefix. `index` is its exact boundary,
|
|
@@ -525,6 +644,24 @@ an append-order cursor rather than a handler-settlement marker. Hand it to the
|
|
|
525
644
|
client to resume there. Snapshot write-back uses platform `waitUntil`; this
|
|
526
645
|
read never dispatches handlers or waits for pending work.
|
|
527
646
|
|
|
647
|
+
On a root session from `server.session(id)`, omitting `through` reads the latest
|
|
648
|
+
committed frontier. On the handler-scoped `ctx.session`, omitting it reads
|
|
649
|
+
through `ctx.event.index`. That boundary is inclusive and stable across retries.
|
|
650
|
+
Use `{ through: 'latest' }` inside a handler only when later committed events
|
|
651
|
+
are intentionally part of the decision. A numeric `through` is an inclusive,
|
|
652
|
+
non-negative safe integer; `through: 0` returns the reducer's initial state.
|
|
653
|
+
|
|
654
|
+
The default handler read and numeric handler reads automatically retain their
|
|
655
|
+
exact folded checkpoint while the triggering event is unfinished. Completion
|
|
656
|
+
or dead-lettering releases the reference and collects the checkpoint when no
|
|
657
|
+
other unfinished handler uses it. There are no retention options. Handler
|
|
658
|
+
`{ through: 'latest' }` reads and all root reads do not retain history.
|
|
659
|
+
|
|
660
|
+
If the latest snapshot is ahead of a numeric boundary, A2 can use the greatest
|
|
661
|
+
eligible retained checkpoint. A root historical read never creates retention
|
|
662
|
+
or writes a stale checkpoint behind the latest snapshot; it folds from the log
|
|
663
|
+
when no eligible checkpoint exists.
|
|
664
|
+
|
|
528
665
|
### `session.stream(options?)`
|
|
529
666
|
|
|
530
667
|
```ts
|
|
@@ -537,7 +674,7 @@ session.stream(options: {
|
|
|
537
674
|
|
|
538
675
|
A live feed of the session's events. `startAfter` is a non-negative safe integer;
|
|
539
676
|
`startAfter: 20` begins with event 21.
|
|
540
|
-
Server-side only; `
|
|
677
|
+
Server-side only; `server.fetch` exposes it over SSE, and over the multiplexed
|
|
541
678
|
socket when `upgrade` is set. Subscribing never dispatches handlers.
|
|
542
679
|
|
|
543
680
|
With `presence: true`, the feed yields one `PresenceSnapshot` first:
|
|
@@ -581,7 +718,7 @@ the storage clock, not the sender stamp, so a hostile stamp can only
|
|
|
581
718
|
vandalize its own field and still expires on schedule.
|
|
582
719
|
|
|
583
720
|
The patch is structurally the `presence` sibling of the push envelope;
|
|
584
|
-
`
|
|
721
|
+
`server.fetch`'s push lane forwards it whole to `session.setPresence(presence)`.
|
|
585
722
|
The API mirrors the wire.
|
|
586
723
|
|
|
587
724
|
The handler-scoped form `ctx.session.setPresence(...)` is the same
|
|
@@ -707,9 +844,7 @@ Both members exist only when the contract declares `presence`; their
|
|
|
707
844
|
value and field types come from its schemas, through the reducer, with
|
|
708
845
|
no type arguments (the reducer is the client's typed handle on the
|
|
709
846
|
contract; it still never folds presence). `setPresence` requires a
|
|
710
|
-
`participant`: the client's default, or the provider's override.
|
|
711
|
-
the map holds only your own echo with
|
|
712
|
-
participants active, the GET route forgot `presence: true`. See
|
|
847
|
+
`participant`: the client's default, or the provider's override. See
|
|
713
848
|
[Presence](/guides/presence).
|
|
714
849
|
|
|
715
850
|
`push` appends optimistically: validated locally, rolled back on
|
|
@@ -1006,10 +1141,12 @@ retried or interrupted.
|
|
|
1006
1141
|
Independent authorized tool handlers run concurrently without a fixed
|
|
1007
1142
|
concurrency limit. A private coordinator reducer tracks generation closure,
|
|
1008
1143
|
cancellation, calls, approvals, and terminal results for the active response.
|
|
1009
|
-
Completed responses do not accumulate in its state. Its
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1144
|
+
Completed responses do not accumulate in its state. Its
|
|
1145
|
+
`ctx.session.state(coordinator, { through: 'latest' })` reads the durable
|
|
1146
|
+
snapshot plus log tail because these joins intentionally observe concurrent
|
|
1147
|
+
tool results. Process memory is not authoritative. Concurrent join checks return
|
|
1148
|
+
the same deterministic continuation event, so they use `ctx.session.append()`
|
|
1149
|
+
and storage deduplicates the race.
|
|
1013
1150
|
|
|
1014
1151
|
Tool results and generation completion have no fixed relative order. The
|
|
1015
1152
|
continuation predicate needs both the closed model step and every required
|
|
@@ -1045,17 +1182,10 @@ it into `createServer({ handlers })` beside application handlers when you need
|
|
|
1045
1182
|
a custom assembly. The table handles input facts, generation requests, model
|
|
1046
1183
|
step completion, tool calls, approval responses, and terminal tool results.
|
|
1047
1184
|
Application handlers spread later can deliberately replace a built-in
|
|
1048
|
-
handler.
|
|
1049
|
-
`
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
`validateAgentPush({ sessionId, events }): void`
|
|
1054
|
-
|
|
1055
|
-
Accepts the browser interaction allowlist: user messages, approval and input
|
|
1056
|
-
responses, interruptions, and explicit retries. It rejects server-authored
|
|
1057
|
-
scheduling and lifecycle events, seeded non-user messages, and input requests.
|
|
1058
|
-
`createAgentServer()` installs it automatically.
|
|
1185
|
+
handler. A custom assembly owns its browser ingress policy. Use
|
|
1186
|
+
`server.fetch(request, { authorize })` to reject server-authored AI event names
|
|
1187
|
+
before they reach the server. `createAgentServer()` installs the built-in
|
|
1188
|
+
browser allowlist automatically.
|
|
1059
1189
|
|
|
1060
1190
|
See [Durable AI agents](/guides/ai-agents) for the protocol and complete
|
|
1061
1191
|
examples.
|
|
@@ -1106,96 +1236,6 @@ frontier. Idle handles are evicted after `gcTime`. This memory layer is
|
|
|
1106
1236
|
separate from `experimental-a2/cache-indexeddb`: memory preserves identity
|
|
1107
1237
|
across route transitions, while IndexedDB preserves the replica across reloads.
|
|
1108
1238
|
|
|
1109
|
-
## `experimental-a2/http`
|
|
1110
|
-
|
|
1111
|
-
### `handle(server, options?)`
|
|
1112
|
-
|
|
1113
|
-
```ts
|
|
1114
|
-
handle(server: A2Server, options?: {
|
|
1115
|
-
before?(args: { request: Request; intent: A2Intent }):
|
|
1116
|
-
Response | undefined | void | Promise<Response | undefined | void>
|
|
1117
|
-
after?(args: { request: Request; intent: A2Intent; outcome: A2Outcome; response: Response }):
|
|
1118
|
-
Response | undefined | void | Promise<Response | undefined | void>
|
|
1119
|
-
upgrade?: UpgradeFn // e.g. (attach) => experimental_upgradeWebSocket(attach)
|
|
1120
|
-
presence?: boolean // interleave presence on every stream lane
|
|
1121
|
-
deadline?: number // epoch ms: close sockets cleanly before it
|
|
1122
|
-
}): { GET(req: Request): Promise<Response>; POST(req: Request): Promise<Response> }
|
|
1123
|
-
|
|
1124
|
-
type A2Intent =
|
|
1125
|
-
| { type: 'ws-upgrade' }
|
|
1126
|
-
| { type: 'stream'; sessionId: string; startAfter: number; transport: 'sse' | 'ws' }
|
|
1127
|
-
| { type: 'history'; sessionId: string; gte: number; lte: number }
|
|
1128
|
-
| { type: 'push'; sessionId: string; events: PushedEvent[]; presence?: PushedPresence; transport: 'http' | 'ws' }
|
|
1129
|
-
|
|
1130
|
-
type A2Outcome =
|
|
1131
|
-
| { type: 'stream' }
|
|
1132
|
-
| { type: 'history'; covered: boolean; events: Event[] }
|
|
1133
|
-
| { type: 'push'; appended: Event[] }
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
The session route pair as one call: `export const { GET, POST } =
|
|
1137
|
-
handle(server)` in a route module (any framework speaking
|
|
1138
|
-
`(req: Request) => Promise<Response>`). A plain `GET` is the live SSE
|
|
1139
|
-
stream, resumed after the `index` query parameter, with a `: connected`
|
|
1140
|
-
prelude, a `: ping` heartbeat every 15s, and a clean close one second
|
|
1141
|
-
before an ambient Vercel invocation deadline when available; presence
|
|
1142
|
-
patches ride as named frames when `presence: true`. A `GET` with
|
|
1143
|
-
`gte`/`lte` query parameters is a history slice: the closed log range
|
|
1144
|
-
as JSON wire events, the read `loadHistory` rides. `POST` is the push
|
|
1145
|
-
envelope `{ sessionId, events, presence? }`, answered with the appended
|
|
1146
|
-
events. A `GET` carrying an upgrade header becomes the multiplexed
|
|
1147
|
-
WebSocket when `options.upgrade` is present, and answers `426` when it
|
|
1148
|
-
is not.
|
|
1149
|
-
|
|
1150
|
-
Parsing is protocol, hooks are policy. A request that fails to parse
|
|
1151
|
-
(missing `sessionId`, malformed bounds, a bad push envelope) answers
|
|
1152
|
-
`INVALID_PAYLOAD` on the wire before any hook runs. `before` sees every
|
|
1153
|
-
parsed intent, HTTP requests and socket frames alike; over the socket,
|
|
1154
|
-
each subscribe arrives as a `stream` intent and each push as a `push`
|
|
1155
|
-
intent, with `request` always the original upgrade Request. Returning a
|
|
1156
|
-
Response short-circuits: over HTTP it is the response, verbatim; over
|
|
1157
|
-
the socket it is translated into the wire's own vocabulary (a denied
|
|
1158
|
-
subscribe answers `unsubscribed`, a denied push a non-retryable error
|
|
1159
|
-
ack), because a Response cannot cross a socket.
|
|
1160
|
-
|
|
1161
|
-
`after` runs only where the library produced an HTTP response: never
|
|
1162
|
-
after a short-circuit, never for `ws-upgrade` or socket frames. It may
|
|
1163
|
-
mutate `response.headers` in place or return a replacement Response.
|
|
1164
|
-
`outcome.covered` on a history read means the closed range came back
|
|
1165
|
-
fully covered (`events.length === lte - gte + 1`): an immutable slice
|
|
1166
|
-
of the append-only log, safe to cache under whatever policy your
|
|
1167
|
-
`after` applies. The history response carries no cache headers of its
|
|
1168
|
-
own.
|
|
1169
|
-
|
|
1170
|
-
The socket is one connection for all of a client's sessions:
|
|
1171
|
-
`subscribe`/`unsubscribe` frames open and close per-session lanes at
|
|
1172
|
-
their own resume frontiers, `sessionId` tags route pushes, presence,
|
|
1173
|
-
and acks, and a lane ending or failing answers `unsubscribed` without
|
|
1174
|
-
taking the socket down. See [Transports](/guides/transports).
|
|
1175
|
-
|
|
1176
|
-
### The rest of the entry
|
|
1177
|
-
|
|
1178
|
-
| Helper | What it does |
|
|
1179
|
-
| ----------------------- | ------------------------------------------------------------------------------ |
|
|
1180
|
-
| `schedulerHandler(...servers)` | returns the delivery route after synchronously verifying one shared scheduler |
|
|
1181
|
-
| `errorResponse(err)` | serializes an `A2Error` to `{ error: { code, message, details } }` + status; the natural return value of a refusing `before` hook |
|
|
1182
|
-
| `deserializeError(body)` | rebuilds an `A2Error` from a wire body, or `null` if the body isn't one |
|
|
1183
|
-
|
|
1184
|
-
`schedulerHandler(...servers)` is the application-facing scheduler route. It
|
|
1185
|
-
requires at least one A2 server. Every server must have a scheduler, use the
|
|
1186
|
-
exact same scheduler instance, and have a unique contract name. Non-A2 values
|
|
1187
|
-
also fail. These checks throw before the request handler is returned, so bad
|
|
1188
|
-
wiring fails when the route module loads. The helper then delegates delivery
|
|
1189
|
-
to the shared adapter's `A2Scheduler.handler(...)` method.
|
|
1190
|
-
|
|
1191
|
-
Different scheduler instances use different routes. Match each QStash route to
|
|
1192
|
-
that instance's resolved `url`; additional QStash routes pass an explicit
|
|
1193
|
-
`url`. Match each Vercel Queues route and trigger to that instance's `topic`.
|
|
1194
|
-
|
|
1195
|
-
`errorResponse` and `deserializeError` are the `A2Error` wire format
|
|
1196
|
-
that `push` and the push lane share. See
|
|
1197
|
-
[Errors](/reference/errors#over-the-wire).
|
|
1198
|
-
|
|
1199
1239
|
## `experimental-a2/cache-indexeddb`
|
|
1200
1240
|
|
|
1201
1241
|
```ts
|
|
@@ -1273,112 +1313,6 @@ log.
|
|
|
1273
1313
|
renames and additions are breaking for dashboards, and are called out in the
|
|
1274
1314
|
package's `CHANGELOG.md`.
|
|
1275
1315
|
|
|
1276
|
-
## `experimental-a2/devtools`
|
|
1277
|
-
|
|
1278
|
-
The shared, isomorphic protocol for browser Devtools, the CLI, and `.a2log`
|
|
1279
|
-
captures.
|
|
1280
|
-
|
|
1281
|
-
| Export | Contract |
|
|
1282
|
-
| --- | --- |
|
|
1283
|
-
| `DEVTOOLS_PROTOCOL_VERSION` | Current HTTP wire protocol version |
|
|
1284
|
-
| `DEVTOOLS_CAPTURE_VERSION` | Current `.a2log` record format version |
|
|
1285
|
-
| `DEVTOOLS_CAPTURE_MEDIA_TYPE` | `application/x-ndjson` |
|
|
1286
|
-
| `DEVTOOLS_SESSION_PAGE_LIMIT` | Default event count requested per session page |
|
|
1287
|
-
| `DEVTOOLS_SESSION_PAGE_MAX_LIMIT` | Maximum event count accepted per session page |
|
|
1288
|
-
| `DEVTOOLS_CAPABILITIES` | Protocol resources and capture format advertised by the server |
|
|
1289
|
-
| `devtoolsSessionRevision({ events, snapshots })` | Compute the lifecycle invalidation token after assembling pages |
|
|
1290
|
-
| `encodeDevtoolsCapture(detail, { capturedAt? })` | Encode one session detail as exact NDJSON and add its SHA-256 footer |
|
|
1291
|
-
| `parseDevtoolsCapture(value)` | Parse NDJSON text or bytes into a `DevtoolsCapture` while retaining unknown fields on known records |
|
|
1292
|
-
| `verifyDevtoolsCapture(capture)` | Verify versions, record shapes, session identity, indexes, counts, dates, and the SHA-256 digest |
|
|
1293
|
-
|
|
1294
|
-
The wire types are `DevtoolsWireSessionSummary`, `DevtoolsWireEvent`,
|
|
1295
|
-
`DevtoolsWireSnapshot`, `DevtoolsContractsResponse`,
|
|
1296
|
-
`DevtoolsSessionsResponse`, `DevtoolsSessionDetail`, `DevtoolsSessionPage`, and
|
|
1297
|
-
`DevtoolsCapabilities`. Capture records use `DevtoolsCaptureManifest`,
|
|
1298
|
-
`DevtoolsCaptureEvent`, `DevtoolsCaptureSnapshot`,
|
|
1299
|
-
`DevtoolsCaptureFooter`, and `DevtoolsCaptureRecord`. A parsed
|
|
1300
|
-
`DevtoolsCapture` exposes the same canonical sequence through `records`.
|
|
1301
|
-
|
|
1302
|
-
```ts test/verify-a2log.ts
|
|
1303
|
-
import { readFile } from 'node:fs/promises'
|
|
1304
|
-
import {
|
|
1305
|
-
parseDevtoolsCapture,
|
|
1306
|
-
verifyDevtoolsCapture,
|
|
1307
|
-
} from 'experimental-a2/devtools'
|
|
1308
|
-
|
|
1309
|
-
export async function verifyA2Log(path: string) {
|
|
1310
|
-
const capture = parseDevtoolsCapture(await readFile(path))
|
|
1311
|
-
await verifyDevtoolsCapture(capture)
|
|
1312
|
-
return capture
|
|
1313
|
-
}
|
|
1314
|
-
```
|
|
1315
|
-
|
|
1316
|
-
The manifest records the contract, session, revision, capture time, protocol
|
|
1317
|
-
version, and capture version. Event records contain the exact durable payload
|
|
1318
|
-
and operational bookkeeping. Snapshot records contain metadata but never
|
|
1319
|
-
cached reducer state. The final `end` record contains counts, the highest event
|
|
1320
|
-
index, and a digest of every preceding encoded record. The session revision is
|
|
1321
|
-
an invalidation token, not the integrity digest. Encoding rejects durable
|
|
1322
|
-
values that canonical JSON would coerce or omit.
|
|
1323
|
-
|
|
1324
|
-
## `experimental-a2/devtools/server`
|
|
1325
|
-
|
|
1326
|
-
```ts
|
|
1327
|
-
// anywhere on the server:
|
|
1328
|
-
import type { DevtoolsServer } from 'experimental-a2/devtools/server'
|
|
1329
|
-
|
|
1330
|
-
declare function createDevtools(options: {
|
|
1331
|
-
servers: readonly DevtoolsServer[]
|
|
1332
|
-
authorize?: (
|
|
1333
|
-
request: Request,
|
|
1334
|
-
) => boolean | Response | Promise<boolean | Response>
|
|
1335
|
-
}): {
|
|
1336
|
-
handler(): (request: Request) => Promise<Response>
|
|
1337
|
-
}
|
|
1338
|
-
```
|
|
1339
|
-
|
|
1340
|
-
A read-only dashboard over the servers' durable logs. The handler serves the
|
|
1341
|
-
complete browser application, its JSON endpoints, and live SSE invalidations.
|
|
1342
|
-
It discovers sessions and reads stored causal, dispatch, completion, failure,
|
|
1343
|
-
and snapshot metadata. The causal forest and lifecycle timeline come directly
|
|
1344
|
-
from stored events. The initial page response includes the selected dashboard
|
|
1345
|
-
data so the browser does not need a contracts, sessions, and detail request
|
|
1346
|
-
waterfall. It never drains or heals a session.
|
|
1347
|
-
|
|
1348
|
-
Every route is a `GET`. The versioned resources are `capabilities`,
|
|
1349
|
-
`contracts`, `sessions`, `session`, `watch`, and `export` under the handler's
|
|
1350
|
-
`_a2/` path. `export` returns an integrity-checkable `.a2log` with the media
|
|
1351
|
-
type from `DEVTOOLS_CAPTURE_MEDIA_TYPE`.
|
|
1352
|
-
|
|
1353
|
-
`session` without pagination parameters returns one complete detail. Add a
|
|
1354
|
-
positive `limit` to receive a `DevtoolsSessionPage`. Its `throughIndex` is the
|
|
1355
|
-
inclusive event frontier frozen by the first request. Pass its opaque `cursor`
|
|
1356
|
-
back until the cursor is null. Snapshot metadata appears on the first page
|
|
1357
|
-
only. The cursor is bound to its contract and session, and the server rejects
|
|
1358
|
-
reuse against another log. The advertised maximum page size is clamped by the
|
|
1359
|
-
server.
|
|
1360
|
-
|
|
1361
|
-
The browser, CLI, watch loop, and export route walk these bounded pages for
|
|
1362
|
-
you. They reject a gap or early end. An exact export includes every event
|
|
1363
|
-
through one finite frontier, even when a backend provider limits one range
|
|
1364
|
-
response. Events appended during the walk belong to a later read.
|
|
1365
|
-
|
|
1366
|
-
Without `authorize`, the handler is available only when `NODE_ENV` is exactly
|
|
1367
|
-
`development`. Any other value, including unset, returns 404. When `authorize`
|
|
1368
|
-
is present, only a literal `true` grants access. Returning `false` also returns
|
|
1369
|
-
404. Returning a `Response` passes that response through, which supports
|
|
1370
|
-
redirects and authentication challenges. The mounted handler is the
|
|
1371
|
-
authorization boundary for the browser, CLI, SSE stream, and exact capture
|
|
1372
|
-
download. A2 does not add a separate Devtools credential store.
|
|
1373
|
-
|
|
1374
|
-
The built-in memory, SQLite, Postgres, and Redis stores support inspection.
|
|
1375
|
-
Custom `A2Store` implementations can omit the optional `inspect` interface; the
|
|
1376
|
-
dashboard returns 501 for those logs. A custom inspection implementation may
|
|
1377
|
-
add `readEvents(sessionId, { afterIndex, throughIndex?, limit })` for efficient
|
|
1378
|
-
bounded reads. It returns `{ events, throughIndex }`, where the first call
|
|
1379
|
-
freezes the inclusive frontier and continuations preserve it. Without this
|
|
1380
|
-
method, A2 reads the complete log for each page and slices it in memory.
|
|
1381
|
-
|
|
1382
1316
|
## `experimental-a2/scheduler-vercel`
|
|
1383
1317
|
|
|
1384
1318
|
`vercelQueues(options?)` returns an `A2Scheduler` backed by Vercel Queues.
|
|
@@ -1490,10 +1424,9 @@ Mount the application handler once and pass every server that shares the
|
|
|
1490
1424
|
scheduler:
|
|
1491
1425
|
|
|
1492
1426
|
```ts app/api/a2/scheduler/route.ts
|
|
1493
|
-
import {
|
|
1494
|
-
import { ordersServer, billingServer } from '@/server'
|
|
1427
|
+
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
1495
1428
|
|
|
1496
|
-
export const POST =
|
|
1429
|
+
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
1497
1430
|
```
|
|
1498
1431
|
|
|
1499
1432
|
The route must be reachable by the selected QStash server after any platform
|
|
@@ -1574,46 +1507,6 @@ duplicate-message error is success. A2's durable AI tool execution uses this
|
|
|
1574
1507
|
private classification. Direct `session.schedule()` calls still reject with
|
|
1575
1508
|
the original error object.
|
|
1576
1509
|
|
|
1577
|
-
## `a2 devtools`
|
|
1578
|
-
|
|
1579
|
-
The package installs an `a2` binary with a read-only `devtools` namespace.
|
|
1580
|
-
|
|
1581
|
-
| Command | Purpose |
|
|
1582
|
-
| --- | --- |
|
|
1583
|
-
| `contracts` | List contracts mounted at the Devtools URL |
|
|
1584
|
-
| `sessions --contract NAME` | List session summaries, with optional `--limit` and `--cursor` |
|
|
1585
|
-
| `show --contract NAME --session ID` | Read one session's exact durable detail |
|
|
1586
|
-
| `export --contract NAME --session ID` | Verify and save an `.a2log`; use `--output` and opt into replacement with `--force` |
|
|
1587
|
-
| `check --contract NAME --session ID` | Verify a live capture and optionally require `--settled`, `--no-dead-letters`, `--no-caught-failures`, or `--max-redispatches N` |
|
|
1588
|
-
| `verify FILE` | Verify a saved capture offline |
|
|
1589
|
-
|
|
1590
|
-
Live commands read the mounted URL from `--url` or `A2_DEVTOOLS_URL`.
|
|
1591
|
-
`A2_DEVTOOLS_TOKEN` supplies the default bearer token. `--bearer-env ENV` and
|
|
1592
|
-
repeatable `--header-env HEADER=ENV` reference other environment variables.
|
|
1593
|
-
Credential values are never accepted as CLI arguments or URL components.
|
|
1594
|
-
Output uses `--format human`, `json`, or `ndjson`.
|
|
1595
|
-
|
|
1596
|
-
## `experimental-a2/testing`
|
|
1597
|
-
|
|
1598
|
-
Node-only helpers for verified capture loading and pure reducer replay.
|
|
1599
|
-
|
|
1600
|
-
| Export | Contract |
|
|
1601
|
-
| --- | --- |
|
|
1602
|
-
| `loadCapture(source, contract)` | Read a filesystem path or URL, verify its integrity, validate its contract and payloads, and return typed events |
|
|
1603
|
-
| `prepareCapture(capture, contract)` | Apply the same verification and contract typing to an already parsed capture |
|
|
1604
|
-
| `replayCapture(loaded, reducer, { throughIndex? })` | Fold captured events through the reducer, stopping at an optional inclusive index |
|
|
1605
|
-
|
|
1606
|
-
`LoadedCapture` contains the verified `capture`, the `contract`, typed public
|
|
1607
|
-
`events` with `createdAt` revived as `Date`, and unmodified operational
|
|
1608
|
-
`rawEvents`.
|
|
1609
|
-
`CaptureReplay` returns `{ state, index }`. A cutpoint of 0 returns the initial
|
|
1610
|
-
state and index 0. Supporting types are `CaptureSource`, `LoadedCapture`,
|
|
1611
|
-
`ReplayCaptureOptions`, and `CaptureReplay`.
|
|
1612
|
-
|
|
1613
|
-
Replay does not use cached snapshot state, construct a server, dispatch a
|
|
1614
|
-
handler, invoke an AI model or tool, or repeat external effects. It reproduces
|
|
1615
|
-
only the current reducer's pure projection over already recorded events.
|
|
1616
|
-
|
|
1617
1510
|
## Entry points
|
|
1618
1511
|
|
|
1619
1512
|
| Entry point | Ships | Peer dependency |
|
|
@@ -1624,7 +1517,6 @@ only the current reducer's pure projection over already recorded events.
|
|
|
1624
1517
|
| `experimental-a2/react` | `createReact` | `react` |
|
|
1625
1518
|
| `experimental-a2/ai` | agent contract, `handlerContext`, reducer, and append builders: isomorphic | `ai` |
|
|
1626
1519
|
| `experimental-a2/ai/server` | AI SDK runner and built-in handlers | `ai` |
|
|
1627
|
-
| `experimental-a2/http` | route-side transport and scheduler helpers | none |
|
|
1628
1520
|
| `experimental-a2/store-postgres` | `postgres`: Postgres store backend | `pg` (or inject a client) |
|
|
1629
1521
|
| `experimental-a2/store-redis` | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
|
|
1630
1522
|
| `experimental-a2/store-redis-http` | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
|
|
@@ -1634,9 +1526,6 @@ only the current reducer's pure projection over already recorded events.
|
|
|
1634
1526
|
| `experimental-a2/scheduler-qstash` | `qstash`: signed recovery and timers | `@upstash/qstash` |
|
|
1635
1527
|
| `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
|
|
1636
1528
|
| `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
|
|
1637
|
-
| `experimental-a2/devtools` | versioned wire protocol and exact capture codec | none |
|
|
1638
|
-
| `experimental-a2/devtools/server` | durable read-only dashboard handler | none |
|
|
1639
|
-
| `experimental-a2/testing` | Node-only capture loading and pure reducer replay | none |
|
|
1640
1529
|
|
|
1641
1530
|
Core `experimental-a2` imports none of the backends, enforced by a browser-bundle
|
|
1642
1531
|
test in CI, not just convention. Importing `experimental-a2/store-postgres` is what
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Errors
|
|
3
|
-
description: One error class,
|
|
3
|
+
description: One error class, stable codes, and clear rules about what append will never throw for.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## `A2Error`
|
|
@@ -35,6 +35,7 @@ export async function POST(req: Request) {
|
|
|
35
35
|
| Code | Thrown when | Retryable |
|
|
36
36
|
| ------------------------- | ------------------------------------------------------------------ | --------- |
|
|
37
37
|
| `INVALID_PAYLOAD` | a payload fails its schema; nothing was written, and `details` carries the issues ([Standard Schema](https://standardschema.dev) format) | no |
|
|
38
|
+
| `FORBIDDEN` | `server.fetch` authorization denies a parsed stream, history read, push, or presence update | no |
|
|
38
39
|
| `UNKNOWN_EVENT_TYPE` | an event type isn't in the contract's `events` map | no |
|
|
39
40
|
| `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
|
|
40
41
|
| `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the configured store has no presence capability (thrown at construction; for environment-resolved stores, deferred to the first store use) | no |
|
|
@@ -73,12 +74,9 @@ The push route serializes an `A2Error` as:
|
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
with a mapped status: `INVALID_PAYLOAD`, `UNKNOWN_EVENT_TYPE`, and
|
|
76
|
-
`PARTIAL_DUPLICATE_BATCH` are 400; `
|
|
77
|
-
`STORE_UNAVAILABLE` is 503.
|
|
77
|
+
`PARTIAL_DUPLICATE_BATCH` are 400; `FORBIDDEN` is 403;
|
|
78
|
+
`SUPERSEDED_ATTEMPT` is 409; `STORE_UNAVAILABLE` is 503.
|
|
78
79
|
|
|
79
80
|
The client's `push` deserializes the body back into an `A2Error`, so client
|
|
80
81
|
and server code branch on identical codes. `push` auto-retries only
|
|
81
|
-
`STORE_UNAVAILABLE`.
|
|
82
|
-
`deserializeError`, ships in `experimental-a2/http` alongside `handle`;
|
|
83
|
-
a `before` hook that wants the wire's own error shapes returns
|
|
84
|
-
`errorResponse(new A2Error(...))`.
|
|
82
|
+
`STORE_UNAVAILABLE`. `server.fetch` owns the HTTP serialization.
|