experimental-a2 0.0.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 +128 -0
- package/dist/ai-server.browser.d.ts +1 -0
- package/dist/ai-server.browser.js +4 -0
- package/dist/ai-server.d.ts +65 -0
- package/dist/ai-server.js +494 -0
- package/dist/ai.d.ts +282 -0
- package/dist/ai.js +922 -0
- package/dist/cache-indexeddb.d.ts +1 -0
- package/dist/cache-indexeddb.js +0 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.js +410 -0
- package/dist/contract-B0kAXoaL.js +60 -0
- package/dist/contract-DL8btVd9.d.ts +161 -0
- package/dist/devtools-server.browser.d.ts +1 -0
- package/dist/devtools-server.browser.js +4 -0
- package/dist/devtools-server.d.ts +22 -0
- package/dist/devtools-server.js +1087 -0
- package/dist/errors-BJRMd-h6.js +23 -0
- package/dist/errors-xL_JTXsY.d.ts +20 -0
- package/dist/http.d.ts +44 -0
- package/dist/http.js +119 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/inspection-E7qbD0Xj.js +10 -0
- package/dist/internal-Dm8Ejnud.js +36 -0
- package/dist/log-Dg1I8NRr.d.ts +245 -0
- package/dist/log-memory.d.ts +11 -0
- package/dist/log-memory.js +345 -0
- package/dist/log-polling-RO7kclzR.js +83 -0
- package/dist/log-postgres.d.ts +40 -0
- package/dist/log-postgres.js +628 -0
- package/dist/log-redis.d.ts +31 -0
- package/dist/log-redis.js +711 -0
- package/dist/log-sqlite.d.ts +17 -0
- package/dist/log-sqlite.js +450 -0
- package/dist/log-yJbXUf72.js +5 -0
- package/dist/otel.d.ts +12 -0
- package/dist/otel.js +41 -0
- package/dist/react.d.ts +54 -0
- package/dist/react.js +85 -0
- package/dist/recovery-vercel.d.ts +60 -0
- package/dist/recovery-vercel.js +120 -0
- package/dist/retryable-lazy-DZWmHpii.js +19 -0
- package/dist/server-DYsnKTTy.js +780 -0
- package/dist/server.browser.d.ts +1 -0
- package/dist/server.browser.js +11 -0
- package/dist/server.d.ts +136 -0
- package/dist/server.js +2 -0
- package/dist/telemetry-C78al20p.d.ts +32 -0
- package/dist/validate-XKT4FSNn.js +28 -0
- package/dist/wire-2QpU1EtJ.js +62 -0
- package/docs/01-quickstart.mdx +214 -0
- package/docs/concepts/01-contracts.mdx +138 -0
- package/docs/concepts/02-handlers.mdx +146 -0
- package/docs/concepts/03-durability.mdx +230 -0
- package/docs/concepts/04-state.mdx +133 -0
- package/docs/guides/01-timers.mdx +85 -0
- package/docs/guides/02-cancellation.mdx +107 -0
- package/docs/guides/03-react.mdx +234 -0
- package/docs/guides/04-local-first.mdx +88 -0
- package/docs/guides/05-production.mdx +179 -0
- package/docs/guides/06-ai-agents.mdx +659 -0
- package/docs/guides/07-devtools.mdx +101 -0
- package/docs/guides/08-application-data.mdx +114 -0
- package/docs/index.mdx +282 -0
- package/docs/reference/01-api.mdx +637 -0
- package/docs/reference/02-errors.mdx +77 -0
- package/package.json +111 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Devtools
|
|
3
|
+
description: Mount one read-only handler to inspect sessions, event lifecycles, failures, and snapshots from the durable log.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Mount the dashboard
|
|
7
|
+
|
|
8
|
+
The devtools application is one HTTP handler. Give it every A2 server you want
|
|
9
|
+
to inspect, then mount it on a route that matches the root and its child paths.
|
|
10
|
+
In Next.js, an optional catch-all route does both:
|
|
11
|
+
|
|
12
|
+
```ts app/api/a2/devtools/[[...path]]/route.ts
|
|
13
|
+
import { createDevtools } from 'experimental-a2/devtools/server'
|
|
14
|
+
import { billingServer, ordersServer } from '@/server'
|
|
15
|
+
|
|
16
|
+
const devtools = createDevtools({
|
|
17
|
+
servers: [ordersServer, billingServer],
|
|
18
|
+
authorize: async (_request) => {
|
|
19
|
+
// Return true after your auth or any other checks.
|
|
20
|
+
return process.env.NODE_ENV !== 'production'
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const GET = devtools.handler()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open `/api/a2/devtools`. There is no client factory, provider, React component,
|
|
28
|
+
or separate datastore. The handler serves the browser application itself and
|
|
29
|
+
reads the same log as the servers.
|
|
30
|
+
|
|
31
|
+
The initial HTML contains the first page of the selected contract's sessions
|
|
32
|
+
and the selected session's durable detail. The browser starts from that
|
|
33
|
+
bootstrap data without a loading screen or an initial API request waterfall,
|
|
34
|
+
then takes over navigation and live updates.
|
|
35
|
+
|
|
36
|
+
The selected contract and session stay in the URL. Refreshing preserves the
|
|
37
|
+
view, links open the same session, and browser back and forward navigation work.
|
|
38
|
+
|
|
39
|
+
Omit `authorize` for the same development-only behavior shown above. An
|
|
40
|
+
unguarded handler returns 404 in production. In a deployed dashboard, provide
|
|
41
|
+
your application's access check. Returning `false` hides the route with a 404;
|
|
42
|
+
returning a `Response` supports a redirect or authentication challenge.
|
|
43
|
+
|
|
44
|
+
## Causal forest
|
|
45
|
+
|
|
46
|
+
Every stored `cause` connects a child to the event index and handler attempt
|
|
47
|
+
whose `ctx.append` first persisted it. The dashboard builds that forest
|
|
48
|
+
directly from the session log. It does not need a trace table or another write
|
|
49
|
+
on the append path.
|
|
50
|
+
|
|
51
|
+
A null cause appears as **origin unknown**. New top-level appends write null,
|
|
52
|
+
but events written before causal metadata also read as null. The dashboard does
|
|
53
|
+
not guess which one it is. If a cause points outside the visible log, the row
|
|
54
|
+
keeps the stored edge and marks its parent unavailable.
|
|
55
|
+
|
|
56
|
+
## What the timeline means
|
|
57
|
+
|
|
58
|
+
Each row is one durable event. Its bar begins at `createdAt`, when the event
|
|
59
|
+
entered the log, and ends at `processedAt` or `failedAt`. A pending event stays
|
|
60
|
+
open. Markers show the operation boundaries A2 stores:
|
|
61
|
+
|
|
62
|
+
- first and latest dispatch from `firstClaimedAt` and `lastClaimedAt`
|
|
63
|
+
- latest caught failure from `lastFailedAt` and `lastFailedAttempt`
|
|
64
|
+
- completion from `processedAt` and `processedByAttempt`
|
|
65
|
+
- dead-lettering from `failedAt`
|
|
66
|
+
|
|
67
|
+
`attemptCount` counts every durable dispatch claim. `failureCount` counts only
|
|
68
|
+
caught handler failures. A hard crash can increase the first without
|
|
69
|
+
increasing the second. The dashboard calls that difference a dispatch without
|
|
70
|
+
an outcome because it may be active work, a hard crash, or a superseded
|
|
71
|
+
attempt. It does not claim to know which one occurred.
|
|
72
|
+
|
|
73
|
+
Migrated events can have an `attemptCount` without a `firstClaimedAt`. The
|
|
74
|
+
dashboard shows the first dispatch time as unknown instead of moving it to
|
|
75
|
+
`createdAt` or `lastClaimedAt`.
|
|
76
|
+
|
|
77
|
+
The bar is event lifetime, not handler execution time. A2 stores lifecycle
|
|
78
|
+
summaries, not one span row for every attempt. Time spent waiting, running a
|
|
79
|
+
handler, coordinating a lease, or waiting for recovery remains one honest
|
|
80
|
+
interval.
|
|
81
|
+
|
|
82
|
+
Snapshots appear as their reducer name, event frontier, and last update time.
|
|
83
|
+
The dashboard does not send cached snapshot state to the browser.
|
|
84
|
+
|
|
85
|
+
## Live updates
|
|
86
|
+
|
|
87
|
+
The selected session opens an SSE connection to the same handler. The stream
|
|
88
|
+
sends invalidations, not a second copy of state. On each invalidation the
|
|
89
|
+
browser reads the durable session again. A disconnected browser can miss every
|
|
90
|
+
frame and still become correct immediately after reconnecting.
|
|
91
|
+
|
|
92
|
+
While work is pending, the handler checks processing metadata quickly. It
|
|
93
|
+
backs off after the session settles and emits heartbeats for intermediaries.
|
|
94
|
+
Only sessions someone is viewing have a live inspection loop.
|
|
95
|
+
|
|
96
|
+
## Custom logs
|
|
97
|
+
|
|
98
|
+
The memory, SQLite, Postgres, and Redis adapters provide the optional read-only
|
|
99
|
+
inspection interface. A custom `A2Log` can implement `inspect.listSessions`
|
|
100
|
+
and `inspect.listSnapshots` to appear in the dashboard. Logs without inspection
|
|
101
|
+
continue to work normally; their devtools endpoint returns 501.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A2 and your database
|
|
3
|
+
description: Use A2 for things with a lifecycle. Keep collections, relationships, and queries in your application database.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Start with ownership
|
|
7
|
+
|
|
8
|
+
A2 does not replace your application database. It synchronizes one known
|
|
9
|
+
session at a time: its events, its derived state, and optimistic writes from
|
|
10
|
+
the browser. It does not synchronize tables or database queries.
|
|
11
|
+
|
|
12
|
+
Use your database for the records and relationships that the rest of the
|
|
13
|
+
application queries.
|
|
14
|
+
|
|
15
|
+
Decide which system owns each fact:
|
|
16
|
+
|
|
17
|
+
- If A2 owns it, append an event and derive the session's state with a reducer.
|
|
18
|
+
- If your database owns it, write it with the database's normal transaction.
|
|
19
|
+
|
|
20
|
+
Do not make both paths authoritative for the same fact.
|
|
21
|
+
|
|
22
|
+
## What belongs in A2
|
|
23
|
+
|
|
24
|
+
A2 fits something with a lifecycle: an order moving through fulfillment, an
|
|
25
|
+
agent producing a response, an approval waiting for a decision, or an import
|
|
26
|
+
that can fail and resume.
|
|
27
|
+
|
|
28
|
+
A **contract** names the events in one kind of lifecycle. A **session** is one
|
|
29
|
+
instance of it:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
orders / order-42
|
|
33
|
+
orders / order-43
|
|
34
|
+
agents / run-abc
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The session is the boundary A2 orders, recovers, folds, and streams. Put events
|
|
38
|
+
in the same session when they must be ordered or viewed together. Split work
|
|
39
|
+
that should progress independently.
|
|
40
|
+
|
|
41
|
+
A contract is not a table. A session is not a table either. One contract
|
|
42
|
+
usually serves many sessions, and one session may describe changes that touch
|
|
43
|
+
several records elsewhere in an application.
|
|
44
|
+
|
|
45
|
+
## What belongs in the database
|
|
46
|
+
|
|
47
|
+
Use your application database for collections and relationships: accounts,
|
|
48
|
+
projects, issue lists, labels, permissions, search, joins, pagination, and
|
|
49
|
+
reporting. Use its transactions for constraints that must hold across several
|
|
50
|
+
rows before a request returns.
|
|
51
|
+
|
|
52
|
+
The Postgres adapter in `experimental-a2/log-postgres` stores A2's own logs. It does not
|
|
53
|
+
watch your application tables, turn their changes into events, or combine an
|
|
54
|
+
A2 append with your SQL transaction. The A2 log and your tables may share one
|
|
55
|
+
Postgres database, but they remain separate data models and transaction
|
|
56
|
+
boundaries.
|
|
57
|
+
|
|
58
|
+
## An issue tracker
|
|
59
|
+
|
|
60
|
+
An issue tracker is mostly a collection and query product. Postgres should
|
|
61
|
+
usually own its core data:
|
|
62
|
+
|
|
63
|
+
| Part | Owner |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| issues, projects, labels, comments | Postgres |
|
|
66
|
+
| permissions, board filters, search | Postgres |
|
|
67
|
+
| an AI investigation attached to an issue | one A2 session |
|
|
68
|
+
| an import or automation run | one A2 session |
|
|
69
|
+
|
|
70
|
+
The A2 session can store the investigation's messages, progress, tool calls,
|
|
71
|
+
approval, and result. It refers to the issue id, but it does not become a
|
|
72
|
+
second owner of the issue.
|
|
73
|
+
|
|
74
|
+
An individual issue also has a lifecycle, so A2 can own it when the issue's
|
|
75
|
+
ordered history and live state are the center of the product. That choice has
|
|
76
|
+
a cost: boards and search span many sessions and need another query model. If
|
|
77
|
+
those collection views are the center of the product, let the database lead.
|
|
78
|
+
|
|
79
|
+
Do not put an entire workspace in one session just to make cross-issue queries
|
|
80
|
+
possible. Every event in one session is ordered together, and every client of
|
|
81
|
+
that session follows the same growing log.
|
|
82
|
+
|
|
83
|
+
## Database writes from handlers
|
|
84
|
+
|
|
85
|
+
Handlers may call your database like any other side effect. They are
|
|
86
|
+
at-least-once, so make the write idempotent with `event.id`. The event is
|
|
87
|
+
durable before the handler runs, and the SQL write may happen later or be
|
|
88
|
+
retried.
|
|
89
|
+
|
|
90
|
+
That is a good fit for a reaction to a lifecycle event. It is not one atomic
|
|
91
|
+
write across A2 and your tables. If a relational change must commit before the
|
|
92
|
+
request succeeds, make that change through the database's write path.
|
|
93
|
+
|
|
94
|
+
## Cross-session indexes
|
|
95
|
+
|
|
96
|
+
Sometimes an A2-owned lifecycle needs a small database index, such as a list of
|
|
97
|
+
active imports. A handler can maintain that index. Treat it as an advanced
|
|
98
|
+
integration:
|
|
99
|
+
|
|
100
|
+
- it is eventually consistent with the session logs;
|
|
101
|
+
- writes must be idempotent;
|
|
102
|
+
- A2 orders events within one session, not across sessions;
|
|
103
|
+
- A2 does not expose one API that scans and replays every session to rebuild
|
|
104
|
+
the index.
|
|
105
|
+
|
|
106
|
+
Back up and migrate that index like application data. If search, joins, and
|
|
107
|
+
cross-session reporting are central rather than incidental, the database
|
|
108
|
+
should probably own that domain.
|
|
109
|
+
|
|
110
|
+
## The short version
|
|
111
|
+
|
|
112
|
+
Use A2 when people open something by id and watch it progress. Use your
|
|
113
|
+
database when people list, filter, join, and edit many records. In an
|
|
114
|
+
application that needs both, split ownership along that line.
|
package/docs/index.mdx
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Introduction
|
|
3
|
+
description: A2 is durable sync and reactions for things with a lifecycle. Append events, run handlers, and keep one live view from server to browser.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add experimental-a2
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
A2 is durable sync and reactions for things with a lifecycle.
|
|
11
|
+
|
|
12
|
+
An order, agent run, approval, or import gets a session. You append events.
|
|
13
|
+
Handlers react to them, and usually append the next one. The browser follows
|
|
14
|
+
the same log and folds the same state as the server. That's the whole model;
|
|
15
|
+
the rest of this page is it happening.
|
|
16
|
+
|
|
17
|
+
There is no declared state machine. Events record facts, reducers compute the
|
|
18
|
+
current view, and handlers perform the reactions.
|
|
19
|
+
|
|
20
|
+
## Define a contract
|
|
21
|
+
|
|
22
|
+
Events are the vocabulary. A contract names them once, as a plain
|
|
23
|
+
importable value. The server implements it, and the browser types itself
|
|
24
|
+
off it later.
|
|
25
|
+
|
|
26
|
+
```ts contracts.ts
|
|
27
|
+
import { z } from 'zod'
|
|
28
|
+
import * as a2 from 'experimental-a2'
|
|
29
|
+
|
|
30
|
+
export const orders = a2.contract({
|
|
31
|
+
name: 'orders',
|
|
32
|
+
events: {
|
|
33
|
+
created: z.object({ items: z.array(z.string()) }),
|
|
34
|
+
'shop.notified': z.object({}),
|
|
35
|
+
cancelled: z.object({}),
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## React to events
|
|
41
|
+
|
|
42
|
+
Handlers are plain async functions: no determinism rules, no replay, no
|
|
43
|
+
wrappers around side effects. Each one reacts to a fact and usually
|
|
44
|
+
appends the next one.
|
|
45
|
+
|
|
46
|
+
```ts server/orders.ts
|
|
47
|
+
import { createServer } from 'experimental-a2/server'
|
|
48
|
+
import { orders } from '@/contracts'
|
|
49
|
+
|
|
50
|
+
export const ordersServer = createServer({
|
|
51
|
+
contract: orders,
|
|
52
|
+
handlers: {
|
|
53
|
+
created: async ({ event, append }) => {
|
|
54
|
+
// your side effect; event.id makes a stable idempotency key:
|
|
55
|
+
// await notifyShop(event.payload, { idempotencyKey: event.id })
|
|
56
|
+
await append({ type: 'shop.notified', payload: {} })
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Open a session
|
|
63
|
+
|
|
64
|
+
A session is one instance of the contract, with its own log and its own
|
|
65
|
+
ordering. It is the thing A2 keeps durable and live. The id is yours:
|
|
66
|
+
|
|
67
|
+
- `session('order-8194')`: an order moving through fulfillment
|
|
68
|
+
- `session('chat-a8c1')`: an agent chat streaming its reply
|
|
69
|
+
- `session('cart-julian')`: a shopping cart
|
|
70
|
+
- `session('doc-roadmap')`: a document with three people typing in it
|
|
71
|
+
|
|
72
|
+
Anything whose story spans more than one request. Put events in the same
|
|
73
|
+
session when they must be ordered or viewed together. Any server code can open
|
|
74
|
+
one and append: a route handler, a webhook, a cron job.
|
|
75
|
+
|
|
76
|
+
```ts app/api/orders/route.ts
|
|
77
|
+
import { ordersServer } from '@/server/orders'
|
|
78
|
+
|
|
79
|
+
export async function POST(req: Request) {
|
|
80
|
+
const body = await req.json()
|
|
81
|
+
await ordersServer.session(body.orderId).append({
|
|
82
|
+
type: 'created',
|
|
83
|
+
payload: { items: body.items },
|
|
84
|
+
})
|
|
85
|
+
return Response.json({ ok: true })
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That append is durable. The event hits the log before anything else happens,
|
|
90
|
+
then A2 starts its handler inline. In production, queue-backed recovery wakes
|
|
91
|
+
pending work after a crash or timeout.
|
|
92
|
+
|
|
93
|
+
## State is a fold
|
|
94
|
+
|
|
95
|
+
There is no separate state record for the session to keep in sync. Fold its log
|
|
96
|
+
through a reducer, a pure function derived from the contract. It imports
|
|
97
|
+
nothing server-only, which is about to matter.
|
|
98
|
+
|
|
99
|
+
```ts reducer.ts
|
|
100
|
+
import { orders } from '@/contracts'
|
|
101
|
+
|
|
102
|
+
export const orderStatus = orders
|
|
103
|
+
.reducer({ name: 'order-status', initialState: { status: 'new' } })
|
|
104
|
+
.fold((state, event) => {
|
|
105
|
+
switch (event.type) {
|
|
106
|
+
case 'created':
|
|
107
|
+
return { status: 'created' }
|
|
108
|
+
case 'shop.notified':
|
|
109
|
+
return { status: 'notified' }
|
|
110
|
+
case 'cancelled':
|
|
111
|
+
return { status: 'cancelled' }
|
|
112
|
+
default:
|
|
113
|
+
return state
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// anywhere on the server:
|
|
120
|
+
import { ordersServer } from '@/server/orders'
|
|
121
|
+
import { orderStatus } from '@/reducer'
|
|
122
|
+
|
|
123
|
+
const { state } = await ordersServer.session(orderId).state(orderStatus)
|
|
124
|
+
// state → { status: 'notified' }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## The browser joins the session
|
|
128
|
+
|
|
129
|
+
The reducer is isomorphic, so the browser runs the same fold the server
|
|
130
|
+
does. A session streams to the client live and takes optimistic writes,
|
|
131
|
+
all typed by the one contract:
|
|
132
|
+
|
|
133
|
+
```ts app/orders/[orderId]/session.ts
|
|
134
|
+
'use client'
|
|
135
|
+
import { createReact } from 'experimental-a2/react'
|
|
136
|
+
import { orderStatus } from '@/reducer'
|
|
137
|
+
|
|
138
|
+
export const { SessionProvider, useSession } = createReact({
|
|
139
|
+
reducer: orderStatus,
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```tsx app/orders/[orderId]/order-client.tsx
|
|
144
|
+
'use client'
|
|
145
|
+
import { useSession } from './session'
|
|
146
|
+
|
|
147
|
+
export function OrderStatus() {
|
|
148
|
+
const { state, connection, push } = useSession()
|
|
149
|
+
return (
|
|
150
|
+
<div>
|
|
151
|
+
{state.status}
|
|
152
|
+
{connection.status === 'live' ? ' · live' : ''}
|
|
153
|
+
<button onClick={() => void push({ type: 'cancelled', payload: {} })}>
|
|
154
|
+
Cancel order
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`push` applies instantly, validates against the contract before anything
|
|
162
|
+
leaves the browser, and rolls back if the server says no. Mounting
|
|
163
|
+
`SessionProvider` and the small stream route it talks to takes about
|
|
164
|
+
twenty lines; [Live UI](/guides/react) wires it end to end.
|
|
165
|
+
|
|
166
|
+
## The properties
|
|
167
|
+
|
|
168
|
+
- **Handlers are plain async functions.** If it runs on your laptop, it
|
|
169
|
+
runs in production.
|
|
170
|
+
- **History is real.** [`history()`](/concepts/state) returns what
|
|
171
|
+
actually happened, in order. Debugging is reading, not reconstructing.
|
|
172
|
+
- **Nothing inside a session is sacred except its log.** Snapshots, leases, queue
|
|
173
|
+
messages: all disposable caches. Delete them and A2 rebuilds
|
|
174
|
+
everything from the log.
|
|
175
|
+
|
|
176
|
+
## FAQ
|
|
177
|
+
|
|
178
|
+
<details>
|
|
179
|
+
<summary>Why not a workflow engine?</summary>
|
|
180
|
+
|
|
181
|
+
Workflow engines replay your code from the top on every wake-up. So the
|
|
182
|
+
code has to be deterministic, so every side effect gets wrapped in a step
|
|
183
|
+
function, and `sleep()` becomes something magical instead of something
|
|
184
|
+
you'd never call in a serverless function.
|
|
185
|
+
|
|
186
|
+
A2's answer is older and simpler: write everything down. Every meaningful
|
|
187
|
+
thing that happens is an event in a log. Handlers are stateless functions
|
|
188
|
+
that react to one event at a time. State isn't stored. It's computed, by
|
|
189
|
+
folding over the log whenever you need it. There's no orchestrator to
|
|
190
|
+
operate.
|
|
191
|
+
|
|
192
|
+
</details>
|
|
193
|
+
|
|
194
|
+
<details>
|
|
195
|
+
<summary>Isn't this just event sourcing?</summary>
|
|
196
|
+
|
|
197
|
+
It's the useful core of it. A log of facts, state as a fold: the idea is
|
|
198
|
+
decades old, and it's a good one. A2 cuts the ceremony that made it a big
|
|
199
|
+
commitment. No command bus, no projection cluster, no upcasting
|
|
200
|
+
framework. A contract, a log, handlers, reducers.
|
|
201
|
+
|
|
202
|
+
</details>
|
|
203
|
+
|
|
204
|
+
<details>
|
|
205
|
+
<summary>How do I wait five days?</summary>
|
|
206
|
+
|
|
207
|
+
You don't sleep. You schedule an event. Anything that can deliver an
|
|
208
|
+
HTTP call later (QStash, a cron, a payment provider's webhook) hits a
|
|
209
|
+
route that appends. The scheduled thing is data, a session id plus an
|
|
210
|
+
event, not a suspended function. See [Timers](/guides/timers).
|
|
211
|
+
|
|
212
|
+
</details>
|
|
213
|
+
|
|
214
|
+
<details>
|
|
215
|
+
<summary>How do I cancel work that's already running?</summary>
|
|
216
|
+
|
|
217
|
+
Cancellation is an event too: same route, same auth, same log. Handlers
|
|
218
|
+
opt in with `abortOn` and receive a live `AbortSignal`; reducers cut the
|
|
219
|
+
derived view exactly. See [Cancellation](/guides/cancellation).
|
|
220
|
+
|
|
221
|
+
</details>
|
|
222
|
+
|
|
223
|
+
<details>
|
|
224
|
+
<summary>What happens when a handler keeps failing?</summary>
|
|
225
|
+
|
|
226
|
+
It retries with backoff, then dead-letters after ten caught failures, and the
|
|
227
|
+
session stalls at that event, visibly, instead of silently skipping it.
|
|
228
|
+
[Durability](/concepts/durability) has the exact guarantees and the
|
|
229
|
+
recovery story behind them.
|
|
230
|
+
|
|
231
|
+
</details>
|
|
232
|
+
|
|
233
|
+
<details>
|
|
234
|
+
<summary>What does production need?</summary>
|
|
235
|
+
|
|
236
|
+
Two decisions: a log backend (Postgres or Redis) and queue-backed
|
|
237
|
+
recovery. Development needs neither. SQLite appears under `.a2/` and
|
|
238
|
+
state survives restarts. See [Going to production](/guides/production).
|
|
239
|
+
|
|
240
|
+
</details>
|
|
241
|
+
|
|
242
|
+
<details>
|
|
243
|
+
<summary>What does A2 sync?</summary>
|
|
244
|
+
|
|
245
|
+
One session whose id the application already knows. The browser follows its
|
|
246
|
+
events, folds them into live state, and can push changes optimistically. A2
|
|
247
|
+
does not replicate database tables or subscribe to queries across sessions.
|
|
248
|
+
|
|
249
|
+
</details>
|
|
250
|
+
|
|
251
|
+
<details>
|
|
252
|
+
<summary>Where does my application database fit?</summary>
|
|
253
|
+
|
|
254
|
+
A2 does not replace it. Use A2 for things with a lifecycle. Keep collections,
|
|
255
|
+
relationships, constraints, search, and reporting in your application
|
|
256
|
+
database. Split ownership instead of writing the same fact through both paths.
|
|
257
|
+
See [A2 and your database](/guides/application-data).
|
|
258
|
+
|
|
259
|
+
</details>
|
|
260
|
+
|
|
261
|
+
## Where to next
|
|
262
|
+
|
|
263
|
+
<CardGroup cols={2}>
|
|
264
|
+
<Card title="Quickstart" href="/quickstart" icon="rocket">
|
|
265
|
+
A contract, a handler, and a durable append in five minutes.
|
|
266
|
+
</Card>
|
|
267
|
+
<Card title="Contracts and sessions" href="/concepts/contracts" icon="folder">
|
|
268
|
+
The two nouns you'll use everywhere.
|
|
269
|
+
</Card>
|
|
270
|
+
<Card title="Durability" href="/concepts/durability" icon="wrench">
|
|
271
|
+
What A2 guarantees, and exactly how.
|
|
272
|
+
</Card>
|
|
273
|
+
<Card title="Live UI with React" href="/guides/react" icon="lightbulb">
|
|
274
|
+
Stream a session to the browser with optimistic writes.
|
|
275
|
+
</Card>
|
|
276
|
+
<Card title="Durable AI agents" href="/guides/ai-agents" icon="bot">
|
|
277
|
+
Run AI SDK agents with durable messages, progress, tools, and approvals.
|
|
278
|
+
</Card>
|
|
279
|
+
<Card title="A2 and your database" href="/guides/application-data" icon="database">
|
|
280
|
+
Decide what belongs in a session and what belongs in ordinary tables.
|
|
281
|
+
</Card>
|
|
282
|
+
</CardGroup>
|