experimental-a2 0.5.1 → 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.
- package/CHANGELOG.md +67 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +8 -7
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +41 -7
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +74 -35
- package/dist/react.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
- package/dist/server-286j79Mt.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
- package/dist/server-DgXmORIq.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
- package/dist/store-flRz1OWh.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +1 -59
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +1 -81
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
- package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -3
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +3 -4
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +1 -72
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/guides/03-react.mdx +118 -29
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +8 -14
- package/docs/guides/09-presence.mdx +19 -21
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +186 -279
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +9 -5
- package/src/client.ts +71 -35
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/react.ts +118 -44
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +73 -225
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +0 -81
- package/src/store-postgres.ts +0 -100
- package/src/store-redis-core.ts +47 -211
- package/src/store-redis-http.ts +0 -1
- package/src/store-redis.ts +0 -1
- package/src/store-sqlite.ts +0 -119
- package/src/store.ts +0 -60
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-BKlyLiOU.js +0 -1008
- package/dist/client-BKlyLiOU.js.map +0 -1
- package/dist/client-D7mvIXrF.d.ts +0 -191
- package/dist/client-D7mvIXrF.d.ts.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
|
@@ -63,29 +63,25 @@ explicit leave required).
|
|
|
63
63
|
|
|
64
64
|
## The route
|
|
65
65
|
|
|
66
|
-
The same `
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`setPresence`.
|
|
66
|
+
The same `server.fetch` route as [Live UI](/guides/react). Presence is
|
|
67
|
+
inferred from the contract. A contract with presence fields streams
|
|
68
|
+
presence patches with events, starting with a snapshot of the current
|
|
69
|
+
map. Its push body also accepts an optional `presence` sibling to
|
|
70
|
+
`events`, forwarded to `setPresence`.
|
|
71
71
|
|
|
72
72
|
```ts app/api/canvas-events/route.ts
|
|
73
|
-
import { handle } from 'experimental-a2/http'
|
|
74
73
|
import { canvasServer } from '@/server/canvas'
|
|
75
74
|
|
|
76
|
-
export const
|
|
77
|
-
|
|
78
|
-
before({ request, intent }) {
|
|
79
|
-
// here's where you'd do auth, or any other checks. On a push,
|
|
80
|
-
// intent.presence carries the whole patch; the participant id
|
|
81
|
-
// is caller-supplied, so authorize it like you authorize events.
|
|
82
|
-
// createServer's validatePush is the same seam and covers every
|
|
83
|
-
// transport (socket presence frames never become intents): on the
|
|
84
|
-
// presence plane it receives { sessionId, events: [], presence }.
|
|
85
|
-
},
|
|
86
|
-
})
|
|
75
|
+
export const GET = canvasServer.fetch
|
|
76
|
+
export const POST = canvasServer.fetch
|
|
87
77
|
```
|
|
88
78
|
|
|
79
|
+
Wrap `server.fetch` when the route needs authentication. Pass
|
|
80
|
+
`authorize` to check the typed A2 operation before field validation or
|
|
81
|
+
I/O. A push operation carries the complete presence patch, including
|
|
82
|
+
its caller-supplied participant id. The same hook runs for HTTP and
|
|
83
|
+
WebSocket presence.
|
|
84
|
+
|
|
89
85
|
`setPresence` validates each field against the contract, then
|
|
90
86
|
broadcasts. No append transaction, no dispatch, no scheduler arm, no log
|
|
91
87
|
row. A bad field throws `INVALID_PAYLOAD`; an unknown field throws
|
|
@@ -94,8 +90,11 @@ presence-only push acks `[]`.
|
|
|
94
90
|
|
|
95
91
|
## The browser
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
nonce, the playground's guest name).
|
|
93
|
+
Presence needs a `participant` id (yours to mint: a user id, a tab
|
|
94
|
+
nonce, the playground's guest name). State it once on
|
|
95
|
+
`createClient({ participant })` when the app knows it at module scope,
|
|
96
|
+
or pass it to the provider, whose `participant` prop overrides the
|
|
97
|
+
client's. The hook grows two members. The
|
|
99
98
|
session module is the usual pair from
|
|
100
99
|
[Live UI](/guides/react#the-session-module), bound to a reducer that
|
|
101
100
|
carries the canvas contract:
|
|
@@ -174,8 +173,7 @@ Both members are typed from the contract's `presence` map, through the
|
|
|
174
173
|
reducer, with no type arguments: field names, payloads, and `null`
|
|
175
174
|
clears all autocomplete. On a contract without `presence` the members
|
|
176
175
|
do not exist, so reaching for them is a compile error, not a silent
|
|
177
|
-
no-op.
|
|
178
|
-
echo while others are active, the GET route forgot `presence: true`.
|
|
176
|
+
no-op.
|
|
179
177
|
|
|
180
178
|
## Reconciling the planes: `seen`
|
|
181
179
|
|
|
@@ -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,
|
|
11
|
-
|
|
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)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
68
|
-
each resuming from its own frontier
|
|
69
|
-
`sessionId
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
fire-and-forget and
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
108
|
-
connection status,
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|