experimental-a2 0.17.0 → 0.18.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 (211) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-Bjce9_5U.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-Bjce9_5U.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +2 -2
  10. package/dist/{ai-UrWfKeuv.js → ai-BpOGm-zB.js} +2 -2
  11. package/dist/{ai-UrWfKeuv.js.map → ai-BpOGm-zB.js.map} +1 -1
  12. package/dist/{ai-vpGtcaCB.d.ts → ai-DrUaMyRt.d.ts} +3 -3
  13. package/dist/{ai-vpGtcaCB.d.ts.map → ai-DrUaMyRt.d.ts.map} +1 -1
  14. package/dist/ai-client.d.ts +2 -2
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.browser.d.ts +1 -0
  17. package/dist/ai-fx.browser.js +6 -0
  18. package/dist/ai-fx.browser.js.map +1 -0
  19. package/dist/ai-fx.d.ts +10 -0
  20. package/dist/ai-fx.d.ts.map +1 -0
  21. package/dist/ai-fx.js +634 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-BuLh_EYi.js +273 -0
  24. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  25. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  26. package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
  27. package/dist/ai-pi.browser.d.ts +1 -0
  28. package/dist/ai-pi.browser.js +6 -0
  29. package/dist/ai-pi.browser.js.map +1 -0
  30. package/dist/ai-pi.d.ts +17 -0
  31. package/dist/ai-pi.d.ts.map +1 -0
  32. package/dist/ai-pi.js +904 -0
  33. package/dist/ai-pi.js.map +1 -0
  34. package/dist/ai-react.d.ts +5 -5
  35. package/dist/ai-react.js +2 -2
  36. package/dist/ai-server.d.ts +47 -25
  37. package/dist/ai-server.d.ts.map +1 -1
  38. package/dist/ai-server.js +106 -37
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai.d.ts +1 -1
  41. package/dist/ai.js +1 -1
  42. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  43. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  44. package/dist/{client-COaiaDub.js → client-BReKqwZC.js} +3 -3
  45. package/dist/{client-COaiaDub.js.map → client-BReKqwZC.js.map} +1 -1
  46. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  47. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  48. package/dist/client.d.ts +3 -3
  49. package/dist/client.js +1 -1
  50. package/dist/index.d.ts +5 -5
  51. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  52. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  53. package/dist/{internal-CbVvEwad.js → internal-B2T3U4Rq.js} +8 -2
  54. package/dist/internal-B2T3U4Rq.js.map +1 -0
  55. package/dist/otel.d.ts +1 -1
  56. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  57. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/reducer-BAXc8JyF.js.map +1 -1
  61. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  62. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  63. package/dist/scheduler-qstash.d.ts +2 -2
  64. package/dist/scheduler-qstash.js +2 -2
  65. package/dist/scheduler-vercel.d.ts +2 -2
  66. package/dist/scheduler-vercel.js +2 -2
  67. package/dist/{server-Rj64fNvX.js → server-Dw_k_7kO.js} +93 -60
  68. package/dist/server-Dw_k_7kO.js.map +1 -0
  69. package/dist/{server-CGM8WzRG.d.ts → server-En7cbi3v.d.ts} +36 -7
  70. package/dist/server-En7cbi3v.d.ts.map +1 -0
  71. package/dist/server.d.ts +4 -4
  72. package/dist/server.js +1 -1
  73. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-Bo-XTFEy.d.ts} +2 -2
  74. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  75. package/dist/{store-Gpkq7WBo.d.ts → store-Djdv0bnZ.d.ts} +31 -2
  76. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  77. package/dist/store-N8PXxDAS.js.map +1 -1
  78. package/dist/store-memory.d.ts +1 -1
  79. package/dist/store-memory.js +32 -6
  80. package/dist/store-memory.js.map +1 -1
  81. package/dist/{store-polling-4_y8lETm.js → store-polling-B5FcwJfo.js} +2 -2
  82. package/dist/{store-polling-4_y8lETm.js.map → store-polling-B5FcwJfo.js.map} +1 -1
  83. package/dist/store-postgres.d.ts +1 -1
  84. package/dist/store-postgres.js +54 -3
  85. package/dist/store-postgres.js.map +1 -1
  86. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-BAwS8RWn.js} +3 -3
  87. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  88. package/dist/store-redis-http.d.ts +1 -1
  89. package/dist/store-redis-http.js +3 -3
  90. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-BRITti_o.js} +187 -68
  91. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  92. package/dist/store-redis.d.ts +1 -1
  93. package/dist/store-redis.js +1 -1
  94. package/dist/store-sqlite.d.ts +1 -1
  95. package/dist/store-sqlite.js +32 -2
  96. package/dist/store-sqlite.js.map +1 -1
  97. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  98. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  99. package/dist/{wire-DjfHOvdW.js → wire-rSUglYo8.js} +2 -2
  100. package/dist/{wire-DjfHOvdW.js.map → wire-rSUglYo8.js.map} +1 -1
  101. package/docs/01-quickstart.mdx +49 -83
  102. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  103. package/docs/actors/01-introduction.mdx +48 -86
  104. package/docs/actors/02-concurrency.mdx +43 -70
  105. package/docs/actors/03-timers.mdx +28 -42
  106. package/docs/actors/04-routes.mdx +110 -150
  107. package/docs/actors/meta.ts +1 -1
  108. package/docs/agents/01-introduction.mdx +257 -0
  109. package/docs/agents/02-tools.mdx +247 -0
  110. package/docs/agents/03-conversations.mdx +538 -0
  111. package/docs/agents/04-harnesses.mdx +136 -0
  112. package/docs/agents/meta.ts +1 -0
  113. package/docs/client/01-react.mdx +294 -0
  114. package/docs/client/02-transports.mdx +86 -0
  115. package/docs/client/03-presence.mdx +224 -0
  116. package/docs/client/meta.ts +1 -0
  117. package/docs/contract/01-events.mdx +57 -0
  118. package/docs/contract/02-state.mdx +142 -0
  119. package/docs/contract/03-sessions.mdx +175 -0
  120. package/docs/index.mdx +108 -144
  121. package/docs/reference/01-core.mdx +139 -0
  122. package/docs/reference/02-server.mdx +855 -0
  123. package/docs/reference/03-client.mdx +301 -0
  124. package/docs/reference/04-ai.mdx +722 -0
  125. package/docs/reference/05-adapters.mdx +351 -0
  126. package/docs/reference/06-errors.mdx +138 -0
  127. package/docs/reference/meta.ts +1 -1
  128. package/docs/server/01-handlers.mdx +200 -0
  129. package/docs/server/02-timers.mdx +130 -0
  130. package/docs/server/03-cancellation.mdx +141 -0
  131. package/docs/server/04-routes.mdx +243 -0
  132. package/docs/server/05-production.mdx +322 -0
  133. package/docs/server/06-guarantees.mdx +123 -0
  134. package/examples/playground/DEPLOY.md +1 -1
  135. package/examples/playground/app/a0/[appId]/page.tsx +11 -6
  136. package/examples/playground/app/a0/app-list.tsx +9 -2
  137. package/examples/playground/app/a0/app-view.tsx +9 -1
  138. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  139. package/examples/playground/app/a0/apps.test.ts +12 -0
  140. package/examples/playground/app/a0/apps.ts +22 -7
  141. package/examples/playground/app/a0/chat.tsx +47 -1
  142. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  143. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  144. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  145. package/examples/playground/app/a0/model.test.ts +18 -1
  146. package/examples/playground/app/a0/model.ts +47 -10
  147. package/examples/playground/app/a0/no-photon.cjs +4 -0
  148. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  149. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  150. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  153. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  154. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  155. package/examples/playground/app/a0/server.ts +100 -45
  156. package/examples/playground/app/a0/session.ts +17 -5
  157. package/examples/playground/app/a0/shell.tsx +11 -3
  158. package/examples/playground/app/agent/ingress.test.ts +6 -3
  159. package/examples/playground/app/agent/server.ts +9 -6
  160. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  161. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  162. package/examples/playground/app/chat/server.test.ts +30 -26
  163. package/examples/playground/app/chat/server.ts +46 -46
  164. package/examples/playground/next.config.ts +17 -5
  165. package/examples/playground/package.json +4 -1
  166. package/examples/playground/vercel.json +18 -0
  167. package/package.json +27 -3
  168. package/src/ai-agent.ts +261 -87
  169. package/src/ai-fx.browser.ts +5 -0
  170. package/src/ai-fx.ts +833 -0
  171. package/src/ai-harness.ts +518 -0
  172. package/src/ai-model-metadata.ts +11 -6
  173. package/src/ai-pi.browser.ts +5 -0
  174. package/src/ai-pi.ts +1263 -0
  175. package/src/ai-sdk-step.ts +2 -2
  176. package/src/ai-server.ts +3 -1
  177. package/src/internal.ts +7 -0
  178. package/src/libfx.d.ts +34 -0
  179. package/src/reducer.ts +1 -1
  180. package/src/server.ts +177 -19
  181. package/src/store-memory.ts +42 -7
  182. package/src/store-postgres.ts +80 -0
  183. package/src/store-redis-core.ts +210 -79
  184. package/src/store-redis-notify.ts +2 -0
  185. package/src/store-sqlite.ts +60 -0
  186. package/src/store.ts +33 -0
  187. package/src/telemetry.ts +1 -0
  188. package/dist/internal-CbVvEwad.js.map +0 -1
  189. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  190. package/dist/server-Rj64fNvX.js.map +0 -1
  191. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  192. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  193. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  194. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  195. package/docs/concepts/01-contracts.mdx +0 -237
  196. package/docs/concepts/02-handlers.mdx +0 -284
  197. package/docs/concepts/03-durability.mdx +0 -314
  198. package/docs/concepts/04-state.mdx +0 -209
  199. package/docs/guides/01-timers.mdx +0 -193
  200. package/docs/guides/02-cancellation.mdx +0 -161
  201. package/docs/guides/03-react.mdx +0 -324
  202. package/docs/guides/04-local-first.mdx +0 -47
  203. package/docs/guides/05-production.mdx +0 -534
  204. package/docs/guides/06-ai-agents.mdx +0 -1408
  205. package/docs/guides/08-application-data.mdx +0 -113
  206. package/docs/guides/09-presence.mdx +0 -262
  207. package/docs/guides/10-transports.mdx +0 -221
  208. package/docs/reference/01-api.mdx +0 -2358
  209. package/docs/reference/02-errors.mdx +0 -150
  210. /package/docs/{concepts → contract}/meta.ts +0 -0
  211. /package/docs/{guides → server}/meta.ts +0 -0
@@ -0,0 +1,301 @@
1
+ ---
2
+ title: Client
3
+ description: "`experimental-a2/client`, `experimental-a2/react`, and `experimental-a2/cache-indexeddb`."
4
+ ---
5
+
6
+ ## `experimental-a2/client`
7
+
8
+ ```ts
9
+ createClient(options: {
10
+ contract: Contract // requires a public reducer
11
+ api: ClientApi
12
+ gcTime?: number // idle session lifetime; 5 minutes by default
13
+ participant?: string // default presence identity for every session
14
+ reconnect?: (failure: {
15
+ sessionId: string
16
+ error: A2ClientError | null
17
+ attempt: number
18
+ }) => number | false
19
+ }): A2Client
20
+
21
+ type ClientApi =
22
+ | SessionUrl // one route: GET SSE stream + POST push
23
+ | { type: 'http'; push: SessionUrl; stream: SessionUrl }
24
+ | { type: 'ws'; url: SessionUrl; multiplex?: false }
25
+ | { type: 'ws'; url: string; multiplex: true }
26
+
27
+ type SessionUrl = string | ((sessionId: string) => string)
28
+ ```
29
+
30
+ The framework-agnostic session client `experimental-a2/react` is built on: the
31
+ SSE subscription with frontier resume and reconnection, the optimistic push
32
+ queue with ack and rollback, and the local fold. Use it from any framework, or
33
+ none.
34
+
35
+ **`reconnect`**
36
+
37
+ - Runs after a failed connection or a stream end, including a clean end with
38
+ `error: null`. Not for an explicit restart, an idle-stream refresh, or a
39
+ caller closing the stream.
40
+ - Returns the delay in ms before the next attempt (finite, non-negative, at
41
+ most 2,147,483,647), or `false` to pause. Synchronous; a throw or invalid
42
+ result pauses with an `A2ClientError`.
43
+ - `attempt` starts at 1, counts consecutive attempts per session, and resets
44
+ when the stream goes live or `session.reconnect()` restarts it. It is
45
+ separate from the lifetime `reconnects` count.
46
+ - Default: retry indefinitely, from 500 ms doubling up to five seconds.
47
+ - Paused: `connection.status` is `paused` with its last error. State,
48
+ hydration, optimistic writes, and leases stay intact. Neither a new lease nor
49
+ a push resumes it (push retries are independent); `session.reconnect()`
50
+ does, with a fresh attempt count. Releasing the last lease still closes the
51
+ stream; a later first lease starts a new connection. Resuming keeps the
52
+ stream's own position, even if hydration advanced state while paused.
53
+
54
+ **`SessionUrl` functions** run for each HTTP request or WebSocket connection;
55
+ A2 still adds its `sessionId` and read-bound query parameters. The function
56
+ does not authorize that ID: bind any path scope or URL token to
57
+ `operation.sessionId` in `authorize`, and keep URL tokens short-lived (URLs
58
+ reach infrastructure logs). A multiplexed WebSocket URL is a plain string. See
59
+ [Transports](/client/transports).
60
+
61
+ ### `client.session(id, options?)`
62
+
63
+ `client.session(id, { initial?, initialEvents?, participant? })` returns a
64
+ handle:
65
+
66
+ | Member | Does |
67
+ | --- | --- |
68
+ | `getSnapshot()`, `subscribe()` | the `useSyncExternalStore` contract; snapshots carry `state`, `events`, `index`, `history`, `hydrated`, and `connection`, as in [`useSession`](#usesessionoptions) |
69
+ | `push()`, `loadHistory()`, `reconnect()` | as in `useSession` |
70
+ | `connect()` | takes a stream lease and returns its release. Leases refcount per handle: the first connects, releasing the last closes, releasing twice is a no-op |
71
+ | `close()` | hard stop: drops every lease and closes now; a later `connect()` starts fresh |
72
+ | `setPresence()` | presence contracts only, as in `useSession`; snapshots then carry `presence` |
73
+
74
+ | Option | What it is |
75
+ | --- | --- |
76
+ | `initial` | an [`InitialSnapshot<S>`](#sessionprovider), as in React |
77
+ | `initialEvents` | earlier raw events for history UIs; they merge into `events` and never affect the folded state or stream frontier |
78
+ | `participant` | the identity `setPresence` sends under; overrides `createClient`'s |
79
+
80
+ - One `A2Client` returns the same live handle for repeated `session(id)`
81
+ calls. A later call with a further server-rendered frontier advances it
82
+ without dropping pending optimistic events; a stale render cannot move it
83
+ backward.
84
+ - Idle handles are evicted after `gcTime`. This memory layer keeps identity
85
+ across route transitions; `experimental-a2/cache-indexeddb` keeps the replica
86
+ across reloads.
87
+ - A pending `initial` Promise delays cold connection startup, including a
88
+ connection a push acquires, and starts no request of its own. Replacing the
89
+ source or `close()` invalidates its pending result and any deferred adoption
90
+ behind a write.
91
+ - Releasing the last lease does not cancel the caller's Promise; it can still
92
+ seed cached data without reopening transport. Idle eviction detaches A2's
93
+ listener. Repeating the same Promise in that cached runtime does not restart
94
+ cancelled work.
95
+
96
+ ## `experimental-a2/react`
97
+
98
+ ### `createReact(options)`
99
+
100
+ ```ts
101
+ createReact(options: {
102
+ client: A2Client // bind a shared client: api + contract + session identity
103
+ }): { SessionProvider, useSession }
104
+
105
+ // or, bind the contract and supply api through the Provider:
106
+ createReact(options: {
107
+ contract: Contract // requires a public reducer
108
+ }): { SessionProvider, useSession }
109
+ ```
110
+
111
+ A factory, like `createContext`: call it once in a `'use client'` module and
112
+ export the bound pair. Server components import `SessionProvider` from it as a
113
+ client reference; client components import `useSession` from the same file.
114
+ Bind a shared `client` when code outside the provider pushes before a route
115
+ transition and the destination should adopt the same optimistic session;
116
+ `{ contract }` suits provider-bound clients. See
117
+ [Bind React once](/client/react#bind-react-once).
118
+
119
+ ### `<SessionProvider>`
120
+
121
+ | Prop | What it is |
122
+ | --------------- | ------------------------------------------------------ |
123
+ | `sessionId` | which session to subscribe to |
124
+ | `initial` | optional snapshot or Promise of a snapshot; `null` means bootstrap |
125
+ | `initialEvents` | optional earlier raw events for a history UI |
126
+ | `participant` | overrides the client's `participant`; one of the two is required to call `setPresence` |
127
+
128
+ - Supplies scope and optional initial data. Mounted enabled hooks own
129
+ connection leases and reconnect from the last streamed event.
130
+ - `participant` binds at the session's first resolution; later changes are
131
+ ignored for the session's runtime lifetime.
132
+ - `initial` is the complete state/index snapshot, plus `clearIndex` once the
133
+ session has been cleared. A new session streams from after `initial.index`,
134
+ including events appended between render and connection. Later snapshots
135
+ advance state but keep the resume position, so delayed events still enter
136
+ the feed without folding twice.
137
+ - `initialEvents` only preloads the raw `events` feed with entries before that
138
+ frontier; it never affects state hydration or stream resumption.
139
+
140
+ `InitialSnapshot<S>` is `StateSnapshot<S> | null | undefined |
141
+ Promise<StateSnapshot<S> | null | undefined>`, exported from `experimental-a2`
142
+ and `experimental-a2/client`. A cold session waits for a supplied Promise
143
+ before connecting. A snapshot seeds state; `null` or `undefined` selects
144
+ subscription bootstrap. A rejection pauses cold startup and reports through
145
+ `connection.error`; supply a new Promise, snapshot, or `null` to resume. A warm
146
+ session keeps its state and connection. The Provider never suspends. Keep
147
+ Promise identity stable. See
148
+ [Seed the first render](/client/react#seed-the-first-render).
149
+
150
+ ### `useSession(options?)`
151
+
152
+ ```ts
153
+ const { state, hydrated, push, reconnect, events, index, loadHistory, history, connection } = useSession()
154
+ ```
155
+
156
+ | Option | Default | What it does |
157
+ | --- | --- | --- |
158
+ | `sessionId`, `client`, `participant`, `initial` | from the Provider and bound client | scope and seed |
159
+ | `enabled` | `true` | owns a connection lease. `false` defers automatic I/O: the hook observes cached state, other owners continue, and explicit pushes can still acquire a connection |
160
+ | `suspend` | `false` | `true` reads the `initial` Promise with React `use()`, through the nearest Suspense and Error boundaries. Otherwise a pending cold Promise renders the reducer seed. Automatic bootstrap never suspends, even when the Promise resolves to `null` or `undefined` |
161
+
162
+ | Member | What it is |
163
+ | --- | --- |
164
+ | `state` | the reducer's client state type. A supplied snapshot seeds the first render; otherwise `reducer.initialState` (through the client projection) shows until a snapshot replaces it atomically. Folds live events through the shared reducer |
165
+ | `hydrated` | `true` once an initial snapshot or bootstrap applies, even an empty state at index 0; `false` while only the reducer seed exists. Stays `true` through reconnects, disabled hooks, and a failed replacement preload. Not a sign of a live connection or fresh data. Shared with `session.getSnapshot()` |
166
+ | `events` | the raw feed: observed, seeded, or backscrolled. Starts after `initial.index` unless `initialEvents` or `loadHistory` add earlier entries |
167
+ | `index` | the latest server-confirmed state index, the `lastSeenIndex` for [cancellation](/server/cancellation) |
168
+ | `push`, `reconnect`, `loadHistory`, `history`, `connection` | below |
169
+ | `presence`, `setPresence` | presence contracts only; below |
170
+
171
+ Without a supplied snapshot, the server sends one first on the same
172
+ subscription; bootstrap errors and retries use `connection`.
173
+
174
+ **`loadHistory({ before?, limit? })`** fetches a bounded slice below the
175
+ frontier (same route, `gte`/`lte` query parameters) and resolves with the
176
+ events in that range.
177
+
178
+ - Merges into `events`: deduped, ordered, shared by every handle of the
179
+ session. Never touches `state` or the optimistic overlay.
180
+ - Defaults walk back 50 at a time from the oldest loaded event, also after a
181
+ newer snapshot hydrates. `before` is an exclusive upper bound for another
182
+ range.
183
+ - Calls serialize per session; loaded ranges are not refetched.
184
+ - `history` is `{ loading, complete, oldestLoaded }`. `complete`: the feed
185
+ reaches index 1, or the first event after the latest clear, or the log is
186
+ empty.
187
+ - The `ws` api has no history lane: `loadHistory` throws a `TypeError`.
188
+
189
+ **Clears** reset `state` to the reducer's initial state and drop `events` at or
190
+ below the marker. Unconfirmed pushes stay in the overlay and apply to the reset
191
+ state. A push acked below the marker was cleared with it: it leaves the overlay
192
+ and its `confirmed` resolves.
193
+
194
+ **`push`** appends optimistically: validated locally, rolled back on rejection,
195
+ retried for `STORE_UNAVAILABLE` and transport failures. Awaiting it gives the
196
+ server ack; its `confirmed` is a lazy promise for when the stream has delivered
197
+ the batch back:
198
+
199
+ ```ts
200
+ const t0 = performance.now()
201
+ const result = push({ type: 'created', payload })
202
+ await result // the server ack: push→ack latency
203
+ await result.confirmed // the stream round-trip: ack→stream latency
204
+ const roundtripMs = performance.now() - t0
205
+ ```
206
+
207
+ - `confirmed` waits for the stream to deliver through the acked index. A
208
+ bootstrap snapshot, or a stream starting after a supplied initial index,
209
+ delivers through that index; hydration does not otherwise confirm an
210
+ unobserved ack.
211
+ - A push never waits for bootstrap, and an HTTP push never waits for a pending
212
+ initial Promise. A WebSocket push rides its connection, which opens once that
213
+ Promise settles.
214
+ - Overlapping calls on one session client update the view immediately and
215
+ enter the transport in invocation order, each waiting only for the previous
216
+ call's ack or final rejection. Other sessions and clients run concurrently
217
+ and converge on the log's durable order.
218
+ - A rejection rejects both promises with the same `A2ClientError`. `confirmed`
219
+ is materialized only when accessed, so ignoring it costs nothing.
220
+
221
+ **`reconnect`** (`session.reconnect(): void`) restarts a leased subscription
222
+ now from its current cursor, including one the policy paused. It keeps state,
223
+ pending writes, and leases, and does nothing without an active lease. It
224
+ requests a restart; it returns no promise for catch-up. Use it after a custom
225
+ REST mutation. A multiplexed WebSocket keeps its socket and other session
226
+ lanes; a single-session WebSocket replaces its socket.
227
+
228
+ **`connection`** is a discriminated union; `error` only exists while
229
+ disconnected:
230
+
231
+ ```ts
232
+ // in a client module:
233
+ import type { A2ClientError } from 'experimental-a2/client'
234
+
235
+ type Connection =
236
+ | { status: 'idle' }
237
+ | { status: 'connecting'; reconnects: number; error: A2ClientError | null }
238
+ | { status: 'paused'; reconnects: number; error: A2ClientError | null }
239
+ | { status: 'live'; reconnects: number }
240
+ | { status: 'closed' }
241
+ ```
242
+
243
+ "Reconnecting…" is `status === 'connecting' && reconnects > 0`. `live` means
244
+ bytes are flowing: the server heartbeats the stream (`: ping` every 15s), and a
245
+ client that hears nothing for two beats aborts and reconnects.
246
+
247
+ **`presence`** is the replicated ephemeral map,
248
+ `Record<participantId, { [field]: { value, seen, at } }>`, including this
249
+ client. **`setPresence(values)`** is fire-and-forget:
250
+
251
+ - Validated locally and merged into the local map immediately.
252
+ - Coalesced on the wire to a fixed cadence: a leading send, then at most one
253
+ merged send per interval, later values winning per field.
254
+ - Stamped with the frontier at send time as `seen`.
255
+ - No ack, no `confirmed`, no retry. Parallel sends are safe: merges are
256
+ last-writer-wins by `at`, and stamps are monotonic per session (wall-seeded,
257
+ guarded so overlapping sends never invert).
258
+
259
+ Both exist only when the contract declares `presence`, typed from its schemas
260
+ through the reducer with no type arguments (the reducer never folds presence).
261
+ `setPresence` requires a `participant`, from the client or the provider. See
262
+ [Presence](/client/presence).
263
+
264
+ ### `useSession(client, sessionId, options?)`
265
+
266
+ ```ts
267
+ useSession(client: A2Client, sessionId: string, options?: {
268
+ participant?: string // overrides the client's participant
269
+ initial?: InitialSnapshot<S>
270
+ enabled?: boolean
271
+ suspend?: boolean
272
+ }): UseSessionResult
273
+ ```
274
+
275
+ The provider-less hook, for components that reach sessions ad hoc (a sidebar
276
+ of channel sessions, a user session read from a menu). Same result as the
277
+ bound hook.
278
+
279
+ - Identity-mapped: every hook and provider mounting a session shares one
280
+ runtime. The stream is refcounted: the first mount connects, the last
281
+ unmount closes (StrictMode-safe).
282
+ - `initial` takes the result of `session.state(reducer)` or its Promise. Pass
283
+ it whole: its `clearIndex` lets a cached client drop history from before a
284
+ clear it never streamed (the automatic snapshot carries it too). Without it,
285
+ the subscription obtains a snapshot. For deliberate full replay, supply
286
+ `{ state: reducer.initialState, index: 0 }`.
287
+ - `enabled: false` defers automatic I/O, as above.
288
+
289
+ See [React](/client/react).
290
+
291
+ ## `experimental-a2/cache-indexeddb`
292
+
293
+ ```ts
294
+ indexedDb(): A2ClientCache
295
+ ```
296
+
297
+ A browser-side cache for `SessionProvider`: the session's events (immutable;
298
+ writes are idempotent by index), a folded snapshot (invalidated by a reducer
299
+ rename), and the pending-push queue. `cache.clear(sessionId?)` is the logout
300
+ story. Create instances in client code; they're live objects, not serializable
301
+ props. See [Client lifetime](/client/react#client-lifetime).