Skip to content

feat: SSE transport as a portable alternative to the WebSocket - #195

Merged
antfu merged 2 commits into
mainfrom
feat/sse-transport
Aug 13, 2026
Merged

feat: SSE transport as a portable alternative to the WebSocket#195
antfu merged 2 commits into
mainfrom
feat/sse-transport

Conversation

@antfubot

@antfubot antfubot commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Intent

Serve devframe RPC over SSE + HTTP POST wherever the WebSocket upgrade isn't available (serverless hosts, buffering reverse proxies, restrictive networks) — same birpc wire protocol, same serialization, auth, origin policy, shared state, and streaming. Builds on the transport-neutral session seam from #193.

Server

  • attachSseRpcTransport (devframe/rpc/transports/sse-server): a web-standard fetch handler on one method-dispatched route — GET opens the event stream, POST carries RPC frames, OPTIONS answers the CORS preflight for registered viewers. Mirrors birpc 4.1's SSE wire semantics (session id as the stream's first frame, echoed in x-birpc-session, client-initiated responses parked in the POST body; server-initiated traffic streamed down) while integrating devframe's origin gate, dual strict-JSON/structured-clone codec, and connect-time token auth. Keep-alive comment every 30s so intermediaries don't drop idle streams.
  • The instance shell mounts it at <base>__sse on the same h3 app that serves __connection.json — every HTTP-backed tier (owned server, shared server, handler/nodeMiddleware, the Vite bridge's middleware) serves it with zero upgrade wiring, and the transport only loads on first use. One RPC core is now shared across transports, so WS peers and SSE sessions live in one session/broadcast space.
  • Config: ws: false → SSE-only (backend: 'sse', DF0057 if a host still wires upgrades); sse: false → WS-only; both → RPC-less shell (backend: 'none', SPA/MCP still served). Available on initDevframe, createDevServer, initHub, and cli.ws/cli.sse defaults.

Client

  • createSseRpcChannel (devframe/rpc/transports/sse-client): fetch-streaming (no EventSource), wire-compatible with the server half; closed-is-done like the WS channel.
  • The WS mode's status machine / call guarding / trust handshake is extracted into a shared client/rpc-live.ts; the SSE mode is a second thin wrapper. resolveSseUrl applies the same proxy-safe resolution rules as resolveWsUrl.
  • connectDevframe gains transport: 'auto' | 'websocket' | 'sse' — auto trusts the server's advertised primary (WS preferred when both are present; no client-side fallback probing), explicit values pin and fail clearly when unadvertised — plus a readonly client.transport.

Also in this PR

  • createRpcWireCodec / peekRpcWireFrame factor the per-connection wire codec shared by all four transport halves — exposed on devframe/internal only (explicitly unstable), keeping the public devframe/rpc surface unchanged. AGENTS.md gains a convention: be very strict about adding or changing public APIs.
  • The ConnectionMeta widening from refactor!: transport-neutral RPC sessions and connection meta #193 is now produced: sse endpoint field, backend: 'sse' | 'none'.
  • Docs: new Transports guide, docs/errors/DF0057.md.
  • Hub examples (parity): the subsystem drawer gains a Transport section — active-transport indicator + segmented Auto/WS/SSE toggle riding a ?transport= param.
  • examples/sse-basic: minimal SSE-only vanilla app (ws: false) showing the transport field, RPC calls, and a shared-state clock streaming down.

Verification

  • 10 wire-protocol unit tests (sse.test.ts) + 8 e2e tests (sse-e2e.test.ts: real server, full connectDevframe over SSE — trust handshake, shared state, streaming, origin rejection, SSE-only, RPC-less).
  • Full suite 1099/1099, typecheck across 27 packages, lint + knip clean, API snapshots regenerated.
  • Browser-verified: sse-basic (transport=sse, live clock, RPC round-trip) and hub-vite's toggle switching SSE ↔ WS with the full hub booting on each.

Created with the help of an agent.

Add an SSE + HTTP POST RPC transport alongside the WebSocket, for hosts
and proxies where the upgrade isn't available:

- Server: `attachSseRpcTransport` (devframe/rpc/transports/sse-server) —
  a fetch-style handler mirroring birpc's SSE wire semantics (session id
  as the stream's first event, echoed in `x-birpc-session` on POSTs,
  client-initiated responses parked in the POST body), with devframe's
  origin gate, dual strict-JSON/structured-clone codec, connect-time
  token auth, and a 30s keep-alive comment. Mounted by the instance
  shell at `<base>__sse` on the same app serving `__connection.json`,
  so every HTTP-backed tier — including the Vite bridge's middleware —
  serves it with no upgrade wiring.
- Client: `createSseRpcChannel` (fetch-streaming, no EventSource) plus
  an SSE client mode sharing the WS mode's status machine, call
  guarding, and trust handshake (extracted into `client/rpc-live.ts`).
  `connectDevframe` gains `transport: 'auto' | 'websocket' | 'sse'`
  (auto trusts the server's advertised primary) and a readonly
  `transport` field.
- Config: `ws: false` runs SSE-only (`backend: 'sse'`); `sse: false`
  disables the endpoint; both off is an RPC-less shell
  (`backend: 'none'`). `ConnectionMeta` advertises the `sse` endpoint
  with the same proxy-safe resolution rules as the WebSocket.
- The wire codec shared by all four transport halves is factored into
  `createRpcWireCodec` (rpc/serialization).
- New diagnostic DF0057 (upgrade wiring with `ws: false`) + docs page,
  a Transports guide, hub example drawers gain a transport indicator +
  Auto/WS/SSE toggle (hub-vite + hub-next, parity), and a new
  `examples/sse-basic` minimal SSE-only app.
@netlify

netlify Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploy Preview for devfra failed.

Name Link
🔨 Latest commit fb5789b
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a7d4a1eae7c6a0008ac174d

`createRpcWireCodec` / `peekRpcWireFrame` / `RpcWireCodec` are
cross-transport plumbing, not user API — they rode onto the public
`devframe/rpc` surface through the serialization module's star export.
Move them to their own module surfaced only via `devframe/internal`
(explicitly unstable), where custom transport implementations reach
them.

Also add an AGENTS.md convention: be very strict about adding or
changing public APIs — prefer devframe/internal for shared plumbing,
watch what rides along star-exported barrels, and treat every tsnapi
snapshot diff as an API-design decision.
@antfu
antfu merged commit fa66400 into main Aug 13, 2026
7 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants