experimental-a2 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +8 -7
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
  27. package/dist/server-286j79Mt.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
  29. package/dist/server-DgXmORIq.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
  34. package/dist/store-flRz1OWh.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +1 -59
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.js +1 -81
  43. package/dist/store-postgres.js.map +1 -1
  44. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
  45. package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
  46. package/dist/store-redis-http.d.ts +1 -1
  47. package/dist/store-redis-http.js +2 -3
  48. package/dist/store-redis-http.js.map +1 -1
  49. package/dist/store-redis.d.ts +1 -1
  50. package/dist/store-redis.js +3 -4
  51. package/dist/store-redis.js.map +1 -1
  52. package/dist/store-sqlite.d.ts +1 -1
  53. package/dist/store-sqlite.d.ts.map +1 -1
  54. package/dist/store-sqlite.js +1 -72
  55. package/dist/store-sqlite.js.map +1 -1
  56. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  57. package/dist/wire-B6te_wns.js.map +1 -0
  58. package/docs/guides/03-react.mdx +20 -28
  59. package/docs/guides/05-production.mdx +9 -11
  60. package/docs/guides/06-ai-agents.mdx +8 -14
  61. package/docs/guides/09-presence.mdx +14 -19
  62. package/docs/guides/10-transports.mdx +104 -86
  63. package/docs/reference/01-api.mdx +143 -277
  64. package/docs/reference/02-errors.mdx +5 -7
  65. package/package.json +1 -14
  66. package/src/ai-server.ts +9 -5
  67. package/src/client.ts +2 -2
  68. package/src/errors.ts +1 -0
  69. package/src/internal.ts +3 -62
  70. package/src/push-envelope.ts +24 -21
  71. package/src/scheduler-qstash.ts +3 -3
  72. package/src/scheduler-vercel.ts +2 -2
  73. package/src/server-fetch.ts +344 -0
  74. package/src/server.ts +73 -225
  75. package/src/session-socket.ts +36 -20
  76. package/src/sse.ts +2 -2
  77. package/src/store-memory.ts +0 -81
  78. package/src/store-postgres.ts +0 -100
  79. package/src/store-redis-core.ts +47 -211
  80. package/src/store-redis-http.ts +0 -1
  81. package/src/store-redis.ts +0 -1
  82. package/src/store-sqlite.ts +0 -119
  83. package/src/store.ts +0 -60
  84. package/src/wire.ts +2 -1
  85. package/dist/ai-D_PGS-JR.d.ts +0 -334
  86. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  87. package/dist/cli-B3VuxoDe.js +0 -599
  88. package/dist/cli-B3VuxoDe.js.map +0 -1
  89. package/dist/cli-bin.d.ts +0 -1
  90. package/dist/cli-bin.js +0 -7
  91. package/dist/cli-bin.js.map +0 -1
  92. package/dist/cli.d.ts +0 -20
  93. package/dist/cli.d.ts.map +0 -1
  94. package/dist/cli.js +0 -2
  95. package/dist/client-CdMqi7mC.d.ts +0 -202
  96. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  97. package/dist/client-Dj5d3SP_.js +0 -1026
  98. package/dist/client-Dj5d3SP_.js.map +0 -1
  99. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  100. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  101. package/dist/devtools-kJJaORn-.js +0 -340
  102. package/dist/devtools-kJJaORn-.js.map +0 -1
  103. package/dist/devtools-server.browser.d.ts +0 -1
  104. package/dist/devtools-server.browser.js +0 -6
  105. package/dist/devtools-server.browser.js.map +0 -1
  106. package/dist/devtools-server.d.ts +0 -23
  107. package/dist/devtools-server.d.ts.map +0 -1
  108. package/dist/devtools-server.js +0 -1270
  109. package/dist/devtools-server.js.map +0 -1
  110. package/dist/devtools.d.ts +0 -2
  111. package/dist/devtools.js +0 -2
  112. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  113. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  114. package/dist/http.d.ts +0 -151
  115. package/dist/http.d.ts.map +0 -1
  116. package/dist/http.js +0 -706
  117. package/dist/http.js.map +0 -1
  118. package/dist/inspection-DaxB5jM2.js +0 -13
  119. package/dist/inspection-DaxB5jM2.js.map +0 -1
  120. package/dist/internal-DstsI6Re.js.map +0 -1
  121. package/dist/platform-B4TnJtWu.js +0 -34
  122. package/dist/platform-B4TnJtWu.js.map +0 -1
  123. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  124. package/dist/server-Duw6MVlB.js.map +0 -1
  125. package/dist/store-DysUkTH3.d.ts.map +0 -1
  126. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  127. package/dist/testing.browser.d.ts +0 -1
  128. package/dist/testing.browser.js +0 -6
  129. package/dist/testing.browser.js.map +0 -1
  130. package/dist/testing.d.ts +0 -32
  131. package/dist/testing.d.ts.map +0 -1
  132. package/dist/testing.js +0 -103
  133. package/dist/testing.js.map +0 -1
  134. package/dist/wire-BFQmSJ-9.js.map +0 -1
  135. package/docs/guides/07-devtools.mdx +0 -229
  136. package/src/cli-bin.ts +0 -5
  137. package/src/cli.ts +0 -1046
  138. package/src/devtools-app.ts +0 -989
  139. package/src/devtools-server.browser.ts +0 -5
  140. package/src/devtools-server.ts +0 -604
  141. package/src/devtools.ts +0 -716
  142. package/src/http.ts +0 -394
  143. package/src/inspection.ts +0 -39
  144. package/src/testing.browser.ts +0 -5
  145. package/src/testing.ts +0 -185
@@ -7,8 +7,8 @@ description: "One client, three wires: a single HTTP route, split routes for per
7
7
 
8
8
  Everything the client does is transport-agnostic: optimistic pushes
9
9
  with ack and `confirmed`, frontier-resumed live reads, reconnection
10
- with backoff, the presence throttle and re-send. `api` picks the wire
11
- underneath it:
10
+ with backoff, and presence throttling. `api` picks the wire underneath
11
+ it:
12
12
 
13
13
  ```ts
14
14
  // in a 'use client' session module:
@@ -18,73 +18,111 @@ api: { type: 'ws', url: '/api/order-events' }
18
18
  ```
19
19
 
20
20
  - **The string** is the default and the recommendation: one route
21
- serving GET (SSE stream) and POST (push), one file, one auth surface.
22
- Everything else in these docs uses it.
23
- - **`http` split** serves the two verbs from two routes. Use it when
24
- platform duration limits differ per verb: the stream is a long-lived
25
- read that wants a high `maxDuration`, the push is a short write that
26
- doesn't.
27
- - **`ws`** rides everything over one WebSocket: streams come down it,
28
- pushes and presence go up it. One socket carries every session of
29
- the client; a page showing ten sessions holds one connection, not
30
- ten. Use it when latency or per-message cost matters; at a presence
31
- cadence of fifteen sends a second, each send is a socket frame
32
- instead of a route invocation.
21
+ serving GET (SSE stream and history) and POST (push).
22
+ - **`http` split** serves the two verbs from separate routes. Use it
23
+ when platform duration limits differ per verb.
24
+ - **`ws`** rides streams, pushes, and presence over one multiplexed
25
+ WebSocket. One socket carries every session of the client.
33
26
 
34
27
  A split socket is unrepresentable on purpose. The socket is one
35
- connection in both directions; there is nothing left to split.
28
+ connection in both directions.
36
29
 
37
30
  One wire is missing from `ws` by design: the history lane.
38
31
  [`loadHistory`](/guides/react#the-client-component) is a bounded cold
39
- read and rides plain HTTP; on a `ws` api it throws a `TypeError`.
32
+ read and rides plain HTTP. On a `ws` API it throws a `TypeError`.
33
+
34
+ ## The HTTP route
35
+
36
+ `server.fetch` is bound, so an unprotected route is two exports:
37
+
38
+ ```ts app/api/public-order-events/route.ts
39
+ import { ordersServer } from '@/server/orders'
40
+
41
+ export const GET = ordersServer.fetch
42
+ export const POST = ordersServer.fetch
43
+ ```
44
+
45
+ A plain GET streams one session over SSE. A GET with `gte` and `lte`
46
+ returns a history slice. POST parses the push envelope and appends its
47
+ events. If the contract declares presence, the same route streams and
48
+ accepts presence automatically.
49
+
50
+ Wrap the call when the route needs authentication or authorization:
51
+
52
+ ```ts app/api/protected-order-events/route.ts
53
+ import { ordersServer } from '@/server/orders'
54
+
55
+ async function fetchOrders(request: Request): Promise<Response> {
56
+ // your authentication:
57
+ const user = { id: 'user-1' }
58
+ if (!user) return new Response(null, { status: 401 })
59
+
60
+ return ordersServer.fetch(request, {
61
+ authorize(operation) {
62
+ // your per-session and per-operation authorization:
63
+ // return canAccess(user, operation)
64
+ return true
65
+ },
66
+ })
67
+ }
68
+
69
+ export const GET = fetchOrders
70
+ export const POST = fetchOrders
71
+ ```
72
+
73
+ `authorize` runs after A2 parses the operation and before schema
74
+ validation or durable I/O. It receives `stream`, `history`, and `push`
75
+ operations. Push operations include parsed event names and payloads,
76
+ plus the complete presence patch when present. Return `false` to deny.
77
+ A2 answers `FORBIDDEN` with status 403. If the hook throws, A2 treats
78
+ that as an infrastructure failure and answers `STORE_UNAVAILABLE`.
79
+
80
+ The hook is per `fetch` call. Authentication stays in the route, where
81
+ request headers, cookies, and framework context are available. The
82
+ captured user can then authorize strongly typed A2 operations without
83
+ repeating request parsing.
40
84
 
41
85
  ## The WebSocket route
42
86
 
43
- The same `handle` route serves both transports. Pass `options.upgrade`
44
- and a GET carrying an upgrade header becomes the socket; plain GETs
45
- stay SSE, POST keeps working next to it. A `ws` client never calls
46
- POST, an `http` client never upgrades; the transports are additive.
87
+ Pass the platform's upgrade capability on GET. POST can stay a direct
88
+ export for HTTP clients:
47
89
 
48
- ```ts app/api/order-events/route.ts
90
+ ```ts app/api/socket-order-events/route.ts
49
91
  import { experimental_upgradeWebSocket } from '@vercel/functions'
50
- import { handle } from 'experimental-a2/http'
51
92
  import { ordersServer } from '@/server/orders'
52
93
 
53
- export const { GET, POST } = handle(ordersServer, {
54
- before({ request, intent }) {
55
- // here's where you'd do auth, or any other checks: the upgrade
56
- // itself, every subscribe, and every push arrive here as intents
57
- },
58
- upgrade: (attach) =>
59
- experimental_upgradeWebSocket(attach, {
60
- // ws defaults to 100 MiB per frame; POST bodies cap at about
61
- // 4.5 MB on the platform. Keep the two ingress paths at parity.
62
- maxPayload: 4 * 1024 * 1024,
63
- }),
64
- })
94
+ export const POST = ordersServer.fetch
95
+
96
+ export function GET(request: Request): Promise<Response> {
97
+ return ordersServer.fetch(request, {
98
+ upgradeWebSocket: (attach) =>
99
+ experimental_upgradeWebSocket(attach, {
100
+ maxPayload: 4 * 1024 * 1024,
101
+ }),
102
+ })
103
+ }
65
104
  ```
66
105
 
67
- The socket is multiplexed: `subscribe` frames open per-session lanes,
68
- each resuming from its own frontier; every down frame carries the
69
- `sessionId` it belongs to; pushes and presence route by it. One
70
- heartbeat, one connection, all of the client's sessions.
71
-
72
- Auth has two moments. `before` runs for the upgrade itself
73
- (`intent.type === 'ws-upgrade'`), while the request is still a request;
74
- return a Response to refuse and no socket ever opens. It then runs
75
- again for every subscribe and push frame: each subscribe arrives as a
76
- `stream` intent, each push as a `push` intent, with `request` always
77
- the original upgrade Request. A Response cannot cross a socket, so a
78
- denial answers in the wire's own vocabulary: a denied subscribe gets an
79
- `unsubscribed` notice (every other session on the socket streams on), a
80
- denied push a non-retryable error ack. Socket presence frames are
81
- fire-and-forget and never become intents; the presence plane's policy
82
- seam on every wire is `validatePush`.
83
-
84
- Two more options ride along: `presence: true` interleaves presence with
85
- events on every lane, the same opt-in as `stream()`, and `deadline`
86
- (epoch milliseconds) closes the socket cleanly ahead of a known
87
- platform deadline, so clients reconnect on your schedule.
106
+ The socket is multiplexed. Subscribe frames open per-session lanes,
107
+ each resuming from its own frontier. Every down frame carries its
108
+ `sessionId`; pushes and presence route by it. One heartbeat and one
109
+ connection serve all of the client's sessions.
110
+
111
+ Authenticate the physical upgrade before calling `server.fetch`.
112
+ Then pass `authorize` to check every session subscribe and every push
113
+ or presence frame. There is no separate upgrade operation: request
114
+ policy belongs to the route, while A2 operation policy belongs to
115
+ `authorize`.
116
+
117
+ A denied subscribe receives an `unsubscribed` notice and leaves other
118
+ sessions connected. A denied event push receives a non-retryable
119
+ `FORBIDDEN` ack. Denied presence is silently dropped because presence
120
+ is fire-and-forget and repaints on the next update. Failed presence
121
+ authorization is dropped the same way and leaves the socket connected.
122
+
123
+ A2 reads the platform's ambient invocation deadline and closes the
124
+ socket cleanly before it. `waitUntil` and deadline discovery remain
125
+ platform capabilities used internally, not options on `server.fetch`.
88
126
 
89
127
  ## The client
90
128
 
@@ -104,37 +142,17 @@ export const { SessionProvider, useSession } = createReact({
104
142
  ```
105
143
 
106
144
  Nothing else changes. `useSession` is identical, `push` returns the
107
- same ack and `confirmed`, presence coalesces on the same cadence. The
108
- connection status, the stall watchdog (two missed heartbeats), and
109
- reconnection with backoff all behave as they do over SSE, because they
110
- are the same machinery above the wire seam.
145
+ same ack and `confirmed`, and presence coalesces on the same cadence.
146
+ The connection status, stall watchdog, and reconnection behavior are
147
+ shared above the wire seam.
111
148
 
112
149
  ## Lifecycle
113
150
 
114
- A socket closes when the platform ends the function invocation, or
115
- when the server closes it deliberately ahead of a known deadline. The
116
- client treats every close the same way it treats a dropped SSE stream:
117
- reconnect with backoff, re-subscribe every session at its own frontier,
118
- receive fresh presence snapshots, re-send its own presence fields set
119
- since the disconnect. A push whose socket died before the ack rejects as
120
- retryable: nothing was acknowledged, and if the append had already
121
- committed, the client-generated event ids make the retry an idempotent
122
- replay (you get the original events back). Push retries wait for the
123
- restored socket rather than a fixed clock, capped by the reconnect
124
- backoff, so a push caught in a routine blip resolves the way it would
125
- have over HTTP, where pushes never depended on the stream.
126
-
127
- On Vercel, WebSocket connections pin to one function instance and new
128
- connections may land anywhere. A2 never relies on instance affinity:
129
- the log is the shared state, so two clients on two instances converge
130
- through it, exactly as they do over HTTP.
131
-
132
- ## Choosing
133
-
134
- SSE plus POST is the right default: simplest route, ordinary HTTP
135
- semantics, and one auth surface. Reach for `ws` when the send rate
136
- makes per-request overhead visible (live cursors, drag ghosts) or when
137
- round-trip latency is the product. Reach for the `http` split only when
138
- per-route duration configuration forces it. The protocol underneath is
139
- the same in every case: the log replays, presence repaints, and the
140
- wire is an implementation detail.
151
+ When a socket closes, the client reconnects with backoff, re-subscribes
152
+ every session at its own frontier, receives fresh presence snapshots,
153
+ and re-sends its own latest presence values. Event pushes keep their
154
+ client-generated ids, so retry remains idempotent.
155
+
156
+ SSE and WebSocket are transport choices, not different consistency
157
+ models. Both resume from the durable event frontier and treat presence
158
+ as ephemeral repaintable state.