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
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: State
3
+ description: "State is a fold over the log: raw history for facts, a reducer for the current view."
4
+ ---
5
+
6
+ ## History
7
+
8
+ ```ts
9
+ // anywhere on the server:
10
+ const events = await ordersServer.session(orderId).history()
11
+ ```
12
+
13
+ Every event in the session, oldest first: always the raw log, never a summary
14
+ or snapshot. Use it for audits, debugging, and handler questions like "did the
15
+ shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
16
+
17
+ ## Reducers
18
+
19
+ Derive a reducer from the [`orders` contract](/contract/events) and fold:
20
+
21
+ ```ts reducer.ts
22
+ import { reducer } from 'experimental-a2'
23
+ import { z } from 'zod'
24
+ import { orders } from './contracts'
25
+
26
+ export const ordersReducer = reducer({
27
+ contract: orders,
28
+ name: 'order-status',
29
+ initialState: { status: 'new' },
30
+ stateSchema: z.object({
31
+ status: z.enum(['new', 'created', 'notified', 'expired']),
32
+ shopId: z.string().optional(),
33
+ }),
34
+ }).fold((state, event) => {
35
+ switch (event.type) {
36
+ case 'created':
37
+ return { status: 'created', shopId: event.payload.shopId }
38
+ case 'shop.notified':
39
+ return { ...state, status: 'notified' }
40
+ case 'expired':
41
+ return { ...state, status: 'expired' }
42
+ default:
43
+ return state
44
+ }
45
+ })
46
+ ```
47
+
48
+ ```ts
49
+ // anywhere on the server, e.g. a server component before first paint:
50
+ const { state, index } = await ordersServer.session(orderId).state(ordersReducer)
51
+ ```
52
+
53
+ A reducer is a `name` (its [cache identity](#snapshots-are-a-cache)), an
54
+ `initialState`, an optional `stateSchema`, and a pure `fold`,
55
+ `(state, event) => state`. The two-step shape fixes the state and event types
56
+ before `.fold()`, so you write no type arguments and literal unions survive.
57
+
58
+ `state()` returns `state` and `index`: the fold includes every event through
59
+ that index and none after. The browser resumes its live stream from there; see
60
+ [React](/client/react).
61
+
62
+ Without `stateSchema`, the state type is inferred from `initialState`, fine
63
+ while every fold arm returns the same shape. With it, the schema is the type
64
+ (the seed may lean on its defaults), `initialState` is validated at
65
+ definition, and a cached fold that fails the schema, in a server snapshot or
66
+ the [browser cache](/client/react), is discarded and refolded. That catches
67
+ shape drift a stale `name` can't.
68
+
69
+ Contract and reducer modules import only schemas and `experimental-a2`, so the
70
+ browser runs the same reducer; see [React](/client/react#publish-the-state). A
71
+ server-only app can keep the contract next to `createServer` instead.
72
+
73
+ ## Read boundaries
74
+
75
+ A root session reads the latest committed frontier. A handler's session stops
76
+ at its triggering event by default, stable across concurrent appends and
77
+ retries:
78
+
79
+ ```ts server.ts
80
+ import { createServer } from 'experimental-a2/server'
81
+ import { orders } from './contracts'
82
+ import { ordersReducer } from './reducer'
83
+
84
+ export const ordersServer = createServer({
85
+ contract: orders,
86
+ handlers: {
87
+ created: async ({ event, session }) => {
88
+ const atTrigger = await session.state(ordersReducer)
89
+ // atTrigger.index === event.index
90
+
91
+ const latest = await session.state(ordersReducer, { through: 'latest' })
92
+ // latest also folds events appended after this one
93
+ },
94
+ },
95
+ })
96
+ ```
97
+
98
+ Use `{ through: 'latest' }` for joins or other logic that intentionally
99
+ observes later events. A numeric `{ through: index }` reads any inclusive log
100
+ boundary; `through: 0` returns `initialState`. `state()` is observational: it
101
+ never runs handlers or waits for pending work, and its index marks committed
102
+ history, not handler completion.
103
+
104
+ A latest read and a following `ctx.session.append(name, ...events)` are
105
+ separate operations, and concurrent appends and retries can move the frontier
106
+ between them. For joins, read with `{ through: 'latest' }`, use a monotone
107
+ readiness check (once ready, always ready), and give the output event a stable
108
+ explicit `id`. Repeated attempts then converge on the same append.
109
+
110
+ ## Snapshots are a cache
111
+
112
+ The store backend caches folded state as snapshots keyed by the reducer's
113
+ `name`, so two reducers over one session never share an entry. Changed the
114
+ fold's logic? Change the name (a `-v2` suffix works). The next read ignores
115
+ the old snapshots, refolds from raw events, and caches under the new name.
116
+
117
+ A missing snapshot, or one `stateSchema` rejects, rebuilds from the log. A
118
+ storage read failure propagates with its backend diagnostics instead: existing
119
+ A2 errors pass through, and others become `STORE_UNAVAILABLE` with the
120
+ original error as their cause.
121
+
122
+ Snapshot write-back runs in platform `waitUntil` after the state is ready. A
123
+ failed write doesn't fail the read: A2 reports it through the `a2.snapshot`
124
+ telemetry span (or `console.error` by default) without snapshot contents, and
125
+ the next read folds the missing tail again.
126
+
127
+ Each session and reducer has one latest snapshot. Older ones exist only while
128
+ handler events that read at their boundary are unfinished (not yet completed
129
+ or dead-lettered). This is automatic: there is no TTL, history limit, or
130
+ retention setting.
131
+
132
+ Deleting every snapshot is always safe. The log rebuilds them.
133
+
134
+ ## Events don't change meaning
135
+
136
+ The log is append-only, and `created` means forever what it meant when it was
137
+ written. When a payload needs a new shape, add a new event type (`created.v2`)
138
+ instead of redefining the old one.
139
+
140
+ Reducers accumulate cases over time and never migrate anything: old sessions
141
+ keep their old events, new sessions write new ones, and the same fold handles
142
+ both.
@@ -0,0 +1,175 @@
1
+ ---
2
+ title: Sessions
3
+ description: "A session is one instance of a contract, with its own ordered log, state, and live stream. Collections and queries stay in your database."
4
+ ---
5
+
6
+ ```ts
7
+ // anywhere on the server: a route, a handler, a script
8
+ const order = ordersServer.session('order-42')
9
+ ```
10
+
11
+ A session is one instance of the contract (one order, one chat, one import)
12
+ with its own ordered log, state, recovery, and live stream. The id is yours.
13
+ Creating the handle does no I/O; nothing loads until you append, read, or
14
+ stream.
15
+
16
+ ## Pick the boundary
17
+
18
+ Put events in one session when they form one history, fold, recovery unit, or
19
+ live view. Split what should be read and recovered independently.
20
+
21
+ A session is not a table or a general query boundary: one `orders` contract can
22
+ serve millions of order sessions through the same server and handlers.
23
+ Collections, joins, search, and constraints across many records belong in
24
+ [your database](#sessions-and-your-database).
25
+
26
+ ## Clearing a session
27
+
28
+ ```ts server/clear-order.ts
29
+ import { ordersServer } from './orders'
30
+
31
+ export async function clearOrder({ orderId, lastIndex }: {
32
+ orderId: string
33
+ lastIndex: number
34
+ }) {
35
+ // here's where you'd do auth, or any other checks
36
+ return ordersServer.session(orderId).clear({ ifLastIndex: lastIndex })
37
+ }
38
+ ```
39
+
40
+ `session.clear()` ends a session's current lifetime: its history is gone, every
41
+ reducer is back at its initial state, and the id keeps working for the next
42
+ lifetime. It appends a reserved marker, so indexes never restart: clearing a
43
+ session whose last event is `42` returns `{ cleared: true, index: 43 }`, and the
44
+ next event is `44`.
45
+
46
+ Pass `ifLastIndex` when newer events must survive. The store checks it in the
47
+ same atomic step that appends the marker; if another event landed first, the
48
+ clear skips and returns `{ cleared: false, index }` with the current head. A
49
+ conditional clear is safe to retry.
50
+
51
+ The old lifetime's pending events are never dispatched, and a handler still
52
+ running for one can't append, return events, or read the new lifetime's state.
53
+ Clearing can't stop code that is already running or undo its side effects.
54
+
55
+ Connected clients reset when the marker reaches them. After a clear,
56
+ `session.state()` also returns `clearIndex`: pass the whole result as a client's
57
+ `initial` so a cached client drops its old history too (the automatic snapshot
58
+ on subscribe already carries it).
59
+
60
+ `clear()` resolves once the old events and cached state are deleted; appends
61
+ and reads keep running meanwhile, and the cost grows with the old history. If
62
+ deletion fails partway, the session is still cleared and the leftovers are
63
+ invisible: retry, or clear again later, to finish. For a very large session,
64
+ prefer a [scheduled clear](#scheduling-a-clear), because the provider retries
65
+ it and each retry resumes the deletion. Presence isn't cleared; it keeps its
66
+ own expiry.
67
+
68
+ `server.fetch` has no clear endpoint. Clearing is a server operation, so
69
+ authorize your own route before calling it.
70
+
71
+ ### Scheduling a clear
72
+
73
+ With a [scheduler](/server/production) configured, a handler can schedule a
74
+ clear. This one clears an idle cart a day after its last activity:
75
+
76
+ ```ts server/carts.ts
77
+ import * as a2 from 'experimental-a2'
78
+ import { createServer } from 'experimental-a2/server'
79
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
80
+ import { z } from 'zod'
81
+
82
+ export const carts = a2.contract({
83
+ name: 'carts',
84
+ events: {
85
+ 'item.added': z.object({ sku: z.string() }),
86
+ },
87
+ })
88
+
89
+ export const cartsServer = createServer({
90
+ contract: carts,
91
+ scheduler: vercelQueues(),
92
+ handlers: {
93
+ 'item.added': async ({ event, session }) => {
94
+ await session.schedule.clear('idle', {
95
+ delay: '1d',
96
+ ifLastIndex: event.index,
97
+ })
98
+ },
99
+ },
100
+ })
101
+ ```
102
+
103
+ The delay counts from the event's stored time. A later event moves the head
104
+ past `event.index`, so this delivery skips and that event's handler schedules
105
+ the next check; only events with a handler extend the policy. A handler's own
106
+ writes move the head too, so one that appends should pass the index its last
107
+ append returned.
108
+
109
+ Delivery runs the same conditional clear and finishes the deletion before it
110
+ acknowledges, so provider retries complete an interrupted delete. No event is
111
+ appended. The clear happens at delivery, not at an exact deadline, within
112
+ provider limits: Vercel Queues accepts delays up to six days.
113
+
114
+ ## Sessions and your database
115
+
116
+ A2 does not replace your application database. It syncs one known session at a
117
+ time (its events, derived state, and optimistic browser writes), not tables or
118
+ queries. Give each fact one owner:
119
+
120
+ - If A2 owns it, append an event and derive state with a reducer.
121
+ - If your database owns it, write it with the database's normal transaction.
122
+
123
+ Never make both paths authoritative for the same fact.
124
+
125
+ Use A2 when people open something by id and watch it progress: an order in
126
+ fulfillment, an agent producing a response, an approval waiting for a decision,
127
+ an import that can fail and resume. Use your database when people list,
128
+ filter, join, and edit many records, and for constraints that must hold across
129
+ rows before a request returns.
130
+
131
+ `experimental-a2/store-postgres` stores A2's own event logs. It does not watch
132
+ your tables, turn their changes into events, or combine an A2 append with your
133
+ SQL transaction. Both can share one Postgres database as separate data models
134
+ and transaction boundaries.
135
+
136
+ ### An issue tracker
137
+
138
+ Postgres usually owns an issue tracker's core data; A2 sessions own the runs
139
+ attached to it:
140
+
141
+ | Part | Owner |
142
+ | --- | --- |
143
+ | issues, projects, labels, comments | Postgres |
144
+ | permissions, board filters, search | Postgres |
145
+ | an AI investigation attached to an issue | one A2 session |
146
+ | an import or automation run | one A2 session |
147
+
148
+ The investigation's session refers to the issue id without becoming a second
149
+ owner of the issue. A2 can own individual issues when their history and live
150
+ state are the center of the product, but boards and search then span many
151
+ sessions and need another query model. Don't put a whole workspace in one
152
+ session for cross-issue queries: every event joins one growing log that every
153
+ client follows.
154
+
155
+ ### Database writes from handlers
156
+
157
+ Handlers may call your database like any other side effect. They are
158
+ at-least-once, so make the write idempotent with `event.id`; it runs after the
159
+ event is durable and may be retried. That is not one atomic write across A2
160
+ and your tables: if a relational change must commit before the request
161
+ succeeds, make it through the database's write path.
162
+
163
+ ### Cross-session indexes
164
+
165
+ A handler can maintain a small database index over A2-owned sessions, such as a
166
+ list of active imports. Treat it as an advanced integration:
167
+
168
+ - it is eventually consistent with the session logs;
169
+ - writes must be idempotent;
170
+ - A2 orders events within one session, not across sessions;
171
+ - no A2 API scans and replays every session to rebuild it.
172
+
173
+ Back up and migrate it like application data. If search, joins, and
174
+ cross-session reporting are central, the database should probably own that
175
+ domain.