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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: State
|
|
3
|
+
description: "State is a fold over the log: raw history for facts, a reducer for the current view."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## History
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// anywhere on the server:
|
|
10
|
+
const events = await ordersServer.session(orderId).history()
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Every event in the session, oldest first: always the raw log, never a summary
|
|
14
|
+
or snapshot. Use it for audits, debugging, and handler questions like "did the
|
|
15
|
+
shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
|
|
16
|
+
|
|
17
|
+
## Reducers
|
|
18
|
+
|
|
19
|
+
Derive a reducer from the [`orders` contract](/contract/events) and fold:
|
|
20
|
+
|
|
21
|
+
```ts reducer.ts
|
|
22
|
+
import { reducer } from 'experimental-a2'
|
|
23
|
+
import { z } from 'zod'
|
|
24
|
+
import { orders } from './contracts'
|
|
25
|
+
|
|
26
|
+
export const ordersReducer = reducer({
|
|
27
|
+
contract: orders,
|
|
28
|
+
name: 'order-status',
|
|
29
|
+
initialState: { status: 'new' },
|
|
30
|
+
stateSchema: z.object({
|
|
31
|
+
status: z.enum(['new', 'created', 'notified', 'expired']),
|
|
32
|
+
shopId: z.string().optional(),
|
|
33
|
+
}),
|
|
34
|
+
}).fold((state, event) => {
|
|
35
|
+
switch (event.type) {
|
|
36
|
+
case 'created':
|
|
37
|
+
return { status: 'created', shopId: event.payload.shopId }
|
|
38
|
+
case 'shop.notified':
|
|
39
|
+
return { ...state, status: 'notified' }
|
|
40
|
+
case 'expired':
|
|
41
|
+
return { ...state, status: 'expired' }
|
|
42
|
+
default:
|
|
43
|
+
return state
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// anywhere on the server, e.g. a server component before first paint:
|
|
50
|
+
const { state, index } = await ordersServer.session(orderId).state(ordersReducer)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A reducer is a `name` (its [cache identity](#snapshots-are-a-cache)), an
|
|
54
|
+
`initialState`, an optional `stateSchema`, and a pure `fold`,
|
|
55
|
+
`(state, event) => state`. The two-step shape fixes the state and event types
|
|
56
|
+
before `.fold()`, so you write no type arguments and literal unions survive.
|
|
57
|
+
|
|
58
|
+
`state()` returns `state` and `index`: the fold includes every event through
|
|
59
|
+
that index and none after. The browser resumes its live stream from there; see
|
|
60
|
+
[React](/client/react).
|
|
61
|
+
|
|
62
|
+
Without `stateSchema`, the state type is inferred from `initialState`, fine
|
|
63
|
+
while every fold arm returns the same shape. With it, the schema is the type
|
|
64
|
+
(the seed may lean on its defaults), `initialState` is validated at
|
|
65
|
+
definition, and a cached fold that fails the schema, in a server snapshot or
|
|
66
|
+
the [browser cache](/client/react), is discarded and refolded. That catches
|
|
67
|
+
shape drift a stale `name` can't.
|
|
68
|
+
|
|
69
|
+
Contract and reducer modules import only schemas and `experimental-a2`, so the
|
|
70
|
+
browser runs the same reducer; see [React](/client/react#publish-the-state). A
|
|
71
|
+
server-only app can keep the contract next to `createServer` instead.
|
|
72
|
+
|
|
73
|
+
## Read boundaries
|
|
74
|
+
|
|
75
|
+
A root session reads the latest committed frontier. A handler's session stops
|
|
76
|
+
at its triggering event by default, stable across concurrent appends and
|
|
77
|
+
retries:
|
|
78
|
+
|
|
79
|
+
```ts server.ts
|
|
80
|
+
import { createServer } from 'experimental-a2/server'
|
|
81
|
+
import { orders } from './contracts'
|
|
82
|
+
import { ordersReducer } from './reducer'
|
|
83
|
+
|
|
84
|
+
export const ordersServer = createServer({
|
|
85
|
+
contract: orders,
|
|
86
|
+
handlers: {
|
|
87
|
+
created: async ({ event, session }) => {
|
|
88
|
+
const atTrigger = await session.state(ordersReducer)
|
|
89
|
+
// atTrigger.index === event.index
|
|
90
|
+
|
|
91
|
+
const latest = await session.state(ordersReducer, { through: 'latest' })
|
|
92
|
+
// latest also folds events appended after this one
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use `{ through: 'latest' }` for joins or other logic that intentionally
|
|
99
|
+
observes later events. A numeric `{ through: index }` reads any inclusive log
|
|
100
|
+
boundary; `through: 0` returns `initialState`. `state()` is observational: it
|
|
101
|
+
never runs handlers or waits for pending work, and its index marks committed
|
|
102
|
+
history, not handler completion.
|
|
103
|
+
|
|
104
|
+
A latest read and a following `ctx.session.append(name, ...events)` are
|
|
105
|
+
separate operations, and concurrent appends and retries can move the frontier
|
|
106
|
+
between them. For joins, read with `{ through: 'latest' }`, use a monotone
|
|
107
|
+
readiness check (once ready, always ready), and give the output event a stable
|
|
108
|
+
explicit `id`. Repeated attempts then converge on the same append.
|
|
109
|
+
|
|
110
|
+
## Snapshots are a cache
|
|
111
|
+
|
|
112
|
+
The store backend caches folded state as snapshots keyed by the reducer's
|
|
113
|
+
`name`, so two reducers over one session never share an entry. Changed the
|
|
114
|
+
fold's logic? Change the name (a `-v2` suffix works). The next read ignores
|
|
115
|
+
the old snapshots, refolds from raw events, and caches under the new name.
|
|
116
|
+
|
|
117
|
+
A missing snapshot, or one `stateSchema` rejects, rebuilds from the log. A
|
|
118
|
+
storage read failure propagates with its backend diagnostics instead: existing
|
|
119
|
+
A2 errors pass through, and others become `STORE_UNAVAILABLE` with the
|
|
120
|
+
original error as their cause.
|
|
121
|
+
|
|
122
|
+
Snapshot write-back runs in platform `waitUntil` after the state is ready. A
|
|
123
|
+
failed write doesn't fail the read: A2 reports it through the `a2.snapshot`
|
|
124
|
+
telemetry span (or `console.error` by default) without snapshot contents, and
|
|
125
|
+
the next read folds the missing tail again.
|
|
126
|
+
|
|
127
|
+
Each session and reducer has one latest snapshot. Older ones exist only while
|
|
128
|
+
handler events that read at their boundary are unfinished (not yet completed
|
|
129
|
+
or dead-lettered). This is automatic: there is no TTL, history limit, or
|
|
130
|
+
retention setting.
|
|
131
|
+
|
|
132
|
+
Deleting every snapshot is always safe. The log rebuilds them.
|
|
133
|
+
|
|
134
|
+
## Events don't change meaning
|
|
135
|
+
|
|
136
|
+
The log is append-only, and `created` means forever what it meant when it was
|
|
137
|
+
written. When a payload needs a new shape, add a new event type (`created.v2`)
|
|
138
|
+
instead of redefining the old one.
|
|
139
|
+
|
|
140
|
+
Reducers accumulate cases over time and never migrate anything: old sessions
|
|
141
|
+
keep their old events, new sessions write new ones, and the same fold handles
|
|
142
|
+
both.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sessions
|
|
3
|
+
description: "A session is one instance of a contract, with its own ordered log, state, and live stream. Collections and queries stay in your database."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// anywhere on the server: a route, a handler, a script
|
|
8
|
+
const order = ordersServer.session('order-42')
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
A session is one instance of the contract (one order, one chat, one import)
|
|
12
|
+
with its own ordered log, state, recovery, and live stream. The id is yours.
|
|
13
|
+
Creating the handle does no I/O; nothing loads until you append, read, or
|
|
14
|
+
stream.
|
|
15
|
+
|
|
16
|
+
## Pick the boundary
|
|
17
|
+
|
|
18
|
+
Put events in one session when they form one history, fold, recovery unit, or
|
|
19
|
+
live view. Split what should be read and recovered independently.
|
|
20
|
+
|
|
21
|
+
A session is not a table or a general query boundary: one `orders` contract can
|
|
22
|
+
serve millions of order sessions through the same server and handlers.
|
|
23
|
+
Collections, joins, search, and constraints across many records belong in
|
|
24
|
+
[your database](#sessions-and-your-database).
|
|
25
|
+
|
|
26
|
+
## Clearing a session
|
|
27
|
+
|
|
28
|
+
```ts server/clear-order.ts
|
|
29
|
+
import { ordersServer } from './orders'
|
|
30
|
+
|
|
31
|
+
export async function clearOrder({ orderId, lastIndex }: {
|
|
32
|
+
orderId: string
|
|
33
|
+
lastIndex: number
|
|
34
|
+
}) {
|
|
35
|
+
// here's where you'd do auth, or any other checks
|
|
36
|
+
return ordersServer.session(orderId).clear({ ifLastIndex: lastIndex })
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`session.clear()` ends a session's current lifetime: its history is gone, every
|
|
41
|
+
reducer is back at its initial state, and the id keeps working for the next
|
|
42
|
+
lifetime. It appends a reserved marker, so indexes never restart: clearing a
|
|
43
|
+
session whose last event is `42` returns `{ cleared: true, index: 43 }`, and the
|
|
44
|
+
next event is `44`.
|
|
45
|
+
|
|
46
|
+
Pass `ifLastIndex` when newer events must survive. The store checks it in the
|
|
47
|
+
same atomic step that appends the marker; if another event landed first, the
|
|
48
|
+
clear skips and returns `{ cleared: false, index }` with the current head. A
|
|
49
|
+
conditional clear is safe to retry.
|
|
50
|
+
|
|
51
|
+
The old lifetime's pending events are never dispatched, and a handler still
|
|
52
|
+
running for one can't append, return events, or read the new lifetime's state.
|
|
53
|
+
Clearing can't stop code that is already running or undo its side effects.
|
|
54
|
+
|
|
55
|
+
Connected clients reset when the marker reaches them. After a clear,
|
|
56
|
+
`session.state()` also returns `clearIndex`: pass the whole result as a client's
|
|
57
|
+
`initial` so a cached client drops its old history too (the automatic snapshot
|
|
58
|
+
on subscribe already carries it).
|
|
59
|
+
|
|
60
|
+
`clear()` resolves once the old events and cached state are deleted; appends
|
|
61
|
+
and reads keep running meanwhile, and the cost grows with the old history. If
|
|
62
|
+
deletion fails partway, the session is still cleared and the leftovers are
|
|
63
|
+
invisible: retry, or clear again later, to finish. For a very large session,
|
|
64
|
+
prefer a [scheduled clear](#scheduling-a-clear), because the provider retries
|
|
65
|
+
it and each retry resumes the deletion. Presence isn't cleared; it keeps its
|
|
66
|
+
own expiry.
|
|
67
|
+
|
|
68
|
+
`server.fetch` has no clear endpoint. Clearing is a server operation, so
|
|
69
|
+
authorize your own route before calling it.
|
|
70
|
+
|
|
71
|
+
### Scheduling a clear
|
|
72
|
+
|
|
73
|
+
With a [scheduler](/server/production) configured, a handler can schedule a
|
|
74
|
+
clear. This one clears an idle cart a day after its last activity:
|
|
75
|
+
|
|
76
|
+
```ts server/carts.ts
|
|
77
|
+
import * as a2 from 'experimental-a2'
|
|
78
|
+
import { createServer } from 'experimental-a2/server'
|
|
79
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
80
|
+
import { z } from 'zod'
|
|
81
|
+
|
|
82
|
+
export const carts = a2.contract({
|
|
83
|
+
name: 'carts',
|
|
84
|
+
events: {
|
|
85
|
+
'item.added': z.object({ sku: z.string() }),
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
export const cartsServer = createServer({
|
|
90
|
+
contract: carts,
|
|
91
|
+
scheduler: vercelQueues(),
|
|
92
|
+
handlers: {
|
|
93
|
+
'item.added': async ({ event, session }) => {
|
|
94
|
+
await session.schedule.clear('idle', {
|
|
95
|
+
delay: '1d',
|
|
96
|
+
ifLastIndex: event.index,
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The delay counts from the event's stored time. A later event moves the head
|
|
104
|
+
past `event.index`, so this delivery skips and that event's handler schedules
|
|
105
|
+
the next check; only events with a handler extend the policy. A handler's own
|
|
106
|
+
writes move the head too, so one that appends should pass the index its last
|
|
107
|
+
append returned.
|
|
108
|
+
|
|
109
|
+
Delivery runs the same conditional clear and finishes the deletion before it
|
|
110
|
+
acknowledges, so provider retries complete an interrupted delete. No event is
|
|
111
|
+
appended. The clear happens at delivery, not at an exact deadline, within
|
|
112
|
+
provider limits: Vercel Queues accepts delays up to six days.
|
|
113
|
+
|
|
114
|
+
## Sessions and your database
|
|
115
|
+
|
|
116
|
+
A2 does not replace your application database. It syncs one known session at a
|
|
117
|
+
time (its events, derived state, and optimistic browser writes), not tables or
|
|
118
|
+
queries. Give each fact one owner:
|
|
119
|
+
|
|
120
|
+
- If A2 owns it, append an event and derive state with a reducer.
|
|
121
|
+
- If your database owns it, write it with the database's normal transaction.
|
|
122
|
+
|
|
123
|
+
Never make both paths authoritative for the same fact.
|
|
124
|
+
|
|
125
|
+
Use A2 when people open something by id and watch it progress: an order in
|
|
126
|
+
fulfillment, an agent producing a response, an approval waiting for a decision,
|
|
127
|
+
an import that can fail and resume. Use your database when people list,
|
|
128
|
+
filter, join, and edit many records, and for constraints that must hold across
|
|
129
|
+
rows before a request returns.
|
|
130
|
+
|
|
131
|
+
`experimental-a2/store-postgres` stores A2's own event logs. It does not watch
|
|
132
|
+
your tables, turn their changes into events, or combine an A2 append with your
|
|
133
|
+
SQL transaction. Both can share one Postgres database as separate data models
|
|
134
|
+
and transaction boundaries.
|
|
135
|
+
|
|
136
|
+
### An issue tracker
|
|
137
|
+
|
|
138
|
+
Postgres usually owns an issue tracker's core data; A2 sessions own the runs
|
|
139
|
+
attached to it:
|
|
140
|
+
|
|
141
|
+
| Part | Owner |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| issues, projects, labels, comments | Postgres |
|
|
144
|
+
| permissions, board filters, search | Postgres |
|
|
145
|
+
| an AI investigation attached to an issue | one A2 session |
|
|
146
|
+
| an import or automation run | one A2 session |
|
|
147
|
+
|
|
148
|
+
The investigation's session refers to the issue id without becoming a second
|
|
149
|
+
owner of the issue. A2 can own individual issues when their history and live
|
|
150
|
+
state are the center of the product, but boards and search then span many
|
|
151
|
+
sessions and need another query model. Don't put a whole workspace in one
|
|
152
|
+
session for cross-issue queries: every event joins one growing log that every
|
|
153
|
+
client follows.
|
|
154
|
+
|
|
155
|
+
### Database writes from handlers
|
|
156
|
+
|
|
157
|
+
Handlers may call your database like any other side effect. They are
|
|
158
|
+
at-least-once, so make the write idempotent with `event.id`; it runs after the
|
|
159
|
+
event is durable and may be retried. That is not one atomic write across A2
|
|
160
|
+
and your tables: if a relational change must commit before the request
|
|
161
|
+
succeeds, make it through the database's write path.
|
|
162
|
+
|
|
163
|
+
### Cross-session indexes
|
|
164
|
+
|
|
165
|
+
A handler can maintain a small database index over A2-owned sessions, such as a
|
|
166
|
+
list of active imports. Treat it as an advanced integration:
|
|
167
|
+
|
|
168
|
+
- it is eventually consistent with the session logs;
|
|
169
|
+
- writes must be idempotent;
|
|
170
|
+
- A2 orders events within one session, not across sessions;
|
|
171
|
+
- no A2 API scans and replays every session to rebuild it.
|
|
172
|
+
|
|
173
|
+
Back up and migrate it like application data. If search, joins, and
|
|
174
|
+
cross-session reporting are central, the database should probably own that
|
|
175
|
+
domain.
|