Skip to content

feat(devframe): streaming channel API for server↔client chunks#307

Merged
antfu merged 5 commits into
mainfrom
antfu/streaming-rpc
May 7, 2026
Merged

feat(devframe): streaming channel API for server↔client chunks#307
antfu merged 5 commits into
mainfrom
antfu/streaming-rpc

Conversation

@antfu
Copy link
Copy Markdown
Member

@antfu antfu commented May 7, 2026

Description

Adds a first-class streaming primitive — ctx.rpc.streaming (server) and rpc.streaming (client) — built on the existing WS/birpc transport, with full Web Streams interop, replay-on-reconnect, cooperative cancellation, and bidirectional support.

Server-to-client: channel.start({ id? }) returns a sink with imperative write / close / error plus a WritableStream<T> for pipeTo; channel.pipeFrom(readable) is a one-call shorthand. rpc.streaming.subscribe(channel, id) returns a reader that's both AsyncIterable<T> and exposes readable: ReadableStream<T>. replayWindow keeps a per-stream ring buffer; reconnecting subscribers resume from afterSeq. closedStreamRetention (default 30 s when replay is on) holds finished streams briefly so late panels can still see the transcript.

Client-to-server uploads: channel.openInbound({ id? }) returns a server-side reader; pair it with a normal action that returns the id. rpc.streaming.upload(channel, id) returns a sink whose chunks / end / error forward over the wire and whose signal aborts when the server cancels. Each inbound is owned by one session — no fan-in, no shared state; client disconnect surfaces as UploadDisconnected to the consuming for await.

Migration & dogfooding: DevToolsTerminalHost now pipes session output through the streaming channel devtoolskit:internal:terminals (the legacy terminal:session:stream-chunk host event is gone). New devframe-streaming-chat example with a Preact UI and four end-to-end tests (happy path, cancel, fan-out, replay). Four new structured diagnostics (DF0029–0032), full guide at docs/guide/streaming.md, RPC/terminals docs updated.

Pre-PR checklist (lint + 449 tests + typecheck + build) all green.

Linked Issues

closes #306

Additional context

The async-storage resolver in devframe/server.ts is now wired so getCurrentRpcSession() works in the standalone-CLI flavour — required for streaming subscribe/cancel and a latent gap before. _emitSessionDisconnected is added to RpcFunctionsHost so adapters can clean up per-session subscribers on WS close (wired in both devframe's server.ts and packages/core/src/node/ws.ts). The streaming channel API is intentionally framework-neutral and contains no Vite-specific bits, in line with devframe's positioning as the bottom of the stack.

🤖 Generated with Claude Code

Adds `ctx.rpc.streaming` (server) and `rpc.streaming` (client) — a
first-class streaming primitive built on the existing WS/birpc transport
with full Web Streams interop, replay-on-reconnect, cooperative
cancellation, and bidirectional support.

Server-to-client (closes #306):
- `channel.start({ id? })` returns a sink with imperative `write` /
  `close` / `error` plus a `WritableStream<T>` for `pipeTo`.
- `channel.pipeFrom(readable)` shorthand for one-call consumption.
- `rpc.streaming.subscribe(channel, id)` returns a reader that's both
  `AsyncIterable<T>` and exposes `readable: ReadableStream<T>`.
- `replayWindow` keeps a per-stream ring buffer; late or reconnecting
  subscribers replay from `afterSeq`. `closedStreamRetention` (default
  30 s when replay is on) holds finished streams briefly.

Client-to-server uploads:
- `channel.openInbound({ id? })` returns a server-side reader; pair
  with a normal RPC action that returns the id.
- `rpc.streaming.upload(channel, id)` returns a sink whose chunks /
  end / error forward over the wire; signal aborts on server cancel.
- Each inbound is owned by one session — no fan-in, no shared state;
  client disconnect surfaces as `UploadDisconnected` to the consumer.

Internal surface:
- `utils/streaming-channel.ts` — framework-neutral `createStreamSink`
  / `createStreamReader` primitives.
- `node/rpc-streaming.ts` and `client/rpc-streaming.ts` — RPC hosts.
- Five wire methods under `devtoolskit:internal:streaming:*`.
- Adds `_emitSessionDisconnected` on `RpcFunctionsHost` so adapters
  can clean up per-session subscribers on WS close. Wired in both
  `devframe/server.ts` and `packages/core/src/node/ws.ts`. Also wires
  the async-storage resolver in devframe's standalone server so
  `getCurrentRpcSession()` works there (was a latent gap).
- Four new structured diagnostics: DF0029 (overflow), DF0030
  (unknown id), DF0031 (write after close), DF0032 (channel name
  collision) with docs pages and sidebar entries.

Migrations & dogfooding:
- `DevToolsTerminalHost` now pipes session output into a streaming
  channel (`devtoolskit:internal:terminals`); the legacy
  `terminal:session:stream-chunk` host event and its rebroadcast are
  removed. Client xterm renderer consumes via `for await`.
- New `devframe-streaming-chat` example — Preact UI driving a
  synthetic LLM token producer, end-to-end tests covering happy
  path, cancel, fan-out, replay.
- Docs: new `guide/streaming.md`, updated RPC and terminals guides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 7, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@vitejs/devtools@307
npm i https://pkg.pr.new/@vitejs/devtools-kit@307
npm i https://pkg.pr.new/@vitejs/devtools-rolldown@307
npm i https://pkg.pr.new/@vitejs/devtools-self-inspect@307

commit: 0348334

antfu and others added 4 commits May 7, 2026 15:23
…uth: false

The browser client unconditionally calls `vite:anonymous:auth` on
connect (`client/rpc-ws.ts`), but devframe's standalone server never
registered a handler for it — so any non-Vite example (CLI / SPA
adapters) hit `[birpc] function "vite:anonymous:auth" not found` the
moment a panel opened. The `auth?: boolean` option on `startHttpAndWs`
was already wired but did nothing.

Now `auth: false` registers a small noop handler that auto-trusts the
session, satisfying the client's hardcoded handshake. Vite consumers
never opt into `auth: false`, so the real `vite:anonymous:auth`
registered by `@vitejs/devtools` is unaffected.

Also opt the `devframe-streaming-chat` example into `cli.auth: false`
so `pnpm run dev` works end-to-end out of the box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upgrades the streaming-chat example from a single-prompt scratch pad
into a real multi-turn chat:

- Conversation log lives in a devframe `sharedState` keyed
  `devframe-streaming-chat:history`. Each `send` action atomically
  appends a user message and an assistant placeholder; tokens stream
  live, and the joined content is committed back to shared state when
  the producer closes. Refresh the page and the log comes back; open
  a second panel and both stay in sync.
- New UI: chat-bubble layout with user / assistant styling, dark-mode
  CSS, demo-prompt chips, send / cancel / clear controls, auto-scroll
  on new messages, blinking cursor while streaming.
- Cancellation: `reader.cancel()` flows through to the producer, which
  commits the partial content with `cancelled: true`. The UI keeps the
  partial response visible.
- Replay: `replayWindow: 1024` means a panel reopened mid-stream sees
  the buffered tokens before resuming live.

Also adds RPC actions `chat:send` and `chat:clear`, and updates tests
to verify history grows across turns, cancellation persists partial
content, and clear empties the log. README covers running locally and
swapping in a real LLM via OpenAI's streaming API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four `devtoolskit:internal:rpc:server-state:*` RPC handlers
(`subscribe` / `get` / `set` / `patch`) were defined only in
`packages/core` and registered via core's builtin set. devframe's
client (`client/rpc-shared-state.ts`) hardcodes calls to those names,
so any non-Vite host (CLI / SPA / build adapters) hit
`[birpc] function "devtoolskit:internal:rpc:server-state:get" not
found` the moment a panel tried to read shared state.

Move the four handlers into `createRpcSharedStateServerHost` so any
`RpcFunctionsHost` ships with the full sync protocol, and add the
matching wire methods to `DevToolsRpcServerFunctions` so callers stay
typed. Delete the duplicate definitions from `packages/core`.

This is the same class of fix as the previous `vite:anonymous:auth`
patch — both wire methods are part of devframe's protocol but used to
live in the wrong package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add `docs/kit/streaming.md` — kit-flavored streaming guide covering
  outbound (`channel.start`, `pipeFrom`, `subscribe`), inbound uploads
  (`openInbound`, `rpc.streaming.upload`), lifecycle, replay,
  backpressure, and the chat-history pattern that combines streaming
  with shared state. Wired into `DevToolsKitNav` + main sidebar.
- Cross-link from `docs/kit/rpc.md` so readers landing on the function-
  type table see the streaming alternative.
- Update `skills/devframe/SKILL.md` with a Streaming Channels section
  (server↔client, lifecycle, Web/Node Streams interop, when-to-use
  matrix). Add the streaming guide to Further Reading.
- Update `skills/vite-devtools-kit/SKILL.md` with a Streaming Channels
  section + new `ctx.rpc.streaming` row in the context table; add a
  detailed `references/streaming-patterns.md` with the chat-history
  composition example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@antfu antfu merged commit 343d2c7 into main May 7, 2026
9 checks passed
@antfu antfu deleted the antfu/streaming-rpc branch May 7, 2026 07:05
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.

Guidance for streaming chat responses in DevTools integrations

1 participant