feat(devframe): streaming channel API for server↔client chunks#307
Merged
Conversation
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>
commit: |
…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>
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.
Description
Adds a first-class streaming primitive —
ctx.rpc.streaming(server) andrpc.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 imperativewrite/close/errorplus aWritableStream<T>forpipeTo;channel.pipeFrom(readable)is a one-call shorthand.rpc.streaming.subscribe(channel, id)returns a reader that's bothAsyncIterable<T>and exposesreadable: ReadableStream<T>.replayWindowkeeps a per-stream ring buffer; reconnecting subscribers resume fromafterSeq.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 whosesignalaborts when the server cancels. Each inbound is owned by one session — no fan-in, no shared state; client disconnect surfaces asUploadDisconnectedto the consumingfor await.Migration & dogfooding:
DevToolsTerminalHostnow pipes session output through the streaming channeldevtoolskit:internal:terminals(the legacyterminal:session:stream-chunkhost event is gone). Newdevframe-streaming-chatexample with a Preact UI and four end-to-end tests (happy path, cancel, fan-out, replay). Four new structured diagnostics (DF0029–0032), full guide atdocs/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.tsis now wired sogetCurrentRpcSession()works in the standalone-CLI flavour — required for streaming subscribe/cancel and a latent gap before._emitSessionDisconnectedis added toRpcFunctionsHostso adapters can clean up per-session subscribers on WS close (wired in both devframe'sserver.tsandpackages/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