feat: SSE transport as a portable alternative to the WebSocket - #195
Merged
Conversation
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.
❌ Deploy Preview for devfra failed.
|
antfubot
force-pushed
the
feat/sse-transport
branch
from
August 13, 2026 03:46
1e3b36d to
f722847
Compare
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 —GETopens the event stream,POSTcarries RPC frames,OPTIONSanswers the CORS preflight for registered viewers. Mirrors birpc 4.1's SSE wire semantics (session id as the stream's first frame, echoed inx-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.<base>__sseon 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.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 oninitDevframe,createDevServer,initHub, andcli.ws/cli.ssedefaults.Client
createSseRpcChannel(devframe/rpc/transports/sse-client): fetch-streaming (noEventSource), wire-compatible with the server half; closed-is-done like the WS channel.client/rpc-live.ts; the SSE mode is a second thin wrapper.resolveSseUrlapplies the same proxy-safe resolution rules asresolveWsUrl.connectDevframegainstransport: '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 readonlyclient.transport.Also in this PR
createRpcWireCodec/peekRpcWireFramefactor the per-connection wire codec shared by all four transport halves — exposed ondevframe/internalonly (explicitly unstable), keeping the publicdevframe/rpcsurface unchanged. AGENTS.md gains a convention: be very strict about adding or changing public APIs.ConnectionMetawidening from refactor!: transport-neutral RPC sessions and connection meta #193 is now produced:sseendpoint field,backend: 'sse' | 'none'.docs/errors/DF0057.md.?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
sse.test.ts) + 8 e2e tests (sse-e2e.test.ts: real server, fullconnectDevframeover SSE — trust handshake, shared state, streaming, origin rejection, SSE-only, RPC-less).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.