Transports
One client, three wires: one HTTP route, split routes for per-verb timeouts, or one WebSocket.
One wire seam, same client
api picks the wire:
// in a 'use client' session module:
api: '/api/order-events'
api: (sessionId) => `/api/orders/${encodeURIComponent(sessionId)}/events`
api: { type: 'http', push: '/api/order-push', stream: '/api/order-stream' }
api: { type: 'ws', url: '/api/order-events' }- One route is the default and the recommendation: a fixed URL or a session URL function, serving GET (SSE stream and history) and POST (push).
httpsplit serves the two verbs from separate routes, for platforms whose duration limits differ per verb.wscarries streams, pushes, and presence over one socket per session. A socket is one connection in both directions, so it has no split form.
The server side of each lives on Routes. Everything above
the wire is shared: useSession, push acks and confirmed, presence
cadence, connection status, stall detection, and reconnection. SSE and
WebSocket resume from the same durable frontier and treat presence as
ephemeral, repaintable state.
Session URL functions
Every session-scoped URL may be a (sessionId) => string function. A2
resolves it for each request or reconnect, then adds its standard query
parameters. Encode the ID when placing it in a path segment.
The function chooses a route, not authority. If a path or connection token is
scoped to one session, authorize must
compare that scope to operation.sessionId. Bind URL tokens to the same
session and keep them short-lived, because URLs can appear in infrastructure
logs.
WebSocket
'use client'
import { createClient } from 'experimental-a2/client'
import { createReact } from 'experimental-a2/react'
import { orders } from '@/contracts'
export const ordersClient = createClient({
contract: orders,
api: { type: 'ws', url: '/api/order-events' },
})
export const { SessionProvider, useSession } = createReact({
client: ordersClient,
})To share one socket across the client's sessions, set multiplex: true in
api and multiplexWebSocket: true on the
route. A multiplexed URL is one plain
string, since the connection carries more than one session.
ws has no history lane. loadHistory
is a bounded cold read over plain HTTP; on a ws api it throws a
TypeError.
Lifecycle
When a socket closes, the client reconnects at its current frontier per its reconnection policy, receives a fresh presence snapshot, and re-sends its own latest presence. A multiplexed socket re-subscribes every active session at its own frontier. Pushes keep their client-generated ids, so retries stay idempotent.
The createClient({ reconnect }) policy returns a delay, or false to
pause. The default reconnects with backoff indefinitely. A clean close also
runs the policy, with error: null. A paused session keeps its state and
leases; session.reconnect() resumes it.
Upgrade authorization stays in the
HTTP route. Browser WebSockets hide a
rejected upgrade's status and body, so it surfaces as an opaque
CONNECTION_FAILED error. Structured subscription errors arrive once a
multiplexed socket is open.