experimental-a2 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.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-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
  11. package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
  12. package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
  13. package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.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 +692 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
  24. package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
  25. package/dist/ai-harness-BuLh_EYi.js +273 -0
  26. package/dist/ai-harness-BuLh_EYi.js.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 +950 -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 +113 -40
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  41. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  42. package/dist/ai.d.ts +1 -1
  43. package/dist/ai.js +1 -1
  44. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  45. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  46. package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
  47. package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
  48. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  49. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  50. package/dist/client.d.ts +3 -3
  51. package/dist/client.js +1 -1
  52. package/dist/index.d.ts +5 -5
  53. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  54. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  55. package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
  56. package/dist/internal-CscSY7K_.js.map +1 -0
  57. package/dist/otel.d.ts +1 -1
  58. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  59. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  60. package/dist/react.d.ts +1 -1
  61. package/dist/react.js +1 -1
  62. package/dist/reducer-BAXc8JyF.js.map +1 -1
  63. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  64. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-qstash.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +7 -7
  69. package/dist/scheduler-vercel.d.ts.map +1 -1
  70. package/dist/scheduler-vercel.js +9 -10
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
  73. package/dist/server-D-Y4pqwu.js.map +1 -0
  74. package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
  75. package/dist/server-DYAATFuT.d.ts.map +1 -0
  76. package/dist/server.d.ts +4 -4
  77. package/dist/server.js +1 -1
  78. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  79. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  80. package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
  81. package/dist/store-CyLUy86v.d.ts.map +1 -0
  82. package/dist/store-N8PXxDAS.js.map +1 -1
  83. package/dist/store-memory.d.ts +1 -1
  84. package/dist/store-memory.js +56 -11
  85. package/dist/store-memory.js.map +1 -1
  86. package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
  87. package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  88. package/dist/store-postgres.d.ts +1 -1
  89. package/dist/store-postgres.d.ts.map +1 -1
  90. package/dist/store-postgres.js +106 -25
  91. package/dist/store-postgres.js.map +1 -1
  92. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
  93. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  94. package/dist/store-redis-http.d.ts +1 -1
  95. package/dist/store-redis-http.js +3 -3
  96. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
  97. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  98. package/dist/store-redis.d.ts +1 -1
  99. package/dist/store-redis.js +1 -1
  100. package/dist/store-sqlite.d.ts +1 -1
  101. package/dist/store-sqlite.js +66 -21
  102. package/dist/store-sqlite.js.map +1 -1
  103. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  104. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  105. package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
  106. package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
  107. package/docs/01-quickstart.mdx +49 -83
  108. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  109. package/docs/actors/01-introduction.mdx +48 -86
  110. package/docs/actors/02-concurrency.mdx +43 -70
  111. package/docs/actors/03-timers.mdx +28 -42
  112. package/docs/actors/04-routes.mdx +110 -150
  113. package/docs/actors/meta.ts +1 -1
  114. package/docs/agents/01-introduction.mdx +257 -0
  115. package/docs/agents/02-tools.mdx +247 -0
  116. package/docs/agents/03-conversations.mdx +538 -0
  117. package/docs/agents/04-harnesses.mdx +136 -0
  118. package/docs/agents/meta.ts +1 -0
  119. package/docs/client/01-react.mdx +294 -0
  120. package/docs/client/02-transports.mdx +86 -0
  121. package/docs/client/03-presence.mdx +224 -0
  122. package/docs/client/meta.ts +1 -0
  123. package/docs/contract/01-events.mdx +57 -0
  124. package/docs/contract/02-state.mdx +142 -0
  125. package/docs/contract/03-sessions.mdx +175 -0
  126. package/docs/index.mdx +109 -145
  127. package/docs/reference/01-core.mdx +139 -0
  128. package/docs/reference/02-server.mdx +860 -0
  129. package/docs/reference/03-client.mdx +301 -0
  130. package/docs/reference/04-ai.mdx +731 -0
  131. package/docs/reference/05-adapters.mdx +351 -0
  132. package/docs/reference/06-errors.mdx +138 -0
  133. package/docs/reference/meta.ts +1 -1
  134. package/docs/server/01-handlers.mdx +200 -0
  135. package/docs/server/02-timers.mdx +130 -0
  136. package/docs/server/03-cancellation.mdx +142 -0
  137. package/docs/server/04-routes.mdx +243 -0
  138. package/docs/server/05-production.mdx +323 -0
  139. package/docs/server/06-guarantees.mdx +132 -0
  140. package/examples/playground/DEPLOY.md +3 -5
  141. package/examples/playground/app/a0/[appId]/page.tsx +12 -13
  142. package/examples/playground/app/a0/app-list.tsx +9 -2
  143. package/examples/playground/app/a0/app-view.tsx +9 -1
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  145. package/examples/playground/app/a0/apps.test.ts +12 -0
  146. package/examples/playground/app/a0/apps.ts +22 -7
  147. package/examples/playground/app/a0/chat.tsx +47 -1
  148. package/examples/playground/app/a0/devtools.tsx +2 -8
  149. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  150. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/model.test.ts +18 -1
  153. package/examples/playground/app/a0/model.ts +47 -10
  154. package/examples/playground/app/a0/no-photon.cjs +4 -0
  155. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  156. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  157. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  158. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  159. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  160. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  161. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  162. package/examples/playground/app/a0/server.ts +98 -54
  163. package/examples/playground/app/a0/session.ts +17 -5
  164. package/examples/playground/app/a0/shell.tsx +11 -3
  165. package/examples/playground/app/agent/ingress.test.ts +6 -3
  166. package/examples/playground/app/agent/server.ts +9 -6
  167. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  168. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  169. package/examples/playground/app/chat/server.test.ts +30 -26
  170. package/examples/playground/app/chat/server.ts +46 -46
  171. package/examples/playground/next.config.ts +17 -5
  172. package/examples/playground/package.json +4 -1
  173. package/examples/playground/vercel.json +18 -0
  174. package/package.json +27 -3
  175. package/src/ai-agent.ts +271 -90
  176. package/src/ai-fx.browser.ts +5 -0
  177. package/src/ai-fx.ts +912 -0
  178. package/src/ai-harness.ts +518 -0
  179. package/src/ai-model-metadata.ts +11 -6
  180. package/src/ai-pi.browser.ts +5 -0
  181. package/src/ai-pi.ts +1322 -0
  182. package/src/ai-sdk-step.ts +2 -2
  183. package/src/ai-server.ts +3 -1
  184. package/src/ai-tool-input.ts +45 -0
  185. package/src/internal.ts +13 -0
  186. package/src/libfx.d.ts +34 -0
  187. package/src/reducer.ts +1 -1
  188. package/src/scheduler-qstash.ts +5 -5
  189. package/src/scheduler-vercel.ts +14 -15
  190. package/src/server.ts +233 -29
  191. package/src/store-memory.ts +92 -16
  192. package/src/store-postgres.ts +156 -25
  193. package/src/store-redis-core.ts +322 -110
  194. package/src/store-redis-notify.ts +2 -0
  195. package/src/store-sqlite.ts +121 -28
  196. package/src/store.ts +55 -3
  197. package/src/telemetry.ts +1 -0
  198. package/dist/internal-CbVvEwad.js.map +0 -1
  199. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  200. package/dist/server-Rj64fNvX.js.map +0 -1
  201. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  202. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  203. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  204. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  205. package/docs/concepts/01-contracts.mdx +0 -237
  206. package/docs/concepts/02-handlers.mdx +0 -284
  207. package/docs/concepts/03-durability.mdx +0 -314
  208. package/docs/concepts/04-state.mdx +0 -209
  209. package/docs/guides/01-timers.mdx +0 -193
  210. package/docs/guides/02-cancellation.mdx +0 -161
  211. package/docs/guides/03-react.mdx +0 -324
  212. package/docs/guides/04-local-first.mdx +0 -47
  213. package/docs/guides/05-production.mdx +0 -534
  214. package/docs/guides/06-ai-agents.mdx +0 -1408
  215. package/docs/guides/08-application-data.mdx +0 -113
  216. package/docs/guides/09-presence.mdx +0 -262
  217. package/docs/guides/10-transports.mdx +0 -221
  218. package/docs/reference/01-api.mdx +0 -2358
  219. package/docs/reference/02-errors.mdx +0 -150
  220. /package/docs/{concepts → contract}/meta.ts +0 -0
  221. /package/docs/{guides → server}/meta.ts +0 -0
@@ -5,11 +5,6 @@ description: "Schedule a typed event back to the same instance. Deadlines surviv
5
5
 
6
6
  ## A deadline is a message, delivered later
7
7
 
8
- `ctx.schedule` mirrors `ctx.send`, one method per declared event, with
9
- a timing argument: exactly one of `{ delay: '1h' }` or `{ at: date }`.
10
- It hands the event to the configured scheduler, and delivery lands on
11
- the instance's lane like any other message.
12
-
13
8
  ```ts escrow.ts
14
9
  import { actor } from 'experimental-a2/actor'
15
10
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
@@ -46,38 +41,29 @@ export const escrow = actor<Escrow>({
46
41
  })
47
42
  ```
48
43
 
49
- Held funds now have two futures, and both are handled: someone accepts
50
- the offer, or the hour passes and the funds release themselves. The
51
- deadline is durable: it outlives the invocation that armed it and any
52
- deploys in between.
44
+ `ctx.schedule` mirrors `ctx.send`, one method per declared event, with
45
+ a timing argument: exactly one of `{ delay: '1h' }` or `{ at: date }`.
46
+ It requires the `scheduler` option. The deadline outlives the
47
+ invocation that armed it and any deploys in between, and delivery lands
48
+ on the instance's lane like any other message.
53
49
 
54
- Scheduling requires the `scheduler` option. Adapters, delay limits, and
55
- delivery semantics are the session timers underneath; see
56
- [Timers and delays](/guides/timers).
50
+ Adapters, delay limits, and delivery semantics are the
51
+ [session timers](/server/timers) underneath.
57
52
 
58
53
  ## Stale timers are no-ops
59
54
 
60
- When `accept` wins the race, the release timer still fires an hour
61
- later. It meets the same first-line guard the finalizer pattern already
62
- uses (`held[offerId] === undefined`) and does nothing. When the timer
63
- wins, a late `accept` meets that guard instead.
64
-
65
- There is no cancellation API, and the log records that the timer fired.
66
- Guarded delivery is the pattern: let the event land, read the state,
67
- return when it no longer applies. The guard that makes finalizers
68
- idempotent is the same guard that makes deadlines safe.
55
+ When `accept` wins, the release timer still fires an hour later, meets
56
+ the `held[offerId] === undefined` guard, and does nothing. When the
57
+ timer wins, a late `accept` meets the same guard. There is no
58
+ cancellation API, and the log records that the timer fired: let the
59
+ event land and return when it no longer applies.
69
60
 
70
61
  ## Identity and retries
71
62
 
72
63
  A timer's name defaults to the target event's name, scoped to the
73
- triggering message. A re-run of a crashed handler therefore addresses
74
- the same timer rather than arming a second one, and provider
75
- deduplication plus stable event ids fold duplicate deliveries into one
76
- append. Relative delays anchor to the triggering message's durable
77
- `createdAt`, so re-runs resolve the same due time.
78
-
79
- One handler run gets one timer per target event. To arm several toward
80
- the same event, name them apart:
64
+ triggering message, so a re-run of a crashed handler addresses the same
65
+ timer instead of arming a second. One handler run gets one timer per
66
+ target event; to arm several toward the same event, name them apart:
81
67
 
82
68
  ```ts
83
69
  // inside a handler:
@@ -87,24 +73,22 @@ await ctx.schedule.release(
87
73
  )
88
74
  ```
89
75
 
76
+ Duplicate deliveries fold into one append, and relative delays anchor
77
+ to the triggering message's `createdAt`; see
78
+ [Identity and retries](/server/timers#identity-and-retries).
79
+
90
80
  ## Scheduling is immediate
91
81
 
92
82
  :::warning
93
- Unlike `ctx.send`, `ctx.schedule` is not buffered. A timer lives at the
94
- provider, not in the log, so it cannot ride the handler's atomic
95
- commit: the call awaits provider acceptance right where it happens. A
96
- handler that schedules and then refuses has still armed the timer.
97
- Guarded delivery makes that safe (the stale timer fires into a no-op),
98
- but order handlers accordingly: guards and refusals first, `ctx.schedule`
99
- last.
83
+ Unlike `ctx.send`, `ctx.schedule` is not buffered: a timer lives at
84
+ the provider, not in the log, so the call awaits provider acceptance
85
+ right where it happens. A handler that schedules and then refuses has
86
+ still armed the timer. Guarded delivery makes that safe, but order
87
+ handlers accordingly: guards and refusals first, `ctx.schedule` last.
100
88
  :::
101
89
 
102
90
  ## From outside a handler
103
91
 
104
- The typed surface lives on handler contexts. To schedule from ordinary
105
- server code, use the instance's underlying session, which takes an
106
- explicit name, timing, and event:
107
-
108
92
  ```ts
109
93
  // anywhere on the server:
110
94
  import { escrow } from '@/escrow'
@@ -116,5 +100,7 @@ await escrow.actor('offer-42').session.schedule(
116
100
  )
117
101
  ```
118
102
 
119
- Root scheduling semantics apply (`{ at }` for retry-stable targets);
120
- see [Timers and delays](/guides/timers#schedule-from-ordinary-server-code).
103
+ Outside handler contexts, schedule through the instance's session with
104
+ an explicit name, timing, and event. Root scheduling semantics apply
105
+ (`{ at }` for retry-stable targets); see
106
+ [Schedule from ordinary server code](/server/timers#schedule-from-ordinary-server-code).
@@ -1,13 +1,10 @@
1
1
  ---
2
2
  title: Routes, clients, and React
3
- description: "One route serves an instance: GET streams live state, POST answers calls. Authorize per operation, project per mount, and mount it in React with one hook."
3
+ description: "One route serves an instance: GET streams live state, POST answers calls. Authorize per operation, project per mount, and call it from any client or one React hook."
4
4
  ---
5
5
 
6
6
  ## The actor for this page
7
7
 
8
- A vault with one internal field, so the projection section below has
9
- something to hide:
10
-
11
8
  ```ts vault.ts
12
9
  import { NonRetriableError } from 'experimental-a2'
13
10
  import { actor } from 'experimental-a2/actor'
@@ -47,9 +44,6 @@ export const vault = actor<Vault>({
47
44
 
48
45
  ## The route
49
46
 
50
- `handle.fetch` serves one instance over HTTP. Your route authenticates
51
- and picks the instance; the library serves the rest.
52
-
53
47
  ```ts app/vault/[vaultId]/route.ts
54
48
  import { vault } from '@/vault'
55
49
 
@@ -66,47 +60,44 @@ export const GET = respond
66
60
  export const POST = respond
67
61
  ```
68
62
 
69
- `GET` is the live stream: a server-sent events response of the
70
- instance's state commits, resumed after the `index` query parameter,
71
- with heartbeats. The stream carries state and nothing else: subscribers
72
- see what the actor is, never other callers' inputs, and a refusal
73
- answers only its caller. The full mailbox stays server-side.
63
+ `handle.fetch` serves one instance; your route authenticates and picks
64
+ which:
74
65
 
75
- `POST` is the call lane. The body names a declared event:
66
+ - **`GET`** streams state commits as server-sent events, resumed after
67
+ the `index` query parameter, with heartbeats. It carries state and
68
+ nothing else: subscribers never see other callers' inputs, and a
69
+ refusal answers only its caller.
70
+ - **`GET ?snapshot=1`** returns the current `{ state, index }`.
71
+ - **`POST`** makes a call and responds with its answer.
72
+
73
+ A `POST` body names a declared event:
76
74
 
77
75
  ```json
78
76
  { "event": "deposit", "input": { "amount": 100 }, "messageId": "m-1" }
79
77
  ```
80
78
 
81
- The response is the answer: `{ state, index }` on success, `409` with
82
- the refusal's message when the handler says no, `400` for an undeclared
83
- event. `messageId` is optional; send one to make retrying the request
84
- idempotent.
85
-
86
- `GET ?snapshot=1` reads the current `{ state, index }` without opening a
87
- stream. It passes through `authorize` as `{ type: 'state', id }` and uses
88
- the same `view` projection as streams and call answers. Snapshot and call
89
- responses use `Cache-Control: private, no-store`.
90
-
91
- After `handle.session.clear()`, snapshots, call answers, and the state
92
- commits in the stream include `clearIndex`. Keep it when you pass a snapshot to `useActor`'s `initial`
93
- prop; the hook also picks it up from calls and refreshes. The stream sends the
94
- actor's initial state through `view` as a reset, and the hook drops older
95
- history. A delayed reset never replaces a newer snapshot.
96
-
97
- A call cleared before it receives its answer gets `410` with
98
- `{ code: 'SESSION_CLEARED', error }`. The handler may already have run, so
99
- treat its external effects as unknown, as after a timeout. The actor client
100
- raises it as an `ActorRequestError` with status `410` and the A2 error as
101
- its `cause`. Refusals keep `409`. A later message that reuses the id can't
79
+ It answers `{ state, index }`, `409` with the refusal's message, or
80
+ `400` for an undeclared event. The optional `messageId` makes retrying
81
+ the request idempotent. Snapshot and call responses use
82
+ `Cache-Control: private, no-store`.
83
+
84
+ ### After a clear
85
+
86
+ After `handle.session.clear()`, snapshots, call answers, and stream
87
+ commits include `clearIndex`; keep it in any snapshot you pass to
88
+ `useActor`'s `initial`. The stream then sends the actor's initial state
89
+ (through `view`) as a reset, and the hook drops older history. A
90
+ delayed reset never replaces a newer snapshot.
91
+
92
+ A call cleared before its answer gets `410` with
93
+ `{ code: 'SESSION_CLEARED', error }`, which the actor client raises as
94
+ an `ActorRequestError` with status `410` and the A2 error as its
95
+ `cause`. The handler may already have run, so treat its effects as
96
+ unknown, as after a timeout. A later message reusing the id can't
102
97
  answer the cleared call.
103
98
 
104
99
  ## Authorize
105
100
 
106
- `authorize` is per-operation policy, checked before any read, write, or
107
- subscription. Authentication (who is asking) stays in your route;
108
- `authorize` decides whether this operation is allowed.
109
-
110
101
  ```ts
111
102
  // app/vault/[vaultId]/route.ts, now with per-operation policy:
112
103
  import { vault } from '@/vault'
@@ -123,19 +114,17 @@ export async function POST(
123
114
  }
124
115
  ```
125
116
 
126
- The operation is a discriminated union: `{ type: 'state', id }` for
127
- snapshot reads, `{ type: 'stream', id, startAfter }` for subscriptions,
128
- `{ type: 'call', id, event, input,
129
- messageId? }` for calls. Returning `false` answers `403`.
117
+ Authentication stays in your route; `authorize` decides whether each
118
+ operation is allowed, before any read, write, or subscription.
119
+ Returning `false` answers `403`. The operation is one of:
130
120
 
131
- ## Views: what each mount shows
121
+ - `{ type: 'state', id }` for snapshot reads
122
+ - `{ type: 'stream', id, startAfter }` for subscriptions
123
+ - `{ type: 'call', id, event, input, messageId? }` for calls
124
+ - `{ type: 'presence', id, participant, values }` for
125
+ [presence](#presence) announcements
132
126
 
133
- State can be mixed-audience: internal bookkeeping next to public
134
- fields. The definition stays audience-blind; audiences are a route
135
- concern, like auth. A `view` is a request-time projection applied to
136
- everything that mount serves: snapshots, every state frame on the stream,
137
- and every call answer (they must agree, or the call lane would leak
138
- what the stream hides).
127
+ ## Views: what each mount shows
139
128
 
140
129
  ```ts
141
130
  // app/vault/[vaultId]/public/route.ts, a projected mount of the same actor:
@@ -152,21 +141,23 @@ export async function GET(
152
141
  }
153
142
  ```
154
143
 
155
- Different routes project the same actor differently: an admin mount
156
- with no view sees everything, the public mount above hides `audit`.
157
- Per-viewer views need no extra machinery, because the route already
158
- holds the identity; a closure does it
159
- (`view: (state) => ({ ...visible, mine: state.perUser[user.id] })`).
160
- View output holds to the same plain-JSON floor as state itself.
144
+ A `view` projects everything its mount serves: snapshots, stream
145
+ frames, and call answers alike, so calls never leak what the stream
146
+ hides. Audiences are a route concern, like auth: an admin mount with no
147
+ view sees everything, and the mount above hides `audit`. For a
148
+ per-viewer view, close over your route's identity:
149
+ `view: (state) => ({ ...visible, mine: state.perUser[user.id] })`. View
150
+ output must be plain JSON, like state.
161
151
 
162
- Views are for internal-but-not-secret fields. Secrets do not belong in
163
- state at all: state commits are log rows, and the log is forever. Keep
164
- secrets outside, pass references, resolve them inside handlers.
152
+ Clients of a projected mount pass its shape as the second type argument
153
+ (`createActorClient<typeof vault, PublicVault>` or
154
+ `useActor<typeof vault, PublicVault>`).
165
155
 
166
- ## Any client
156
+ Views hide internal fields, not secrets. State commits are log rows,
157
+ and the log is forever: keep secrets outside, pass references, and
158
+ resolve them inside handlers.
167
159
 
168
- `createActorClient` is the typed call surface over that route's `POST`
169
- lane. It works anywhere: browsers, other frameworks, other servers.
160
+ ## Any client
170
161
 
171
162
  ```ts
172
163
  // in the browser, or any client at all:
@@ -188,26 +179,20 @@ try {
188
179
  }
189
180
  ```
190
181
 
191
- The import of `vault` is type-only, so it is erased at build time: the
192
- server module never enters a client bundle. On a view-projected mount,
193
- pass the projected shape as the second type argument
194
- (`createActorClient<typeof vault, PublicVault>`) so answers carry it.
195
-
196
- `client.state()` reads a fresh snapshot. The optional `onSnapshot`
197
- callback receives successful state reads and call answers before their
198
- promises resolve. It receives each answer, including older results from
199
- idempotent retries; consumers compare indexes before replacing live state.
182
+ `createActorClient` is the typed call surface over the route's `POST`,
183
+ for browsers, other frameworks, and other servers. The `vault` import
184
+ is type-only, so the server module never enters a client bundle. `client.state()` reads a fresh snapshot. The optional
185
+ `onSnapshot` callback receives successful reads and call answers before
186
+ their promises resolve, including older results from idempotent
187
+ retries, so compare indexes before replacing live state.
200
188
 
201
- `ActorRequestError` carries an HTTP `status` when available. Network
202
- failures, timeouts, and server errors do not establish whether a call
203
- committed. Retry with the same explicit `id` and input to recover its
204
- original answer. A client does not retry calls automatically.
189
+ `ActorRequestError` carries an HTTP `status` when available. A network
190
+ failure, timeout, or server error doesn't establish whether a call
191
+ committed: retry with the same explicit `id` and input to recover its
192
+ original answer. The client never retries calls automatically.
205
193
 
206
194
  ## React
207
195
 
208
- One hook. The server component hands down the first fold, the hook
209
- keeps it live and exposes the same typed calls.
210
-
211
196
  ```tsx app/vault/[vaultId]/page.tsx
212
197
  import { vault } from '@/vault'
213
198
  import { VaultClient } from './vault-client'
@@ -256,56 +241,34 @@ export function VaultClient({
256
241
  }
257
242
  ```
258
243
 
259
- What the hook gives you:
260
-
261
- - **`state`**: the live view. Call answers and stream commits update it;
262
- the fold is library code, so no reducers, schemas, or contracts
263
- appear in browser code.
264
- - **`call`**: the same typed surface as `createActorClient`, because it
265
- is one; `useActor` is a thin React wrapper over the client primitive
266
- and the stream.
267
- - **`index`**: the latest adopted state index, and **`events`**: the
268
- state-commit feed observed through the stream, starting after the initial
269
- snapshot. Call answers and refreshes can advance state before the feed
270
- catches up; delayed commits still enter the feed once.
271
- - **`refresh()`**: reads a fresh snapshot and adopts it before resolving.
272
- Use it after a refusal or when returning to a suspended browser tab.
273
- - **`connection`**: the same discriminated union as
274
- [`useSession`](/guides/react#events-and-history), heartbeat
275
- watchdog included.
276
-
277
- Live, not optimistic. Handlers are server code deciding against
278
- serialized fresh state, so there is nothing correct for the browser to
279
- apply early; a local guess would be wrong exactly when the actor
280
- matters. If a control needs pending UI, `await call.deposit(...)` is a
281
- promise like any other.
282
-
283
- Successful calls update the shared client state before their promises
284
- resolve. Snapshots only advance state: an older call answer or delayed
285
- stream frame never replaces a newer state. React renders that update on
286
- its normal schedule. A call still returns its own answer, even if a newer
287
- state has already arrived. Refusals do not change state or trigger an
288
- automatic refresh.
289
-
290
- The stream tracks its own position for deduplication and reconnects.
291
- Adopting a newer snapshot does not skip stream events: delayed commits
292
- enter `events` without replacing newer state. Events from before the
293
- initial snapshot are not loaded into this feed.
294
-
295
- On a view-projected mount, pass the projected shape as the second type
296
- argument (`useActor<typeof vault, PublicVault>`); `initial`, `state`,
297
- and the call answers all carry it.
244
+ The server component hands down the first snapshot; `useActor` keeps it
245
+ live and returns:
246
+
247
+ - **`state`**: the live view (no reducers, schemas, or contracts in
248
+ browser code).
249
+ - **`call`**: the same typed calls as `createActorClient`, which the
250
+ hook wraps.
251
+ - **`index`**: the latest adopted state index.
252
+ - **`events`**: the state commits the stream observed after the initial
253
+ snapshot. A delayed commit still enters once, even when a call answer
254
+ or refresh already advanced `state`.
255
+ - **`refresh()`**: reads and adopts a fresh snapshot before resolving.
256
+ Use it after a refusal or when a suspended tab returns.
257
+ - **`connection`**: the same union as
258
+ [`useSession`](/client/react#events-and-history), heartbeat watchdog
259
+ included.
260
+
261
+ State is live, not optimistic: handlers decide on the server against
262
+ fresh state. For pending UI, await the call.
263
+
264
+ A successful call updates `state` before its promise resolves (React
265
+ renders on its normal schedule) and returns its own answer, even if
266
+ newer state already arrived. State only advances: an older answer or
267
+ delayed stream frame never replaces newer state. Refusals don't change
268
+ state or trigger a refresh.
298
269
 
299
270
  ## Presence
300
271
 
301
- Who is here, live: cursors, names, viewer counts. Presence is
302
- ephemeral audience state. It replicates to the instance's subscribers
303
- and expires; it never enters the log, and handlers cannot see it (an
304
- actor decides from its log, never from who is watching).
305
-
306
- Declare the vocabulary in the protocol as types, and arm the wire with
307
- `presence: true`:
308
-
309
272
  ```ts board.ts
310
273
  import { actor } from 'experimental-a2/actor'
311
274
 
@@ -327,10 +290,12 @@ export const board = actor<Board>({
327
290
  })
328
291
  ```
329
292
 
330
- The route does not change: the same `GET` stream interleaves presence
331
- updates between state commits, and the same `POST` lane accepts
332
- presence announcements. In React, pass a `participant` identity and
333
- use the map:
293
+ Declare [presence](/client/presence) fields as types in the protocol
294
+ and arm the wire with `presence: true`. Presence replicates to the
295
+ instance's subscribers and expires; it never enters the log, and
296
+ handlers can't see it. The route doesn't change: `GET` interleaves
297
+ presence with state commits, and `POST` accepts announcements. In
298
+ React, pass a `participant` identity and read the map:
334
299
 
335
300
  ```tsx app/board/[boardId]/board-client.tsx
336
301
  'use client'
@@ -366,32 +331,27 @@ export function BoardClient({
366
331
  }
367
332
  ```
368
333
 
369
- `setPresence` is fire-and-forget: throttled on the way out, resent
370
- after a reconnect, `null` clears a field. The map holds each
371
- participant's latest fields with their stamps; how long a value stays
372
- painted is render logic, decided against `at`.
334
+ `setPresence` is fire-and-forget: throttled, resent after a reconnect,
335
+ and `null` clears a field. The map holds each participant's latest
336
+ fields with their stamps; decide how long to paint a value from `at`.
373
337
 
374
- Three guards stand where presence enters, and none of them is a
375
- schema:
338
+ Three guards, none of them a schema:
376
339
 
377
340
  - **Types**: the protocol types `setPresence` and the map, so honest
378
- clients cannot send the wrong shape.
379
- - **The floor**: the library rejects values that are not plain JSON
380
- trees, values over 8 KB, and object-plumbing field names.
381
- - **`authorize`**: every announcement arrives as
382
- `{ type: 'presence', id, participant, values }` before it is
383
- accepted. Clamp, verify the participant matches the authenticated
384
- user, or rate-limit here.
341
+ clients can't send the wrong shape.
342
+ - **The floor**: A2 rejects values that aren't plain JSON trees, values
343
+ over 8 KB, and object-plumbing field names.
344
+ - **[`authorize`](#authorize)**: every announcement arrives as a
345
+ `presence` operation before it is accepted. Clamp, verify the
346
+ participant matches the authenticated user, or rate-limit here.
385
347
 
386
- What no guard does is make peer values trustworthy. Presence is
387
- authored by whoever holds a connection: render it like user input.
348
+ No guard makes peer values trustworthy: presence is authored by whoever
349
+ holds a connection, so render it like user input.
388
350
 
389
351
  ## The session wire underneath
390
352
 
391
- `handle.fetch` serves the actor's two lanes and nothing else. When you
392
- want the raw session protocol instead (the full event feed, pushes,
393
- presence, the `ws` transport), the definition exposes the underlying
394
- server: mount `def.server.fetch` and use the core
395
- [client](/guides/react) against it. That wire shows the whole mailbox
396
- to its subscribers, inputs included, so reserve it for trusted
397
- audiences.
353
+ For the raw session protocol (the full event feed, pushes, presence,
354
+ the `ws` transport), mount `def.server.fetch` as a
355
+ [session route](/server/routes) and use the core
356
+ [client](/client/react). That wire shows the whole mailbox, inputs
357
+ included, so reserve it for trusted audiences.
@@ -1 +1 @@
1
- export default { order: 4 }
1
+ export default { order: 6 }