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.
Files changed (68) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/ai-server.browser.d.ts +1 -0
  3. package/dist/ai-server.browser.js +4 -0
  4. package/dist/ai-server.d.ts +65 -0
  5. package/dist/ai-server.js +494 -0
  6. package/dist/ai.d.ts +282 -0
  7. package/dist/ai.js +922 -0
  8. package/dist/cache-indexeddb.d.ts +1 -0
  9. package/dist/cache-indexeddb.js +0 -0
  10. package/dist/client.d.ts +90 -0
  11. package/dist/client.js +410 -0
  12. package/dist/contract-B0kAXoaL.js +60 -0
  13. package/dist/contract-DL8btVd9.d.ts +161 -0
  14. package/dist/devtools-server.browser.d.ts +1 -0
  15. package/dist/devtools-server.browser.js +4 -0
  16. package/dist/devtools-server.d.ts +22 -0
  17. package/dist/devtools-server.js +1087 -0
  18. package/dist/errors-BJRMd-h6.js +23 -0
  19. package/dist/errors-xL_JTXsY.d.ts +20 -0
  20. package/dist/http.d.ts +44 -0
  21. package/dist/http.js +119 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.js +3 -0
  24. package/dist/inspection-E7qbD0Xj.js +10 -0
  25. package/dist/internal-Dm8Ejnud.js +36 -0
  26. package/dist/log-Dg1I8NRr.d.ts +245 -0
  27. package/dist/log-memory.d.ts +11 -0
  28. package/dist/log-memory.js +345 -0
  29. package/dist/log-polling-RO7kclzR.js +83 -0
  30. package/dist/log-postgres.d.ts +40 -0
  31. package/dist/log-postgres.js +628 -0
  32. package/dist/log-redis.d.ts +31 -0
  33. package/dist/log-redis.js +711 -0
  34. package/dist/log-sqlite.d.ts +17 -0
  35. package/dist/log-sqlite.js +450 -0
  36. package/dist/log-yJbXUf72.js +5 -0
  37. package/dist/otel.d.ts +12 -0
  38. package/dist/otel.js +41 -0
  39. package/dist/react.d.ts +54 -0
  40. package/dist/react.js +85 -0
  41. package/dist/recovery-vercel.d.ts +60 -0
  42. package/dist/recovery-vercel.js +120 -0
  43. package/dist/retryable-lazy-DZWmHpii.js +19 -0
  44. package/dist/server-DYsnKTTy.js +780 -0
  45. package/dist/server.browser.d.ts +1 -0
  46. package/dist/server.browser.js +11 -0
  47. package/dist/server.d.ts +136 -0
  48. package/dist/server.js +2 -0
  49. package/dist/telemetry-C78al20p.d.ts +32 -0
  50. package/dist/validate-XKT4FSNn.js +28 -0
  51. package/dist/wire-2QpU1EtJ.js +62 -0
  52. package/docs/01-quickstart.mdx +214 -0
  53. package/docs/concepts/01-contracts.mdx +138 -0
  54. package/docs/concepts/02-handlers.mdx +146 -0
  55. package/docs/concepts/03-durability.mdx +230 -0
  56. package/docs/concepts/04-state.mdx +133 -0
  57. package/docs/guides/01-timers.mdx +85 -0
  58. package/docs/guides/02-cancellation.mdx +107 -0
  59. package/docs/guides/03-react.mdx +234 -0
  60. package/docs/guides/04-local-first.mdx +88 -0
  61. package/docs/guides/05-production.mdx +179 -0
  62. package/docs/guides/06-ai-agents.mdx +659 -0
  63. package/docs/guides/07-devtools.mdx +101 -0
  64. package/docs/guides/08-application-data.mdx +114 -0
  65. package/docs/index.mdx +282 -0
  66. package/docs/reference/01-api.mdx +637 -0
  67. package/docs/reference/02-errors.mdx +77 -0
  68. package/package.json +111 -0
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,11 @@
1
+ //#region src/server.browser.ts
2
+ /**
3
+ * The browser build of a2/server. There isn't one — on purpose.
4
+ * Contracts and reducers (from `a2`) and the session client (from
5
+ * `a2/client` / `a2/react`) are the isomorphic surface; the server is
6
+ * where handlers and storage live. This module existing in a client
7
+ * bundle means a `'use client'` file (or something it imports)
8
+ * value-imported your server module.
9
+ */
10
+ throw new Error("a2/server is server-only — a client bundle imported it. Import the contract (a2) or the session client (a2/client, a2/react) in browser code instead.");
11
+ //#endregion
@@ -0,0 +1,136 @@
1
+ import { i as EventDefs, n as Contract, r as ContractEvent, s as Reducer, t as AppendInput } from "./contract-DL8btVd9.js";
2
+ import { a as Event, c as IdSource, d as LogStateRead, f as StoredEvent, h as StoredSnapshot, i as Clock, l as LogClaimResult, m as StoredSessionSummary, n as A2LogInspection, o as EventCause, p as StoredSessionPage, r as AppendEvent, s as FailAttemptResult, t as A2Log, u as LogHandoffResult } from "./log-Dg1I8NRr.js";
3
+ import { i as A2Telemetry } from "./telemetry-C78al20p.js";
4
+ //#region src/server.d.ts
5
+ /**
6
+ * Events that arrived over the wire through `parsePushBody` — already
7
+ * envelope-validated, headed for schema validation inside `append`.
8
+ * The brand lets the documented push route hand them straight to
9
+ * `session.append` without weakening typed appends for app code: a
10
+ * hand-written `{ type: string }` literal still fails to compile.
11
+ */
12
+ type PushedEvent = {
13
+ type: string;
14
+ payload: unknown;
15
+ id?: string;
16
+ readonly "~a2.pushed": true;
17
+ };
18
+ /** What every handler receives. */
19
+ type HandlerContext<D extends EventDefs, K extends keyof D & string = keyof D & string> = {
20
+ /** The triggering event. */
21
+ event: ContractEvent<D, K>;
22
+ /** Durable, 1-based dispatch ordinal for this event. */
23
+ attempt: number;
24
+ /** Append what happens next to this session's log. Atomic per call. */
25
+ append: (...events: AppendInput<D>[]) => Promise<ContractEvent<D>[]>;
26
+ /** Every past event in this session, oldest first. Always the raw log. */
27
+ history: () => Promise<ContractEvent<D>[]>;
28
+ /** Fires on `abortOn` events (cancellation slice); dormant otherwise. */
29
+ signal: AbortSignal;
30
+ };
31
+ type Handler<D extends EventDefs, K extends keyof D & string = keyof D & string> = (ctx: HandlerContext<D, K>) => Promise<void>;
32
+ /** A handle on one instance of the machine. Creating it does no I/O. */
33
+ type Session<D extends EventDefs> = {
34
+ readonly sessionId: string;
35
+ append: {
36
+ (...events: AppendInput<D>[]): Promise<ContractEvent<D>[]>;
37
+ /** The push-route path: events from `parsePushBody`. */
38
+ (...events: PushedEvent[]): Promise<ContractEvent<D>[]>;
39
+ };
40
+ history(): Promise<ContractEvent<D>[]>;
41
+ state<S>(reducer: Reducer<D, S>): Promise<{
42
+ state: S;
43
+ index: number;
44
+ }>;
45
+ /**
46
+ * A live feed of this session's events, starting after `startAt`
47
+ * (exclusive). Server-side only — expose it over SSE with
48
+ * `sseResponse` from a2/http.
49
+ */
50
+ stream(opts?: {
51
+ startAt?: number;
52
+ }): AsyncIterable<ContractEvent<D>>;
53
+ };
54
+ /**
55
+ * What recovery needs from a server — structural, so servers of any
56
+ * contract mix in one `handler(...)` call. Deliberately tiny: recovery
57
+ * consults the log through `drain` and nothing else.
58
+ */
59
+ type RecoverableServer = {
60
+ readonly contract: {
61
+ readonly name: string;
62
+ };
63
+ drain(sessionId: string): Promise<{
64
+ settled: boolean;
65
+ }>;
66
+ };
67
+ /**
68
+ * The recovery seam (a2-implementation.md §7, §9). `arm` puts a
69
+ * delayed, coalesced "drain this session" message on durable
70
+ * infrastructure while inline work starts; `handler` returns the route
71
+ * the transport delivers to. Lease holders move the watchdog alongside
72
+ * their renewable execution window. Implementations ship as
73
+ * entry points (`a2/recovery-vercel`); core never imports a transport.
74
+ */
75
+ type A2Recovery = {
76
+ arm(opts: {
77
+ contract: string;
78
+ sessionId: string;
79
+ /** Earliest useful delivery as epoch milliseconds. */
80
+ dueAt?: number;
81
+ }): Promise<void>;
82
+ handler(...servers: RecoverableServer[]): (req: Request) => Promise<Response>;
83
+ };
84
+ type A2Server<D extends EventDefs> = {
85
+ /** The contract this server implements. */
86
+ readonly contract: Contract<D>;
87
+ session(id: string): Session<D>;
88
+ /**
89
+ * Process the session's pending events in order. `settled` means the
90
+ * drain left nothing actionable: every event processed, or the
91
+ * session is stalled at a dead-lettered event (which recovery
92
+ * transports treat as "stop redelivering").
93
+ */
94
+ drain(sessionId: string): Promise<{
95
+ settled: boolean;
96
+ }>;
97
+ };
98
+ /**
99
+ * Which events fire `ctx.signal` while a handler runs — the preemption
100
+ * channel for user cancellation. The array form matches by type; the
101
+ * object form takes per-type predicates for targeted cancellation
102
+ * (`(event, trigger) => event.payload.of === trigger.id`). Handlers
103
+ * without `abortOn` pay nothing. An aborted handler should catch and
104
+ * return normally; throwing means "retry me".
105
+ */
106
+ type AbortSpec<D extends EventDefs, K extends keyof D & string> = Array<keyof D & string> | { [T in keyof D & string]?: true | ((event: ContractEvent<D, T>, trigger: ContractEvent<D, K>) => boolean); };
107
+ type HandlerEntry<D extends EventDefs, K extends keyof D & string = keyof D & string> = Handler<D, K> | {
108
+ abortOn: AbortSpec<D, K>;
109
+ handler: Handler<D, K>;
110
+ };
111
+ type ServerOptions<D extends EventDefs> = {
112
+ /** The contract this server implements (see `a2.contract`). */
113
+ contract: Contract<D>;
114
+ /** Where events live. Defaults: sqlite in dev, memory in tests, required in prod. */
115
+ log?: A2Log;
116
+ /**
117
+ * Queue-backed recovery — e.g. `vercelQueues()` from
118
+ * `a2/recovery-vercel`. Absent means append-driven healing only: a
119
+ * working configuration, but a clockless one. Recommended in
120
+ * production.
121
+ */
122
+ recovery?: A2Recovery;
123
+ /** Optional instrumentation — e.g. `otel()` from `a2/otel`. */
124
+ telemetry?: A2Telemetry;
125
+ /**
126
+ * The reactions, keyed by event type — all present at construction,
127
+ * so a handler can never be silently missing because its module
128
+ * wasn't imported. Compose across files by spreading objects (note:
129
+ * a duplicate key under spread silently last-wins).
130
+ */
131
+ handlers?: { [K in keyof D & string]?: HandlerEntry<D, K>; };
132
+ };
133
+ /** Implement a contract: bind its vocabulary to storage and reactions. */
134
+ declare function createServer<D extends EventDefs>(options: ServerOptions<D>): A2Server<D>;
135
+ //#endregion
136
+ export { type A2Log, type A2LogInspection, A2Recovery, A2Server, AbortSpec, type AppendEvent, type AppendInput, type Clock, type Contract, type ContractEvent, type Event, type EventCause, type EventDefs, type FailAttemptResult, Handler, HandlerContext, HandlerEntry, type IdSource, type LogClaimResult, type LogHandoffResult, type LogStateRead, PushedEvent, RecoverableServer, ServerOptions, Session, type StoredEvent, type StoredSessionPage, type StoredSessionSummary, type StoredSnapshot, createServer };
package/dist/server.js ADDED
@@ -0,0 +1,2 @@
1
+ import { t as createServer } from "./server-DYsnKTTy.js";
2
+ export { createServer };
@@ -0,0 +1,32 @@
1
+ //#region src/telemetry.d.ts
2
+ /**
3
+ * The A2Telemetry interface — the instrumentation seam. Same philosophy
4
+ * as log backends: the interface lives in core, implementations ship as
5
+ * entry points (`a2/otel` adapts it to OpenTelemetry). Without one,
6
+ * every operation runs through a no-op wrapper.
7
+ */
8
+ /** Attribute values a2 emits. */
9
+ type A2AttributeValue = string | number | boolean;
10
+ /** The spans a2 emits today. The catalogue grows with the surface. */
11
+ type A2SpanName = "a2.append" | "a2.drain" | "a2.event" | "a2.state";
12
+ type A2SpanHandle = {
13
+ /** Attach or update an attribute mid-span (e.g. the outcome). */
14
+ setAttribute(key: string, value: A2AttributeValue): void;
15
+ /**
16
+ * Mark the span failed without a2's control flow throwing — used for
17
+ * handler failures, which a2 swallows by design (they go into the
18
+ * retry machinery, not up the stack) but telemetry must still see.
19
+ */
20
+ recordError(error: unknown): void;
21
+ };
22
+ type A2Telemetry = {
23
+ /**
24
+ * Wrap one unit of a2 work. Implementations should time it, record a
25
+ * thrown error as a failure, propagate context so nested spans tree
26
+ * up, and always return `fn`'s result (or rethrow its error) —
27
+ * telemetry observes, it never alters behavior.
28
+ */
29
+ span<T>(name: A2SpanName, attributes: Record<string, A2AttributeValue>, fn: (span: A2SpanHandle) => Promise<T>): Promise<T>;
30
+ };
31
+ //#endregion
32
+ export { A2Telemetry as i, A2SpanHandle as n, A2SpanName as r, A2AttributeValue as t };
@@ -0,0 +1,28 @@
1
+ //#region src/validate.ts
2
+ /**
3
+ * Run a Standard Schema validator, requiring a synchronous result.
4
+ * Validators must be synchronous (specs/a2-api.md §1) — an async one is
5
+ * a definition bug, rejected at definition time where possible and here
6
+ * as a backstop. `what` names the schema in error messages, e.g.
7
+ * "event 'created'" or "the stateSchema".
8
+ */
9
+ function validateSync(schema, value, what) {
10
+ const result = schema["~standard"].validate(value);
11
+ if (result instanceof Promise) throw new TypeError(`the schema for ${what} is asynchronous — a2 schemas must validate synchronously`);
12
+ return result;
13
+ }
14
+ /**
15
+ * Probe a schema to reject always-async validators early. The probe
16
+ * value is deliberately garbage — we only care whether the result is a
17
+ * Promise, not whether it passes.
18
+ */
19
+ function assertSyncSchema(schema, what) {
20
+ const probe = Symbol("a2.schema-probe");
21
+ const result = schema["~standard"].validate(probe);
22
+ if (result instanceof Promise) {
23
+ result.catch(() => {});
24
+ throw new TypeError(`the schema for ${what} is asynchronous — a2 schemas must validate synchronously`);
25
+ }
26
+ }
27
+ //#endregion
28
+ export { validateSync as n, assertSyncSchema as t };
@@ -0,0 +1,62 @@
1
+ import { t as A2Error } from "./errors-BJRMd-h6.js";
2
+ //#region src/wire.ts
3
+ /**
4
+ * The wire format shared by a2/http (server side) and a2/client
5
+ * (browser side): events as JSON with ISO timestamps, and the A2Error
6
+ * envelope. Internal module — both entry points re-export what's
7
+ * public.
8
+ */
9
+ function eventToWire(event) {
10
+ return {
11
+ id: event.id,
12
+ type: event.type,
13
+ payload: event.payload,
14
+ index: event.index,
15
+ sessionId: event.sessionId,
16
+ createdAt: event.createdAt.toISOString()
17
+ };
18
+ }
19
+ function eventFromWire(wire) {
20
+ return {
21
+ id: wire.id,
22
+ type: wire.type,
23
+ payload: wire.payload,
24
+ index: wire.index,
25
+ sessionId: wire.sessionId,
26
+ createdAt: new Date(wire.createdAt)
27
+ };
28
+ }
29
+ function isWireEvent(value) {
30
+ if (value === null || typeof value !== "object") return false;
31
+ const v = value;
32
+ return typeof v["id"] === "string" && typeof v["type"] === "string" && typeof v["index"] === "number" && typeof v["sessionId"] === "string" && typeof v["createdAt"] === "string";
33
+ }
34
+ const ERROR_STATUS = {
35
+ INVALID_PAYLOAD: 400,
36
+ UNKNOWN_EVENT_TYPE: 400,
37
+ PARTIAL_DUPLICATE_BATCH: 400,
38
+ LOG_UNAVAILABLE: 503,
39
+ LOG_NOT_CONFIGURED: 500
40
+ };
41
+ function errorStatus(code) {
42
+ return ERROR_STATUS[code];
43
+ }
44
+ function errorToWire(error) {
45
+ const body = { error: {
46
+ code: error.code,
47
+ message: error.message
48
+ } };
49
+ if (error.details !== void 0) body.error.details = error.details;
50
+ return body;
51
+ }
52
+ /** Rebuild an A2Error from a wire body; null if the body isn't one. */
53
+ function errorFromWire(body) {
54
+ if (body === null || typeof body !== "object") return null;
55
+ const err = body.error;
56
+ if (err === null || typeof err !== "object") return null;
57
+ const { code, message, details } = err;
58
+ if (typeof code !== "string" || !(code in ERROR_STATUS)) return null;
59
+ return new A2Error(code, String(message ?? code), { details });
60
+ }
61
+ //#endregion
62
+ export { eventToWire as a, eventFromWire as i, errorStatus as n, isWireEvent as o, errorToWire as r, errorFromWire as t };
@@ -0,0 +1,214 @@
1
+ ---
2
+ title: Quickstart
3
+ description: Define one lifecycle, implement its reactions, and make your first durable append in about five minutes.
4
+ ---
5
+
6
+ ## What you'll build
7
+
8
+ The smallest useful lifecycle: an order that notifies a shop when it arrives.
9
+ One contract, one session, one handler, two routes, and at the end a log you
10
+ can read back.
11
+
12
+ ## 1. Install
13
+
14
+ ```package-install
15
+ npm i experimental-a2 zod
16
+ ```
17
+
18
+ ## 2. Define a contract
19
+
20
+ A contract is a name plus the events it understands: each key an event
21
+ name, each value a schema. Zod here, though any
22
+ [Standard Schema](https://standardschema.dev) validator works, and
23
+ that's the entire upfront declaration. No state list, no transition
24
+ table.
25
+
26
+ ```ts server/orders.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({ shopId: z.string(), items: z.array(z.string()) }),
34
+ 'shop.notified': z.object({}),
35
+ },
36
+ })
37
+ ```
38
+
39
+ The contract is a plain, importable, isomorphic value. The server
40
+ implements it below, the reducer derives from it in step 6, and when a
41
+ browser enters the picture it types the client too.
42
+
43
+ There's no database setup in development: A2 stores events in SQLite at
44
+ `.a2/dev.db`, so state survives restarts. Tests get an in-memory log.
45
+ Production wants an explicit backend; see
46
+ [Going to production](/guides/production).
47
+
48
+ ## 3. Implement it
49
+
50
+ Handlers run where the contract is served. `createServer` binds the
51
+ vocabulary to storage and reactions; each handler gets the triggering
52
+ event and can append what happens next. Same file, below the contract
53
+ (they can split later, when a browser needs the contract alone):
54
+
55
+ ```ts server/orders.ts
56
+ import { createServer } from 'experimental-a2/server'
57
+
58
+ export const ordersServer = createServer({
59
+ contract: orders,
60
+ handlers: {
61
+ created: async ({ event, append }) => {
62
+ // your side effect, e.g. email the shop, idempotent via event.id:
63
+ // await sendEmailToShop(event.payload, { idempotencyKey: event.id })
64
+ await append({ type: 'shop.notified', payload: {} })
65
+ },
66
+ },
67
+ })
68
+ ```
69
+
70
+ All handlers are declared at construction: one table of reactions, so a
71
+ handler can never be silently missing because the module that registered
72
+ it wasn't imported.
73
+
74
+ Two things worth noticing:
75
+
76
+ - The side effect takes an idempotency key. Handlers can run more than
77
+ once (that's what at-least-once delivery means), so external calls
78
+ should be safe to repeat. `event.id` is stable across re-runs. Use it.
79
+ - The `append` inside a handler is already deduplicated. If the handler
80
+ crashes after appending and runs again, the second append is a no-op,
81
+ [here's how](/concepts/handlers#handlers-can-run-twice).
82
+
83
+ ## 4. Append the first event
84
+
85
+ `append` is the entire public surface for moving a session forward. Call it
86
+ from a route:
87
+
88
+ ```ts app/api/orders/route.ts
89
+ import { ordersServer } from '@/server/orders'
90
+
91
+ export async function POST(req: Request) {
92
+ const { orderId, shopId, items } = await req.json()
93
+
94
+ await ordersServer.session(orderId).append({
95
+ type: 'created',
96
+ payload: { shopId, items },
97
+ })
98
+
99
+ return Response.json({ ok: true })
100
+ }
101
+ ```
102
+
103
+ A session is one instance of the contract. `ordersServer.session('order-42')`
104
+ is order 42's lifecycle: the unit A2 orders, recovers, folds, and streams.
105
+ Creating the handle does no I/O, and the id is yours to choose.
106
+
107
+ Try it:
108
+
109
+ ```bash
110
+ curl -X POST localhost:3000/api/orders \
111
+ -H 'content-type: application/json' \
112
+ -d '{ "orderId": "order-42", "shopId": "shop-7", "items": ["flat white"] }'
113
+ ```
114
+
115
+ The payload is validated against the schema before anything is written;
116
+ send garbage and you get `INVALID_PAYLOAD` back, with the issues.
117
+
118
+ When the append returns, the event is durably in the log and A2 has started the
119
+ `created` handler inline. In production, queue-backed recovery wakes pending
120
+ work after a failure or process death. See [Durability](/concepts/durability)
121
+ for the exact contract.
122
+
123
+ ## 5. Read the log back
124
+
125
+ Add a route that returns the session's history:
126
+
127
+ ```ts app/api/orders/[orderId]/route.ts
128
+ import { ordersServer } from '@/server/orders'
129
+
130
+ export async function GET(
131
+ _req: Request,
132
+ { params }: { params: Promise<{ orderId: string }> },
133
+ ) {
134
+ const { orderId } = await params
135
+ const events = await ordersServer.session(orderId).history()
136
+ return Response.json(events)
137
+ }
138
+ ```
139
+
140
+ ```bash
141
+ curl localhost:3000/api/orders/order-42
142
+ ```
143
+
144
+ ```json
145
+ [
146
+ { "index": 1, "type": "created", "payload": { "shopId": "shop-7", "items": ["flat white"] } },
147
+ { "index": 2, "type": "shop.notified", "payload": {} }
148
+ ]
149
+ ```
150
+
151
+ (Trimmed for brevity; each event also carries `id`, `sessionId`, and
152
+ `createdAt`.)
153
+
154
+ That second event is the payoff. You appended one event; the handler
155
+ appended the next; the log shows the whole story, in order. This is what
156
+ debugging looks like in A2: you read.
157
+
158
+ ## 6. Fold state out of the log
159
+
160
+ Raw session history is truth, but UIs usually want a view. Define a reducer
161
+ once:
162
+
163
+ ```ts reducer.ts
164
+ import { orders } from '@/server/orders'
165
+
166
+ export const ordersReducer = orders
167
+ .reducer({ name: 'order-status', initialState: { status: 'new' } })
168
+ .fold((state, event) => {
169
+ switch (event.type) {
170
+ case 'created':
171
+ return { ...state, status: 'created' }
172
+ case 'shop.notified':
173
+ return { ...state, status: 'notified' }
174
+ default:
175
+ return state
176
+ }
177
+ })
178
+ ```
179
+
180
+ A reducer is *derived from* the contract, in two steps by design: the
181
+ first call fixes the state and event types, so `.fold()` receives fully
182
+ concrete ones. No type arguments, no annotations, and literal unions in
183
+ your state survive the fold.
184
+
185
+ And fold anywhere you need the current picture:
186
+
187
+ ```ts
188
+ // anywhere on the server:
189
+ import { ordersServer } from '@/server/orders'
190
+ import { ordersReducer } from '@/reducer'
191
+
192
+ const { state } = await ordersServer.session('order-42').state(ordersReducer)
193
+ // state → { status: 'notified' }
194
+ ```
195
+
196
+ The session's state is not a second record to update. It is a fold over the
197
+ log. More in [Reading state](/concepts/state).
198
+
199
+ ## That's it
200
+
201
+ You have a durable lifecycle, implemented: a validated write path, a handler,
202
+ a log you can read, and a view folded from it.
203
+ Everything else in these docs is the same two moves, append an event and
204
+ react to it, applied to timers, cancellation, live UIs, and production
205
+ infrastructure.
206
+
207
+ <CardGroup cols={2}>
208
+ <Card title="Handlers" href="/concepts/handlers" icon="file">
209
+ The context object, chaining, ordering, and re-runs.
210
+ </Card>
211
+ <Card title="Going to production" href="/guides/production" icon="rocket">
212
+ Postgres, queue-backed recovery, and dead-letter resolution.
213
+ </Card>
214
+ </CardGroup>
@@ -0,0 +1,138 @@
1
+ ---
2
+ title: Contracts and sessions
3
+ description: A contract names one kind of lifecycle. A session is one instance, with its own history, ordering, state, and live stream.
4
+ ---
5
+
6
+ ## Contracts
7
+
8
+ A2 has two nouns. A **contract** defines what can happen in one kind of
9
+ lifecycle. A **session** is one instance of it.
10
+
11
+ ```ts contracts.ts
12
+ import { z } from 'zod'
13
+ import * as a2 from 'experimental-a2'
14
+
15
+ export const orders = a2.contract({
16
+ name: 'orders',
17
+ events: {
18
+ created: z.object({ shopId: z.string(), items: z.array(z.string()) }),
19
+ 'shop.notified': z.object({}),
20
+ 'shop.started': z.object({}),
21
+ 'driver.notified': z.object({ driverId: z.string() }),
22
+ expired: z.object({}),
23
+ },
24
+ })
25
+ ```
26
+
27
+ The name identifies the contract everywhere: in storage, in queue
28
+ messages, in logs. It's what keeps an app with several contracts
29
+ unambiguous.
30
+
31
+ `events` is the contract's full vocabulary. Each key is an event name;
32
+ each value is a schema for its payload. Zod in these docs, but any
33
+ [Standard Schema](https://standardschema.dev) library works: Valibot,
34
+ ArkType, your own. Every append is validated against it: a bad payload
35
+ throws `INVALID_PAYLOAD` before anything is written, an unknown type
36
+ throws `UNKNOWN_EVENT_TYPE`. What lands in the log is the validated
37
+ output, with defaults applied and transforms run.
38
+
39
+ The contract is a plain, importable, **isomorphic** value. It carries
40
+ nothing but schemas. The server implements it, [reducers](/concepts/state)
41
+ derive from it, and the browser types its pushes off it. One artifact,
42
+ shared by every side of the wire.
43
+
44
+ And that's the entire declaration. No states, no transition table. What
45
+ happens on each event is defined where the contract is served; what
46
+ things look like right now is defined by [reducers](/concepts/state). The
47
+ contract just names the vocabulary.
48
+
49
+ :::tip
50
+ Events are facts, so name them in past tense: `created`, `expired`,
51
+ `shop.notified`. If a name reads like a command (`notifyShop`), it's
52
+ describing what a handler should do, not what happened. Dots are a naming
53
+ convention, not a namespace.
54
+ :::
55
+
56
+ ## Serving a contract
57
+
58
+ Handlers and storage bind to the contract with `createServer` from
59
+ `experimental-a2/server`, the entry point that is allowed to touch a log backend
60
+ (and the one that fails loudly if a browser bundle ever imports it):
61
+
62
+ ```ts server/orders.ts
63
+ import { createServer } from 'experimental-a2/server'
64
+ import { postgres } from 'experimental-a2/log-postgres'
65
+ import { orders } from '@/contracts'
66
+
67
+ export const ordersServer = createServer({
68
+ contract: orders,
69
+ log: postgres({ connectionString: process.env.DATABASE_URL }),
70
+ handlers: {
71
+ created: async ({ event, append }) => {
72
+ // your side effect, then:
73
+ await append({ type: 'shop.notified', payload: {} })
74
+ },
75
+ },
76
+ })
77
+ ```
78
+
79
+ Handlers are one table of reactions, declared at construction, so a
80
+ handler can never be silently missing because the module that would
81
+ have registered it wasn't imported. Compose across files by spreading
82
+ objects into `handlers`.
83
+
84
+ | Entry point | Backend | Use it for |
85
+ | ----------------- | --------- | ----------------------------------------- |
86
+ | `experimental-a2/log-postgres` | Postgres | Production |
87
+ | `experimental-a2/log-sqlite` | SQLite | Development: the default, at `.a2/dev.db` |
88
+ | `experimental-a2/log-memory` | In-memory | Tests: the default when `NODE_ENV=test` |
89
+
90
+ When you omit `log`, development gets SQLite (state survives `next dev`
91
+ restarts) and tests get memory. Production gets an error: the server
92
+ throws `LOG_NOT_CONFIGURED` at startup rather than guess where your
93
+ events should live. A failed boot beats events written to a filesystem
94
+ that evaporates.
95
+
96
+ ## Sessions
97
+
98
+ ```ts
99
+ // anywhere on the server: a route, a handler, a script
100
+ const order = ordersServer.session('order-42')
101
+ ```
102
+
103
+ A session is one instance of the contract: one order, one signup, one
104
+ chat, one approval, one import. It has its own log, its own ordering, its own
105
+ state, and its own live stream. The id is yours: use whatever identifies the
106
+ subject in your system.
107
+
108
+ Creating a session does no I/O. It's a handle; nothing loads until you
109
+ append, read, or stream.
110
+
111
+ Sessions are also the unit of ordering: events within one session are
112
+ processed serially, in log order. Different sessions never wait for each
113
+ other. More in [Handlers](/concepts/handlers#ordering).
114
+
115
+ ## Pick the boundary
116
+
117
+ The session is the boundary A2 keeps durable and synchronized. Put events in
118
+ the same session when they must be ordered, folded, recovered, or viewed
119
+ together. Split things that should progress independently.
120
+
121
+ A contract is not a database table, and a session is not a table or a general
122
+ query boundary. One contract serves every instance of that lifecycle. An
123
+ `orders` contract may serve millions of order sessions through the same server
124
+ and handler table.
125
+
126
+ This boundary works well when someone opens a thing by id and watches it
127
+ progress. Collections, joins, search, and constraints across many records
128
+ usually belong in an application database. See
129
+ [A2 and your database](/guides/application-data).
130
+
131
+ ## One more option: recovery
132
+
133
+ Servers accept a `recovery` option, which arms a durable, delayed drain
134
+ for every top-level append, giving crash recovery and handler retries in one
135
+ mechanism. The handler starts without waiting for recovery, and a successful
136
+ initial arm is durable before `append` returns. It changes how fast A2 heals,
137
+ not how it works. Everything in these concept pages holds with or without it.
138
+ Covered in [Going to production](/guides/production).