experimental-a2 0.3.0 → 0.4.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 (91) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/ai-B4YhEnfw.d.ts +333 -0
  3. package/dist/ai-server.d.ts +48 -9
  4. package/dist/ai-server.js +121 -49
  5. package/dist/ai.d.ts +2 -303
  6. package/dist/ai.js +231 -86
  7. package/dist/cli-B3VuxoDe.js +597 -0
  8. package/dist/cli-bin.d.ts +1 -0
  9. package/dist/cli-bin.js +5 -0
  10. package/dist/cli.d.ts +19 -0
  11. package/dist/cli.js +2 -0
  12. package/dist/client-BrfDXQ8A.d.ts +155 -0
  13. package/dist/client-Bt4tAKi9.js +798 -0
  14. package/dist/client.d.ts +2 -90
  15. package/dist/client.js +1 -409
  16. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
  17. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
  18. package/dist/devtools-J_jZ2vQf.d.ts +151 -0
  19. package/dist/devtools-kJJaORn-.js +338 -0
  20. package/dist/devtools-server.browser.js +1 -1
  21. package/dist/devtools-server.d.ts +2 -2
  22. package/dist/devtools-server.js +224 -43
  23. package/dist/devtools.d.ts +2 -0
  24. package/dist/devtools.js +2 -0
  25. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
  26. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
  27. package/dist/http.d.ts +71 -13
  28. package/dist/http.js +302 -41
  29. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +2 -2
  32. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
  33. package/dist/internal-aEotMzu_.js +209 -0
  34. package/dist/otel.d.ts +1 -1
  35. package/dist/platform-B4TnJtWu.js +32 -0
  36. package/dist/react.d.ts +37 -14
  37. package/dist/react.js +26 -15
  38. package/dist/scheduler-qstash.d.ts +78 -0
  39. package/dist/scheduler-qstash.js +499 -0
  40. package/dist/scheduler-task-BpzhPnRS.js +54 -0
  41. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
  42. package/dist/scheduler-vercel.js +226 -0
  43. package/dist/server-CcNnFnoW.js +1405 -0
  44. package/dist/server-YtPq7hjw.d.ts +260 -0
  45. package/dist/server.d.ts +4 -155
  46. package/dist/server.js +2 -2
  47. package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
  48. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
  49. package/dist/store-codec-DTG0Ftek.js +8 -0
  50. package/dist/store-memory.d.ts +11 -0
  51. package/dist/{log-memory.js → store-memory.js} +127 -24
  52. package/dist/{log-polling-DZ1MiKLg.js → store-polling-DgrrAE3d.js} +5 -5
  53. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
  54. package/dist/{log-postgres.js → store-postgres.js} +158 -24
  55. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-DWqx3F47.js} +269 -66
  56. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +7 -7
  57. package/dist/{log-redis-http.js → store-redis-http.js} +20 -12
  58. package/dist/{log-redis.d.ts → store-redis.d.ts} +6 -6
  59. package/dist/{log-redis.js → store-redis.js} +99 -39
  60. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
  61. package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
  62. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
  63. package/dist/testing.browser.d.ts +1 -0
  64. package/dist/testing.browser.js +4 -0
  65. package/dist/testing.d.ts +31 -0
  66. package/dist/testing.js +101 -0
  67. package/dist/wire-DCUZBUlT.js +222 -0
  68. package/docs/01-quickstart.mdx +3 -3
  69. package/docs/concepts/01-contracts.mdx +18 -13
  70. package/docs/concepts/02-handlers.mdx +7 -7
  71. package/docs/concepts/03-durability.mdx +21 -21
  72. package/docs/concepts/04-state.mdx +18 -21
  73. package/docs/guides/01-timers.mdx +154 -49
  74. package/docs/guides/02-cancellation.mdx +30 -4
  75. package/docs/guides/03-react.mdx +20 -21
  76. package/docs/guides/04-local-first.mdx +1 -1
  77. package/docs/guides/05-production.mdx +310 -61
  78. package/docs/guides/06-ai-agents.mdx +249 -49
  79. package/docs/guides/07-devtools.mdx +137 -12
  80. package/docs/guides/08-application-data.mdx +1 -1
  81. package/docs/guides/09-presence.mdx +284 -0
  82. package/docs/guides/10-transports.mdx +131 -0
  83. package/docs/index.mdx +18 -14
  84. package/docs/reference/01-api.mdx +751 -103
  85. package/docs/reference/02-errors.mdx +10 -5
  86. package/package.json +23 -7
  87. package/dist/internal-gCd5qMry.js +0 -44
  88. package/dist/log-memory.d.ts +0 -11
  89. package/dist/recovery-vercel.js +0 -119
  90. package/dist/server-BcLa4RFL.js +0 -877
  91. package/dist/wire-BVsgR8o9.js +0 -62
@@ -0,0 +1,222 @@
1
+ import { t as A2Error } from "./errors-BQuJpe82.js";
2
+ import { g as nullProtoRecord } from "./internal-aEotMzu_.js";
3
+ //#region src/wire.ts
4
+ /**
5
+ * The wire format shared by experimental-a2/http (server side) and experimental-a2/client
6
+ * (browser side): events as JSON with ISO timestamps, and the A2Error
7
+ * envelope. Internal module — both entry points re-export what's
8
+ * public.
9
+ */
10
+ function eventToWire(event) {
11
+ return {
12
+ id: event.id,
13
+ type: event.type,
14
+ payload: event.payload,
15
+ index: event.index,
16
+ sessionId: event.sessionId,
17
+ createdAt: event.createdAt.toISOString()
18
+ };
19
+ }
20
+ function eventFromWire(wire) {
21
+ return {
22
+ id: wire.id,
23
+ type: wire.type,
24
+ payload: wire.payload,
25
+ index: wire.index,
26
+ sessionId: wire.sessionId,
27
+ createdAt: new Date(wire.createdAt)
28
+ };
29
+ }
30
+ function isWireEvent(value) {
31
+ if (value === null || typeof value !== "object") return false;
32
+ const v = value;
33
+ return typeof v["id"] === "string" && typeof v["type"] === "string" && typeof v["index"] === "number" && typeof v["sessionId"] === "string" && typeof v["createdAt"] === "string";
34
+ }
35
+ function presencePatchToWire(patch) {
36
+ return {
37
+ participant: patch.participant,
38
+ values: patch.values,
39
+ seen: patch.seen,
40
+ at: patch.at.toISOString()
41
+ };
42
+ }
43
+ function presencePatchFromWire(wire) {
44
+ return {
45
+ participant: wire.participant,
46
+ values: wire.values,
47
+ seen: wire.seen,
48
+ at: new Date(wire.at)
49
+ };
50
+ }
51
+ function presenceSnapshotToWire(snapshot) {
52
+ const wire = nullProtoRecord();
53
+ for (const [participant, fields] of Object.entries(snapshot.snapshot)) {
54
+ const wireFields = nullProtoRecord();
55
+ for (const [field, entry] of Object.entries(fields)) {
56
+ if (entry === void 0) continue;
57
+ wireFields[field] = {
58
+ value: entry.value,
59
+ seen: entry.seen,
60
+ at: entry.at.toISOString()
61
+ };
62
+ }
63
+ wire[participant] = wireFields;
64
+ }
65
+ return { snapshot: wire };
66
+ }
67
+ function presenceSnapshotFromWire(wire) {
68
+ const map = nullProtoRecord();
69
+ for (const [participant, fields] of Object.entries(wire.snapshot)) {
70
+ const revived = nullProtoRecord();
71
+ for (const [field, entry] of Object.entries(fields)) revived[field] = {
72
+ value: entry.value,
73
+ seen: entry.seen,
74
+ at: new Date(entry.at)
75
+ };
76
+ map[participant] = revived;
77
+ }
78
+ return { snapshot: map };
79
+ }
80
+ function isWirePresencePatch(value) {
81
+ if (value === null || typeof value !== "object") return false;
82
+ const v = value;
83
+ return typeof v["participant"] === "string" && v["values"] !== null && typeof v["values"] === "object" && !Array.isArray(v["values"]) && typeof v["seen"] === "number" && typeof v["at"] === "string";
84
+ }
85
+ function isWirePresenceSnapshot(value) {
86
+ if (value === null || typeof value !== "object") return false;
87
+ const v = value;
88
+ return v["snapshot"] !== null && typeof v["snapshot"] === "object" && !Array.isArray(v["snapshot"]);
89
+ }
90
+ const ERROR_STATUS = {
91
+ INVALID_PAYLOAD: 400,
92
+ UNKNOWN_EVENT_TYPE: 400,
93
+ PARTIAL_DUPLICATE_BATCH: 400,
94
+ SUPERSEDED_ATTEMPT: 409,
95
+ CLAIM_EXPIRED: 409,
96
+ STORE_UNAVAILABLE: 503,
97
+ STORE_NOT_CONFIGURED: 500,
98
+ UNKNOWN_PRESENCE_FIELD: 400,
99
+ PRESENCE_NOT_SUPPORTED: 500
100
+ };
101
+ function errorStatus(code) {
102
+ return ERROR_STATUS[code];
103
+ }
104
+ function errorToWire(error) {
105
+ const body = { error: {
106
+ code: error.code,
107
+ message: error.message
108
+ } };
109
+ if (error.details !== void 0) body.error.details = error.details;
110
+ return body;
111
+ }
112
+ /** Rebuild an A2Error from a wire body; null if the body isn't one. */
113
+ function errorFromWire(body) {
114
+ if (body === null || typeof body !== "object") return null;
115
+ const err = body.error;
116
+ if (err === null || typeof err !== "object") return null;
117
+ const { code, message, details } = err;
118
+ if (typeof code !== "string" || !Object.hasOwn(ERROR_STATUS, code)) return null;
119
+ return new A2Error(code, String(message ?? code), { details });
120
+ }
121
+ const SOCKET_PING_FRAME = JSON.stringify({ kind: "ping" });
122
+ /** One stream item as a socket frame — `sseResponse`'s framing over
123
+ * the same codecs, with `kind` instead of an SSE event name. */
124
+ function socketFrameFor(item) {
125
+ if ("snapshot" in item) return JSON.stringify({
126
+ kind: "presence-snapshot",
127
+ ...presenceSnapshotToWire(item)
128
+ });
129
+ if ("participant" in item) return JSON.stringify({
130
+ kind: "presence",
131
+ ...presencePatchToWire(item)
132
+ });
133
+ return JSON.stringify({
134
+ kind: "event",
135
+ ...eventToWire(item)
136
+ });
137
+ }
138
+ function socketAckFor(req, events) {
139
+ return JSON.stringify({
140
+ kind: "ack",
141
+ req,
142
+ events: events.map(eventToWire)
143
+ });
144
+ }
145
+ function socketErrorAckFor(req, error) {
146
+ return JSON.stringify({
147
+ kind: "ack",
148
+ req,
149
+ ...errorToWire(error)
150
+ });
151
+ }
152
+ function parseSocketFrame(data) {
153
+ let parsed;
154
+ try {
155
+ parsed = JSON.parse(data);
156
+ } catch {
157
+ return null;
158
+ }
159
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
160
+ const frame = parsed;
161
+ switch (frame["kind"]) {
162
+ case "ping": return { kind: "ping" };
163
+ case "event": {
164
+ if (!isWireEvent(parsed)) return null;
165
+ const { id, type, payload, index, sessionId, createdAt } = parsed;
166
+ return {
167
+ kind: "event",
168
+ event: {
169
+ id,
170
+ type,
171
+ payload,
172
+ index,
173
+ sessionId,
174
+ createdAt
175
+ }
176
+ };
177
+ }
178
+ case "presence": {
179
+ if (!isWirePresencePatch(parsed)) return null;
180
+ const { participant, values, seen, at } = parsed;
181
+ return {
182
+ kind: "presence",
183
+ patch: {
184
+ participant,
185
+ values,
186
+ seen,
187
+ at
188
+ }
189
+ };
190
+ }
191
+ case "presence-snapshot":
192
+ if (!isWirePresenceSnapshot(parsed)) return null;
193
+ return {
194
+ kind: "presence-snapshot",
195
+ snapshot: { snapshot: parsed.snapshot }
196
+ };
197
+ case "ack": {
198
+ const req = frame["req"];
199
+ if (typeof req !== "number") return null;
200
+ const events = frame["events"];
201
+ if (Array.isArray(events) && events.every(isWireEvent)) return {
202
+ kind: "ack",
203
+ req,
204
+ events
205
+ };
206
+ const error = errorFromWire(parsed);
207
+ if (error) return {
208
+ kind: "ack",
209
+ req,
210
+ error
211
+ };
212
+ return {
213
+ kind: "ack",
214
+ req,
215
+ error: new A2Error("STORE_UNAVAILABLE", "unintelligible ack")
216
+ };
217
+ }
218
+ default: return null;
219
+ }
220
+ }
221
+ //#endregion
222
+ export { socketFrameFor as _, eventFromWire as a, isWirePresencePatch as c, presencePatchFromWire as d, presencePatchToWire as f, socketErrorAckFor as g, socketAckFor as h, errorToWire as i, isWirePresenceSnapshot as l, presenceSnapshotToWire as m, errorFromWire as n, eventToWire as o, presenceSnapshotFromWire as p, errorStatus as r, isWireEvent as s, SOCKET_PING_FRAME as t, parseSocketFrame as u };
@@ -40,7 +40,7 @@ implements it below, the reducer derives from it in step 6, and when a
40
40
  browser enters the picture it types the client too.
41
41
 
42
42
  There's no database setup in development: A2 stores events in SQLite at
43
- `.a2/dev.db`, so state survives restarts. Tests get an in-memory log.
43
+ `.a2/dev.db`, so state survives restarts. Tests get an in-memory store.
44
44
  Production wants an explicit backend; see
45
45
  [Going to production](/guides/production).
46
46
 
@@ -115,7 +115,7 @@ The payload is validated against the schema before anything is written;
115
115
  send garbage and you get `INVALID_PAYLOAD` back, with the issues.
116
116
 
117
117
  When the append returns, the event is durably in the log and A2 has started the
118
- `created` handler inline. In production, queue-backed recovery wakes pending
118
+ `created` handler inline. In production, a queue-backed scheduler wakes pending
119
119
  work after a failure or process death. See [Durability](/concepts/durability)
120
120
  for the exact contract.
121
121
 
@@ -208,6 +208,6 @@ infrastructure.
208
208
  The context object, chaining, ordering, and re-runs.
209
209
  </Card>
210
210
  <Card title="Going to production" href="/guides/production" icon="rocket">
211
- Postgres, queue-backed recovery, and dead-letter resolution.
211
+ Postgres, a queue-backed scheduler, and dead-letter resolution.
212
212
  </Card>
213
213
  </CardGroup>
@@ -45,6 +45,11 @@ The contract names the complete vocabulary. What happens on each event is
45
45
  defined where the contract is served. [Reducers](/concepts/state) define what
46
46
  things look like right now.
47
47
 
48
+ A contract can also declare `presence`, a second vocabulary for ephemeral
49
+ participant state (cursors, typing, live progress) that is replicated but
50
+ never stored in the log. Events are facts; presence is the moment. See
51
+ [Presence](/guides/presence).
52
+
48
53
  :::tip
49
54
  Events are facts, so name them in past tense: `created`, `expired`,
50
55
  `shop.notified`. If a name reads like a command (`notifyShop`), it's
@@ -55,17 +60,17 @@ convention, not a namespace.
55
60
  ## Serving a contract
56
61
 
57
62
  Storage and optional handlers bind to the contract with `createServer` from
58
- `experimental-a2/server`, the entry point that is allowed to touch a log backend
63
+ `experimental-a2/server`, the entry point that is allowed to touch a store backend
59
64
  (and the one that fails loudly if a browser bundle ever imports it):
60
65
 
61
66
  ```ts server/orders.ts
62
67
  import { createServer } from 'experimental-a2/server'
63
- import { postgres } from 'experimental-a2/log-postgres'
68
+ import { postgres } from 'experimental-a2/store-postgres'
64
69
  import { orders } from '@/contracts'
65
70
 
66
71
  export const ordersServer = createServer({
67
72
  contract: orders,
68
- log: postgres({ connectionString: process.env.DATABASE_URL }),
73
+ store: postgres({ connectionString: process.env.DATABASE_URL }),
69
74
  handlers: {
70
75
  created: async ({ event }) => {
71
76
  // your side effect, then:
@@ -82,13 +87,13 @@ objects into `handlers`.
82
87
 
83
88
  | Entry point | Backend | Use it for |
84
89
  | ----------------- | --------- | ----------------------------------------- |
85
- | `experimental-a2/log-postgres` | Postgres | Production |
86
- | `experimental-a2/log-sqlite` | SQLite | Development: the default, at `.a2/dev.db` |
87
- | `experimental-a2/log-memory` | In-memory | Tests: the default when `NODE_ENV=test` |
90
+ | `experimental-a2/store-postgres` | Postgres | Production |
91
+ | `experimental-a2/store-sqlite` | SQLite | Development: the default, at `.a2/dev.db` |
92
+ | `experimental-a2/store-memory` | In-memory | Tests: the default when `NODE_ENV=test` |
88
93
 
89
- When you omit `log`, development gets SQLite (state survives `next dev`
94
+ When you omit `store`, development gets SQLite (state survives `next dev`
90
95
  restarts) and tests get memory. Production gets an error: the server
91
- throws `LOG_NOT_CONFIGURED` at startup rather than guess where your
96
+ throws `STORE_NOT_CONFIGURED` at startup rather than guess where your
92
97
  events should live. A failed boot beats events written to a filesystem
93
98
  that evaporates.
94
99
 
@@ -127,11 +132,11 @@ progress. Collections, joins, search, and constraints across many records
127
132
  usually belong in an application database. See
128
133
  [A2 and your database](/guides/application-data).
129
134
 
130
- ## One more option: recovery
135
+ ## One more option: a scheduler
131
136
 
132
- Servers accept a `recovery` option. When a top-level append leaves or finds
133
- pending handler work, recovery arms a durable, delayed drain. The handler
134
- starts without waiting for recovery, and a successful initial arm is durable
137
+ Servers accept a `scheduler` option. When a top-level append leaves or finds
138
+ pending handler work, the scheduler arms a durable, delayed drain. The handler
139
+ starts without waiting for the scheduler, and a successful initial arm is durable
135
140
  before `append` returns. Event types without handlers settle during append and
136
- create no recovery work when the session has no older backlog. Covered in
141
+ create no scheduler work when the session has no older backlog. Covered in
137
142
  [Going to production](/guides/production).
@@ -32,16 +32,16 @@ module that registered it was never imported.
32
32
  Handlers are optional per event type. An event without one still gets its full
33
33
  place in the log. You can read it with `history()`, fold it into state, and
34
34
  stream it to a live client. A2 settles it in the append transaction, with no
35
- dispatch claim or recovery message.
35
+ dispatch claim or scheduler message.
36
36
 
37
- ```ts server/order-log.ts
37
+ ```ts server/order-history.ts
38
38
  import { createServer } from 'experimental-a2/server'
39
- import { postgres } from 'experimental-a2/log-postgres'
39
+ import { postgres } from 'experimental-a2/store-postgres'
40
40
  import { orders } from '@/contracts'
41
41
 
42
- export const orderLog = createServer({
42
+ export const orderHistory = createServer({
43
43
  contract: orders,
44
- log: postgres({ connectionString: process.env.DATABASE_URL }),
44
+ store: postgres({ connectionString: process.env.DATABASE_URL }),
45
45
  })
46
46
  ```
47
47
 
@@ -58,7 +58,7 @@ Every handler receives one argument:
58
58
  | `ctx.event` | The triggering event: `{ id, type, payload, index, sessionId, createdAt }`. |
59
59
  | `ctx.attempt` | The durable 1-based dispatch claim for this event. |
60
60
  | `ctx.session` | This session's handle: `id`, `append`, `history`, `state`, and `stream`. |
61
- | `ctx.signal` | An `AbortSignal`, active only with `abortOn`. See [Cancellation](/guides/cancellation). |
61
+ | `ctx.signal` | An `AbortSignal`. Fires when the attempt's lease lapses (`CLAIM_EXPIRED`), when a recovered run provably supersedes it (`SUPERSEDED_ATTEMPT`), and on matching `abortOn` events. See [Cancellation](/guides/cancellation). |
62
62
 
63
63
  `ctx.attempt` starts at `1` and increments on every durable claim. It may skip
64
64
  when a process dies between the claim and handler entry.
@@ -239,7 +239,7 @@ export const drivers = createServer({
239
239
  })
240
240
  ```
241
241
 
242
- The resolved lane is stored with the event when it is appended. Recovery uses
242
+ The resolved lane is stored with the event when it is appended. The scheduler uses
243
243
  that stored value, so a deployment cannot reinterpret pending work. Lane keys
244
244
  are scoped to one session. They are not distributed locks across sessions.
245
245
 
@@ -5,13 +5,13 @@ description: Appends are effectively-once, handlers are at-least-once, and retri
5
5
 
6
6
  ## The contract
7
7
 
8
- - **Appends are effectively-once.** Event ids deduplicate writes. When
9
- `append` returns, the event is in the log.
8
+ - **Appends are effectively-once.** Event ids deduplicate writes. A batch commits
9
+ before its handlers start; they may start before `append` resolves.
10
10
  - **Handlers are at-least-once.** They may run again after a crash, so
11
11
  [side effects use idempotency keys](/concepts/handlers#handlers-can-run-twice).
12
- - **The log stays ordered.** Every session has one immutable event order.
13
- - **Handlers are concurrent by default.** Independent events can run at the
14
- same time. Events in the same lane run one at a time in log order.
12
+ - **The log stays ordered.** Every session has one immutable append order.
13
+ - **Handlers are concurrent by default.** Log order does not serialize them.
14
+ Only events in the same lane run one at a time in log order.
15
15
 
16
16
  ## The correctness model
17
17
 
@@ -44,7 +44,7 @@ processed marker says what finished. The watchdog says when to look again.
44
44
  ```
45
45
 
46
46
  A queue message identifies a session. Every wakeup runs the same drain, and
47
- the log decides what remains. Recovery and explicit `server.drain()` inspect
47
+ the log decides what remains. The scheduler and explicit `server.drain()` inspect
48
48
  the full session. Reads never dispatch handlers.
49
49
 
50
50
  :::note[Recovery is not an event]
@@ -160,25 +160,25 @@ limit or priority queue. Use lanes for ordered mutual exclusion. Put capacity
160
160
  limits near the constrained resource, such as an API client, connection pool,
161
161
  or execution runtime.
162
162
 
163
- ## Append and recovery
163
+ ## Append and scheduling
164
164
 
165
- A top-level append commits first. When pending handler work remains, it starts
166
- the inline drain and optional watchdog arm in parallel. The drain never waits
167
- for the queue. Append joins the arm for at most two seconds; on failure, its
168
- telemetry span records `a2.append.armed = false`, but committed work continues.
169
- `ctx.session.append` rides the active session drain and adds no arm. Returned
170
- events enter the log as part of completion, then become eligible immediately.
165
+ A top-level append commits first. Only then does A2 start the inline drain and
166
+ optional watchdog arm in parallel. A handler may start before `append` returns;
167
+ the call joins the arm for at most two seconds, not the handler. The drain never
168
+ waits for the queue. Arm failure records `a2.append.armed = false`, while
169
+ `ctx.session.append` rides the active drain and adds no arm. Returned events
170
+ enter the log with completion, then become eligible immediately.
171
171
 
172
172
  `session.append.dispatch(...events)` chooses the other execution path. It
173
173
  commits first, skips the current invocation's inline drain, and awaits an
174
- immediate recovery send. It requires configured recovery. Queue delivery then
174
+ immediate scheduler send. It requires a configured scheduler. Queue delivery then
175
175
  claims the same durable events through the normal drain path. If the send
176
176
  fails, retry with the same explicit event IDs; the append itself may already
177
- have committed. The recovery message is a wakeup, not worker affinity. An
177
+ have committed. The scheduler message is a wakeup, not worker affinity. An
178
178
  already-active drain may claim the new work first.
179
179
 
180
180
  An event type without a handler settles in the append transaction with no
181
- dispatch attempt. It starts no drain or recovery arm when the session has no
181
+ dispatch attempt. It starts no drain or scheduler arm when the session has no
182
182
  older pending work. If older handled work is pending, the append still wakes
183
183
  the session-wide healing path.
184
184
 
@@ -194,7 +194,7 @@ The edge is part of the existing append batch and survives process restarts.
194
194
  The stored event also carries `lane`, `firstClaimedAt`, `lastClaimedAt`,
195
195
  `claimHolder`, `claimExpiresAt`, `lastFailedAt`, `lastFailedAttempt`,
196
196
  `processedAt`, `processedByAttempt`, and `returnedEventIds`. Timestamps are
197
- adapter clock values captured for atomic log operations, not exact database
197
+ adapter clock values captured for atomic store operations, not exact database
198
198
  commit times. A stale worker cannot rewrite a newer completion. The durable
199
199
  log can rebuild a causal forest and useful lifecycle boundaries without one
200
200
  row per attempt.
@@ -299,9 +299,9 @@ Resolution is manual: fix and retry, or skip.
299
299
  | Situation | Result |
300
300
  | --- | --- |
301
301
  | Process dies before the first arm is durable | Retry with the same ids, append again later, or call `drain()`. |
302
- | No recovery configured | A later top-level append or explicit `drain()` wakes the session. Reads never do. |
303
- | Recovery dies after claiming but before arming | Its unacknowledged queue delivery is the slower fallback. |
304
- | Log backend unavailable | Safe progress stops until it returns. |
302
+ | No scheduler configured | A later top-level append or explicit `drain()` wakes the session. Reads never do. |
303
+ | Scheduler invocation dies after claiming but before arming | Its unacknowledged queue delivery is the slower fallback. |
304
+ | Store backend unavailable | Safe progress stops until it returns. |
305
305
  | External effect succeeds before A2 completion | The handler may repeat it. Use idempotency or external fencing. |
306
306
 
307
307
  ## What append never throws for
@@ -311,4 +311,4 @@ identical batch retry returns rows with the same ids. A mixed batch throws
311
311
  [`PARTIAL_DUPLICATE_BATCH`](/reference/errors).
312
312
 
313
313
  For the queue route and deployment configuration, see
314
- [Going to production](/guides/production#2-add-recovery).
314
+ [Going to production](/guides/production#2-add-a-scheduler-for-handlers).
@@ -13,7 +13,8 @@ const events = await ordersServer.session(orderId).history()
13
13
  Everything that happened in this session, oldest first. Always the raw log,
14
14
  never a summary, never a snapshot. This is the session's audit trail and
15
15
  debugging story, and it is also fine to use inside handlers for questions like
16
- "did the shop already start?"
16
+ "did the shop already start?" Pass inclusive `gte` or `lte` indexes to read a
17
+ bounded slice.
17
18
 
18
19
  ## Reducers
19
20
 
@@ -86,24 +87,20 @@ export const ordersServer = createServer({
86
87
  })
87
88
  ```
88
89
 
89
- A reducer is a name (its identity, more on that below) plus the values that
90
- anchor it: `initialState` (the seed), an optional `stateSchema`, and the
91
- pure `fold`, `(state, event) => state`. It's derived *from* the
92
- contract, so the events type themselves; the two-step shape is
93
- deliberate: the first call fixes the state and event types, and
94
- `.fold()` receives fully concrete ones. No type arguments, no
95
- annotations, and literal unions (like the `z.enum` status above) survive
96
- the fold intact. `state()` folds the session's events through it and
97
- returns the result, along with `index`: the log position the state
98
- reflects. The browser uses that index to resume a live stream exactly
99
- where server-rendered state left off. See [Live UI](/guides/react).
100
-
101
- The snapshot and its remaining event tail come back in one consistent log
102
- operation. A missing snapshot reads the full log. A snapshot rejected by
103
- `stateSchema`, or a failed cache read, falls back to the full log and rebuilds
104
- from truth. `state()` is observational: it never runs handlers or waits for
105
- pending work to finish. Inside a handler, the returned index includes the
106
- triggering event and may include later events that committed before the read.
90
+ A reducer is a name (its identity, more on that below), `initialState`, an
91
+ optional `stateSchema`, and the pure `fold`, `(state, event) => state`. It is
92
+ derived from the contract, so the events type themselves; the two-step shape
93
+ fixes the state and event types before `.fold()` receives them. No type
94
+ arguments or annotations are needed, and literal unions survive the fold.
95
+ `state()` returns the result with `index`, the last log position included. The
96
+ fold includes every event through that index and none after it. The browser
97
+ resumes its live stream from that boundary. See [Live UI](/guides/react).
98
+
99
+ The snapshot and its remaining event tail come back in one consistent store
100
+ operation. A missing, invalid, or unreadable snapshot rebuilds from the full log.
101
+ `state()` is observational: it never runs handlers or waits for pending work.
102
+ Its index marks committed history, not handler completion. Inside a handler it
103
+ includes the trigger and may include later events committed before the read.
107
104
 
108
105
  The read and a following `ctx.session.append(name, ...events)` are separate
109
106
  operations. Concurrent appends and retries can move the frontier between them.
@@ -121,7 +118,7 @@ cache](/guides/local-first): a cached fold that fails the schema is
121
118
  discarded and refolded, catching shape drift a stale `name` can't.
122
119
 
123
120
  Note what these modules import: schemas and `experimental-a2`. Never `experimental-a2/server`,
124
- never a log backend. Contract and reducer are isomorphic by
121
+ never a store backend. Contract and reducer are isomorphic by
125
122
  construction; the browser runs the same reducer. More on the split in
126
123
  [Live UI](/guides/react#keep-the-backend-out-of-the-bundle). (In a
127
124
  server-only app you can keep the contract next to `createServer`
@@ -129,7 +126,7 @@ instead of in its own file.)
129
126
 
130
127
  ## Snapshots are a cache
131
128
 
132
- Folding a long session on every read would get slow, so the log backend
129
+ Folding a long session on every read would get slow, so the store backend
133
130
  caches folded state as a snapshot. You never interact with it, except for
134
131
  one string.
135
132