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
@@ -1,13 +1,12 @@
1
1
  ---
2
2
  title: Quickstart
3
- description: Define one lifecycle, implement its reactions, and make your first durable append in about five minutes.
3
+ description: Define a contract, react to its events, and make your first durable append in five minutes.
4
4
  ---
5
5
 
6
6
  ## What you'll build
7
7
 
8
- The smallest useful lifecycle: an order that notifies a shop when it arrives.
9
- One contract, one session, one handler, two routes, and at the end a log you
10
- can read back.
8
+ An order that notifies a shop when it arrives: one contract, one handler, two
9
+ routes, and a log you can read back.
11
10
 
12
11
  ## 1. Install
13
12
 
@@ -17,10 +16,7 @@ npm i experimental-a2 zod
17
16
 
18
17
  ## 2. Define a contract
19
18
 
20
- A contract is a name plus the events it understands: each key an event
21
- name, each value a schema. Zod here, though any
22
- [Standard Schema](https://standardschema.dev) validator works, and
23
- that's the contract's complete declaration.
19
+ A contract is a name plus the events it understands, each with a schema:
24
20
 
25
21
  ```ts server/orders.ts
26
22
  import { z } from 'zod'
@@ -35,21 +31,10 @@ export const orders = a2.contract({
35
31
  })
36
32
  ```
37
33
 
38
- The contract is a plain, importable, isomorphic value. The server
39
- implements it below, the reducer derives from it in step 6, and when a
40
- browser enters the picture it types the client too.
41
-
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 store.
44
- Production wants an explicit backend; see
45
- [Going to production](/guides/production).
46
-
47
34
  ## 3. Implement it
48
35
 
49
- Handlers run where the contract is served. `createServer` binds the
50
- vocabulary to storage and reactions; each handler gets the triggering
51
- event and can append what happens next. Same file, below the contract
52
- (they can split later, when a browser needs the contract alone):
36
+ `createServer` binds the contract to storage and handlers. Same file, below
37
+ the contract:
53
38
 
54
39
  ```ts server/orders.ts
55
40
  import { createServer } from 'experimental-a2/server'
@@ -66,24 +51,19 @@ export const ordersServer = createServer({
66
51
  })
67
52
  ```
68
53
 
69
- All handlers are declared at construction: one table of reactions, so a
70
- handler can never be silently missing because the module that registered
71
- it wasn't imported.
72
-
73
- Two things worth noticing:
54
+ - Handlers can run more than once (at-least-once delivery), so make external
55
+ calls safe to repeat. `event.id` is stable across re-runs: use it as the
56
+ idempotency key.
57
+ - The returned event commits atomically with handler success. If the handler
58
+ throws or the process dies first, `shop.notified` doesn't appear. Return one
59
+ event or an array.
74
60
 
75
- - The side effect takes an idempotency key. Handlers can run more than
76
- once (that's what at-least-once delivery means), so external calls
77
- should be safe to repeat. `event.id` is stable across re-runs. Use it.
78
- - The returned event is committed atomically with successful handler
79
- completion. If the handler throws or the process dies first,
80
- `shop.notified` does not appear. A handler can return one event or an array.
61
+ Development needs no database setup: events go to SQLite at `.a2/dev.db` and
62
+ survive restarts. Tests get an in-memory store. Production needs an explicit
63
+ store; see [Production](/server/production).
81
64
 
82
65
  ## 4. Append the first event
83
66
 
84
- `append` is the entire public surface for moving a session forward. Call it
85
- from a route:
86
-
87
67
  ```ts app/api/orders/route.ts
88
68
  import { ordersServer } from '@/server/orders'
89
69
 
@@ -99,11 +79,8 @@ export async function POST(req: Request) {
99
79
  }
100
80
  ```
101
81
 
102
- A session is one instance of the contract. `ordersServer.session('order-42')`
103
- is order 42's lifecycle: the unit A2 recovers, folds, and streams.
104
- Creating the handle does no I/O, and the id is yours to choose.
105
-
106
- Try it:
82
+ `session(orderId)` is one [session](/contract/sessions), one instance of the
83
+ contract. The id is yours, and creating the handle does no I/O.
107
84
 
108
85
  ```bash
109
86
  curl -X POST localhost:3000/api/orders \
@@ -111,18 +88,15 @@ curl -X POST localhost:3000/api/orders \
111
88
  -d '{ "orderId": "order-42", "shopId": "shop-7", "items": ["flat white"] }'
112
89
  ```
113
90
 
114
- The payload is validated against the schema before anything is written;
115
- send garbage and you get `INVALID_PAYLOAD` back, with the issues.
91
+ The payload is validated before anything is written; a bad one fails with
92
+ `INVALID_PAYLOAD` and the issues.
116
93
 
117
- When the append returns, the event is durably in the log and A2 has started the
94
+ When `append` returns, the event is durably in the log and A2 has started the
118
95
  `created` handler inline. In production, a queue-backed scheduler wakes pending
119
- work after a failure or process death. See [Durability](/concepts/durability)
120
- for the exact contract.
96
+ work after a failure or process death. See [Guarantees](/server/guarantees).
121
97
 
122
98
  ## 5. Read the log back
123
99
 
124
- Add a route that returns the session's history:
125
-
126
100
  ```ts app/api/orders/[orderId]/route.ts
127
101
  import { ordersServer } from '@/server/orders'
128
102
 
@@ -147,41 +121,35 @@ curl localhost:3000/api/orders/order-42
147
121
  ]
148
122
  ```
149
123
 
150
- (Trimmed for brevity; each event also carries `id`, `sessionId`, and
151
- `createdAt`.)
152
-
153
- That second event is the payoff. You appended one event; the handler
154
- returned the next; the log shows the whole story, in order. This is what
155
- debugging looks like in A2: you read.
124
+ Each event also carries `id`, `sessionId`, and `createdAt` (trimmed here). You
125
+ appended one event and the handler returned the second. Debugging in A2 is
126
+ reading the log.
156
127
 
157
128
  ## 6. Fold state out of the log
158
129
 
159
- Raw session history is truth, but UIs usually want a view. Define a reducer
160
- once:
130
+ UIs usually want a view, not raw history. Define a reducer once:
161
131
 
162
132
  ```ts reducer.ts
163
133
  import { reducer } from 'experimental-a2'
164
134
  import { orders } from '@/server/orders'
165
135
 
166
- export const ordersReducer = reducer({ contract: orders, name: 'order-status', initialState: { status: 'new' } })
167
- .fold((state, event) => {
168
- switch (event.type) {
169
- case 'created':
170
- return { ...state, status: 'created' }
171
- case 'shop.notified':
172
- return { ...state, status: 'notified' }
173
- default:
174
- return state
175
- }
176
- })
136
+ export const ordersReducer = reducer({
137
+ contract: orders,
138
+ name: 'order-status',
139
+ initialState: { status: 'new' },
140
+ }).fold((state, event) => {
141
+ switch (event.type) {
142
+ case 'created':
143
+ return { ...state, status: 'created' }
144
+ case 'shop.notified':
145
+ return { ...state, status: 'notified' }
146
+ default:
147
+ return state
148
+ }
149
+ })
177
150
  ```
178
151
 
179
- A reducer is *derived from* the contract, in two steps by design: the
180
- first call fixes the state and event types, so `.fold()` receives fully
181
- concrete ones. No type arguments, no annotations, and literal unions in
182
- your state survive the fold.
183
-
184
- And fold anywhere you need the current picture:
152
+ Then fold wherever you need the current picture:
185
153
 
186
154
  ```ts
187
155
  // anywhere on the server:
@@ -192,22 +160,20 @@ const { state } = await ordersServer.session('order-42').state(ordersReducer)
192
160
  // state → { status: 'notified' }
193
161
  ```
194
162
 
195
- The session's state is not a second record to update. It is a fold over the
196
- log. More in [Reading state](/concepts/state).
163
+ State isn't a second record to update. It's a fold over the log; see
164
+ [State](/contract/state).
197
165
 
198
166
  ## That's it
199
167
 
200
- You have a durable lifecycle, implemented: a validated write path, a handler,
201
- a log you can read, and a view folded from it.
202
- Everything else in these docs is the same two moves, append an event and
203
- react to it, applied to timers, cancellation, live UIs, and production
204
- infrastructure.
168
+ You have a validated write path, a handler, a log you can read, and a view
169
+ folded from it. The rest of these docs apply the same two moves, append an
170
+ event and react to it, to timers, cancellation, live UIs, and production.
205
171
 
206
172
  <CardGroup cols={2}>
207
- <Card title="Handlers" href="/concepts/handlers" icon="file">
208
- The context object, chaining, ordering, and re-runs.
173
+ <Card title="Handlers" href="/server/handlers" icon="file">
174
+ Context, chaining, and re-runs.
209
175
  </Card>
210
- <Card title="Going to production" href="/guides/production" icon="rocket">
211
- Postgres, a queue-backed scheduler, and dead-letter resolution.
176
+ <Card title="Production" href="/server/production" icon="rocket">
177
+ A store, a scheduler, dead letters.
212
178
  </Card>
213
179
  </CardGroup>
@@ -1,19 +1,10 @@
1
1
  ---
2
2
  title: Complete examples
3
- description: Read or copy the complete Next.js playground that ships inside the experimental-a2 package.
3
+ description: Every experimental-a2 install includes a complete Next.js playground to copy, run, and read.
4
4
  ---
5
5
 
6
6
  ## Shipped with the package
7
7
 
8
- Every `experimental-a2` installation includes a complete Next.js playground at
9
- `node_modules/experimental-a2/examples/playground`.
10
-
11
- The playground is not a collection of isolated snippets. Each feature owns its
12
- contract, reducer, server, route handlers, client session, and scheduler when it
13
- needs one. It uses only public package entry points.
14
-
15
- Copy it into a new directory to run or modify it:
16
-
17
8
  ```sh
18
9
  cp -R node_modules/experimental-a2/examples/playground ./a2-playground
19
10
  cd a2-playground
@@ -21,12 +12,13 @@ pnpm install
21
12
  pnpm dev
22
13
  ```
23
14
 
24
- The copied `package.json` is standalone. Its A2 dependency is pinned to the
25
- version that supplied the example.
15
+ The playground lives at `node_modules/experimental-a2/examples/playground`.
16
+ Each feature owns its contract, reducer, server, route handlers, client
17
+ session, and scheduler when it needs one, using only public entry points.
26
18
 
27
- The snapshot also includes an `AGENTS.md` file. Coding agents that support
28
- repository instructions discover the example's structure as soon as they work
29
- inside the copied directory.
19
+ The copy is standalone: its `package.json` pins A2 to the version that
20
+ shipped it, and its `AGENTS.md` describes the structure to coding agents that
21
+ work inside the directory.
30
22
 
31
23
  ## Find the right example
32
24
 
@@ -49,8 +41,6 @@ When A2 is installed locally, read these package directories in this order:
49
41
  3. `node_modules/experimental-a2/src` only when implementation details are
50
42
  relevant.
51
43
 
52
- Do not infer application patterns from `dist`. The playground is the reference
53
- for how the public pieces fit together in a real Next.js application.
54
-
55
- The npm snapshot is generated from `apps/playground` in the A2 repository when
56
- the package is published, so the runnable app remains the canonical source.
44
+ Don't infer application patterns from `dist`. The playground, generated from
45
+ `apps/playground` in the A2 repository at publish time, is the reference for
46
+ how the public pieces fit together.
@@ -1,28 +1,22 @@
1
1
  ---
2
2
  title: Introduction
3
- description: "A durable object built on the log: one named instance, typed state, and handlers that process one message at a time."
3
+ description: "Typed state behind a mailbox. Each instance processes one message at a time, and each handler ends in one atomic commit."
4
4
  ---
5
5
 
6
6
  ## The model
7
7
 
8
- An actor is state behind a mailbox. You address an instance by id, send
9
- it typed events, and its handlers process them one at a time: read
10
- state, decide, mutate, return. Each handler ends in one atomic commit.
11
- Between messages the instance is rows in your store, not a process:
12
- nothing to keep alive, no locks to take, no lost updates to debug.
8
+ An actor is state behind a mailbox: you send an instance typed events,
9
+ and its handlers process them one at a time. Between messages an
10
+ instance is rows in your store, not a process. Every instance is an
11
+ ordinary A2 session, so durability, retries, history, and devtools hold
12
+ for actors too.
13
13
 
14
- Every instance is an ordinary A2 session underneath, so everything the
15
- log gives a session (durability, retries, history, devtools) holds for
16
- actors too. The module is three entry points: `experimental-a2/actor`
17
- on the server, `experimental-a2/actor/client` anywhere, and
14
+ Entry points: `experimental-a2/actor` on the server,
15
+ `experimental-a2/actor/client` anywhere, and
18
16
  `experimental-a2/actor/react` for the hook.
19
17
 
20
18
  ## Define one
21
19
 
22
- A protocol declares the state shape and the events with their inputs.
23
- Types, not schemas: handlers are trusted server code, and the compiler
24
- holds them to the protocol. Pass it explicitly.
25
-
26
20
  ```ts vault.ts
27
21
  import { NonRetriableError } from 'experimental-a2'
28
22
  import { actor } from 'experimental-a2/actor'
@@ -52,28 +46,22 @@ export const vault = actor<Vault>({
52
46
  })
53
47
  ```
54
48
 
55
- The `handlers` record must cover every declared event: a missing
56
- handler, an extra one, or a wrong input shape fails to compile. Inputs
57
- arrive typed; `ctx.state` is a mutable draft of the instance's state,
58
- committed atomically when the handler returns. `name` is identity (it
59
- prefixes storage keys), and `state` seeds new instances.
49
+ Pass the protocol explicitly as the type argument. It declares types,
50
+ not schemas: handlers are trusted server code, and a missing handler,
51
+ an extra one, or a wrong input shape fails to compile. `ctx.state` is a
52
+ mutable draft, committed atomically when the handler returns. `name` is
53
+ identity (it prefixes storage keys), and `state` seeds new instances.
60
54
 
61
- Types are compile-time claims. When an event can arrive from the wire,
62
- guard its input in the handler's first lines and throw
63
- `NonRetriableError` on bad shapes (bring a schema library and `.parse`
64
- if you prefer; it is userland either way).
55
+ When an event can arrive from the wire, guard its input in the
56
+ handler's first lines and throw `NonRetriableError` on bad shapes.
65
57
 
66
- `actor` also accepts the `store`, `scheduler`, and `telemetry` options
67
- `createServer` takes. Without a store it uses the same development
68
- default; see [Going to production](/guides/production). A protocol can
69
- declare one more vocabulary, `presence`, for live audience state; see
70
- [Presence](/actors/routes#presence).
58
+ `actor` also accepts `createServer`'s `store`, `scheduler`, and
59
+ `telemetry` options, with the same development default store; see
60
+ [Production](/server/production). A protocol can also declare
61
+ [`presence`](/actors/routes#presence).
71
62
 
72
63
  ## Call it
73
64
 
74
- `call` sends the event and awaits its answer: the state after the
75
- handler ran, and the log index of that commit.
76
-
77
65
  ```ts
78
66
  // anywhere on the server:
79
67
  import { vault } from '@/vault'
@@ -84,18 +72,14 @@ const { state, index } = await savings.call.deposit({ amount: 100 })
84
72
  // state reflects this deposit, and every message before it
85
73
  ```
86
74
 
87
- `vault.actor(id)` is a handle, not a resource; nothing exists until the
88
- first message. Concurrent calls from any number of serverless
75
+ `call` awaits the answer: the state after the handler ran and the log
76
+ index of that commit. `vault.actor(id)` is a handle; nothing exists
77
+ until the first message. Concurrent calls from any number of
89
78
  invocations serialize on the instance's lane, and each answer reflects
90
- its own message. That serialization is the feature:
91
- [Concurrency](/actors/concurrency) is the full story.
79
+ its own message; see [Concurrency](/actors/concurrency).
92
80
 
93
81
  ### Refusals
94
82
 
95
- A handler says no by throwing `NonRetriableError`. That refusal is an
96
- answer, not a failure: nothing commits, nothing retries, and the caller
97
- gets the message.
98
-
99
83
  ```ts
100
84
  // anywhere on the server:
101
85
  import { ActorRefusedError } from 'experimental-a2/actor'
@@ -110,28 +94,23 @@ try {
110
94
  }
111
95
  ```
112
96
 
113
- Three kinds of failure, three behaviors:
97
+ Throwing `NonRetriableError` refuses the message. A refusal is an
98
+ answer, and it is all or nothing: no state change, no buffered sends,
99
+ no retry. The message settles, and the caller receives
100
+ `ActorRefusedError`.
114
101
 
115
- - **`NonRetriableError`** is a refusal, and a refusal is all or
116
- nothing: no state change, no buffered sends. The message is settled,
117
- and the caller receives `ActorRefusedError`.
118
- - **Any other throw** is an infrastructure failure. A2 retries the
119
- message with the usual budget and backoff; the instance's lane waits,
120
- which is deliberate (correctness over liveness). See
102
+ - **Any other throw** retries the message with the usual budget and
103
+ backoff while the instance's lane waits; see
121
104
  [Errors](/reference/errors).
122
105
  - **A crash** re-runs the handler from the pre-message state. `ctx.id`
123
106
  is the message id, stable across re-runs: use it as the idempotency
124
107
  key for external I/O.
125
-
126
- A call cleared by `handle.session.clear()` before it receives its answer
127
- rejects with an `A2Error` whose code is `SESSION_CLEARED`. The handler may
128
- already have run. Over HTTP it's a `410`; see [Routes](/actors/routes).
108
+ - **A clear** by `handle.session.clear()` before the answer rejects the
109
+ call with an `A2Error` whose code is `SESSION_CLEARED`. The handler
110
+ may already have run. Over HTTP it's a `410`.
129
111
 
130
112
  ## Send, don't wait
131
113
 
132
- `send` validates, appends the event durably, and returns without
133
- waiting for the handler.
134
-
135
114
  ```ts
136
115
  // anywhere on the server:
137
116
  import { vault } from '@/vault'
@@ -140,9 +119,9 @@ const { id } = await vault.actor('savings').send.deposit({ amount: 25 })
140
119
  // durably queued; the handler runs on the instance's lane
141
120
  ```
142
121
 
143
- Same mailbox, same ordering; the only difference is that nobody waits
144
- for the answer. Pass `{ id }` in the options to make a retried send
145
- idempotent.
122
+ `send` validates and appends the event durably without waiting for the
123
+ handler: same mailbox, same ordering. Pass `{ id }` in the options to
124
+ make a retried send idempotent.
146
125
 
147
126
  ## Read without queueing
148
127
 
@@ -153,41 +132,24 @@ import { vault } from '@/vault'
153
132
  const { state } = await vault.actor('savings').state()
154
133
  ```
155
134
 
156
- A snapshot read. It never queues behind pending messages, and a new
135
+ A snapshot read: it never queues behind pending messages, and a new
157
136
  instance reads as its seed state.
158
137
 
159
138
  ## State is remembered, not recomputed
160
139
 
161
- Every commit appends a state event to the instance's log, and reads
162
- fold those by last write wins. History is never reinterpreted: you can
163
- rewrite a handler freely without changing what any past commit means.
164
- (This is the opposite trade from [reducers](/concepts/state), which
165
- recompute views from events; actor state is the recorded decision
166
- itself.)
167
-
168
- Because state lives in the log, it holds to the same floor as any
169
- payload: plain JSON trees. A `Date`, a `Map`, or `NaN` fails the commit
170
- loudly instead of being silently coerced.
140
+ Every commit appends a state event, and reads fold them by last write
141
+ wins. Unlike [reducers](/contract/state), nothing is recomputed, so
142
+ rewriting a handler never changes what a past commit means. State must
143
+ be plain JSON: a `Date`, a `Map`, or `NaN` fails the commit loudly.
171
144
 
172
- Two consequences worth designing for:
173
-
174
- - **Old instances meet new code.** After a deploy, state written by the
175
- previous version flows into the new handlers. Default new fields
176
- defensively (`ctx.state.pending ??= {}` as a first line) instead of
177
- assuming the latest shape.
178
- - **The seed applies to new instances only.** Changing `state` in the
179
- definition does not migrate existing instances; they keep their
180
- remembered state.
145
+ - **Old instances meet new code.** After a deploy, default new fields
146
+ defensively (`ctx.state.pending ??= {}` as a first line).
147
+ - **The seed applies to new instances only.** Changing `state` does not
148
+ migrate existing instances.
181
149
 
182
150
  ## The log is still there
183
151
 
184
- An actor is a layer over an ordinary A2 session, not a wall around
185
- one. `handle.session` is that session (history, streams, scheduling),
186
- the definition
187
- exposes its assembled `contract`, `server`, and `reducer`, and the full
188
- mailbox is visible in devtools. Event names starting with `a2.` are
189
- reserved for the actor's own bookkeeping.
190
-
191
- Next: [Concurrency](/actors/concurrency) for the execution
192
- model, [Deadlines and timers](/actors/timers) for scheduled events, and
193
- [Routes, clients, and React](/actors/routes) for the wire.
152
+ `handle.session` is the instance's underlying session (history,
153
+ streams, scheduling). The definition exposes its assembled `contract`,
154
+ `server`, and `reducer`, and devtools show the full mailbox. Event
155
+ names starting with `a2.` are reserved for the actor's own bookkeeping.
@@ -1,50 +1,35 @@
1
1
  ---
2
2
  title: Concurrency
3
- description: "Each instance processes one message at a time. Slow work goes in concurrent handlers: they run in parallel and report back by sending events."
3
+ description: "Each instance processes one message at a time. Slow work goes in concurrent handlers, which run in parallel and report back by sending events."
4
4
  ---
5
5
 
6
6
  ## One message at a time
7
7
 
8
- Every message an instance receives runs on its
9
- [lane](/concepts/handlers#lanes): a serial queue, one handler at a
10
- time, in arrival order. This is what makes handlers easy to trust. A
11
- handler always sees the state every previous message produced, its
12
- checks cannot be raced by another message, and its changes commit
13
- before the next message runs.
8
+ Every message runs on the instance's [lane](/server/handlers#lanes):
9
+ one handler at a time, in arrival order. A handler sees the state every
10
+ previous message produced, no other message can race its checks, and
11
+ its changes commit before the next message runs.
14
12
 
15
- The lane is per instance. A thousand vaults process in parallel; each
16
- vault processes in order. If a single instance receives more traffic
17
- than one queue can absorb, split the identity and give the work more
18
- instances (per user, per order, per document) instead of trying to
19
- make one instance concurrent.
20
-
21
- ## The problem with slow work
22
-
23
- A serial handler holds the lane while it runs. If `withdraw` awaits a
24
- two-second bank API, every deposit waits behind it. The instance stays
25
- correct, but it stops responding.
26
-
27
- The fix is not to weaken the lane. It is to split the work: serial
28
- handlers make the decisions, concurrent handlers do the slow parts,
29
- and the results come back as events.
13
+ Lanes are per instance: a thousand vaults process in parallel, each in
14
+ order. If one instance gets more traffic than one queue can absorb,
15
+ split the identity (per user, per order, per document) instead of
16
+ making one instance concurrent.
30
17
 
31
18
  ## Concurrent handlers
32
19
 
33
- Wrap a handler in `concurrent(...)` and it leaves the lane: it runs in
34
- parallel with serial handlers and with other concurrent handlers. Two
35
- things change inside it:
20
+ A serial handler holds the lane while it runs: if `withdraw` awaits a
21
+ two-second bank API, every deposit waits behind it. Keep decisions
22
+ serial and move the slow part into a handler wrapped in
23
+ `concurrent(...)`. It leaves the lane and runs in parallel with serial
24
+ handlers and other concurrent ones. Inside it:
36
25
 
37
- - There is no `ctx.state` draft to mutate. Reading state becomes a
38
- function call, `ctx.state()`, and the snapshot it returns can go
39
- stale while the handler keeps running.
40
- - State changes happen by sending events. Their serial handlers run on
41
- the lane and decide against current state.
26
+ - `ctx.state()` returns a snapshot, which can go stale while the
27
+ handler runs. There is no draft to mutate.
28
+ - State changes happen by sending events, whose serial handlers decide
29
+ against current state.
42
30
 
43
31
  ## Reserve, transfer, settle
44
32
 
45
- A withdrawal that talks to a slow bank, without ever blocking the
46
- vault: reserve the money, run the transfer, then settle or refund.
47
-
48
33
  ```ts vault.ts
49
34
  import { NonRetriableError } from 'experimental-a2'
50
35
  import { actor, concurrent } from 'experimental-a2/actor'
@@ -109,46 +94,34 @@ export const vault = actor<Vault>({
109
94
  })
110
95
  ```
111
96
 
112
- Step by step:
113
-
114
- 1. **`withdraw` reserves.** It runs on the lane, so the balance check
115
- cannot be raced by another withdrawal. The balance drops, `pending`
116
- records the reserved amount, and the `transfer` event commits
117
- atomically with both. The withdrawal cannot half-start.
118
- 2. **`transfer` does the slow part.** It runs off the lane, so
119
- deposits keep flowing meanwhile. It finishes by sending an event:
120
- `settle` on success, `refund` when the bank says no. It never
121
- changes state itself, and it reports failure by sending, not by
122
- throwing.
123
- 3. **`settle` and `refund` finish the job.** Both check `pending[ref]`
124
- first and return when the entry is gone, so a duplicate or late
125
- message does nothing. The refund amount comes from state, not from
126
- recomputing it.
127
-
128
- A crash at any step re-runs that one message from the state it started
129
- with. Wherever the process dies, the vault lands in a named state:
130
- reserved, settled, or refunded. Money is never half-transferred.
97
+ 1. **`withdraw` reserves** on the lane, so no other withdrawal can race
98
+ the balance check. The balance drop, the `pending` entry, and the
99
+ `transfer` event commit atomically: the withdrawal cannot
100
+ half-start.
101
+ 2. **`transfer` does the slow part** off the lane, so deposits keep
102
+ flowing. It never changes state, and it reports failure by sending
103
+ `refund`, not by throwing.
104
+ 3. **`settle` and `refund` finish.** Both return when `pending[ref]` is
105
+ gone, so a duplicate or late message does nothing. The refund amount
106
+ comes from state.
107
+
108
+ A crash re-runs only that message from the state it started with, so
109
+ the vault always lands reserved, settled, or refunded.
131
110
 
132
111
  ## Buffered sends
133
112
 
134
- `ctx.send` does not append immediately. Everything a handler sends is
135
- buffered and commits when the handler completes: for a serial handler,
136
- in the same commit as its state change; for a concurrent handler, when
137
- it finishes. A refusal discards the buffer along with the state
138
- changes, so a refused message leaves no trace. Re-runs produce the
139
- same message ids, so a retried handler does not append its sends
140
- twice.
113
+ `ctx.send` buffers. A serial handler's sends commit with its state
114
+ change; a concurrent handler's commit when it finishes. A refusal
115
+ discards the buffer with the state changes, so a refused message leaves
116
+ no trace. Re-runs produce the same message ids, so a retried handler
117
+ never appends its sends twice. `ctx.schedule` is
118
+ [not buffered](/actors/timers#scheduling-is-immediate).
141
119
 
142
120
  ## When handlers fail
143
121
 
144
- - A **serial** handler that throws `NonRetriableError` refuses the
145
- message: nothing commits, and the caller gets the answer (see
146
- [Introduction](/actors/introduction#refusals)).
147
- - A **serial** handler that throws anything else is retried on the
148
- lane, and the instance waits. This is deliberate: state never
149
- advances past a failure.
150
- - A **concurrent** handler that throws is retried off the lane;
151
- nothing else waits for it.
152
- - `call` works on serial events only. A concurrent handler produces no
153
- answer to wait for, so calling one is rejected: `send` those events
154
- instead.
122
+ A concurrent handler that throws retries off the lane, and nothing
123
+ waits for it. Serial handlers fail as described in
124
+ [Refusals](/actors/introduction#refusals).
125
+
126
+ `call` works on serial events only. A concurrent handler produces no
127
+ answer, so calling one is rejected: `send` those events instead.