Skip to content

feat(devframe): async generator RPC functions#312

Closed
antfu wants to merge 1 commit into
mainfrom
antfu/async-generator-rpc
Closed

feat(devframe): async generator RPC functions#312
antfu wants to merge 1 commit into
mainfrom
antfu/async-generator-rpc

Conversation

@antfu
Copy link
Copy Markdown
Member

@antfu antfu commented May 7, 2026

Description

Adds type: 'generator' to defineRpcFunction so handlers declared as async function* stream their yields to the client without manual channel scaffolding. The framework substitutes the user's definition with an internal action wrapper that allocates a sink on a hidden devframe:rpc:generators channel and returns an envelope; the client wrapper unwraps it and streaming.subscribe()s automatically, so await rpc.call(...) resolves to a ready-to-iterate StreamReader<Y>. Cancellation flows through getCurrentRpcStream() — an AsyncLocalStorage helper that mirrors getCurrentRpcSession() — and invokeLocalGenerator(rpc, name, ...args) lets server-side callers iterate without paying for the streaming round-trip. Generator definitions reject agent, cacheable, dump, snapshot, and jsonSerializable: true at registration via four new diagnostics (DF0033–DF0036). Also fixes a pre-existing replay bug in node/rpc-streaming.ts: when a producer closed with an error and a subscriber arrived during the closedStreamRetention window, the late subscriber received a clean close instead of the original error — the streaming record now captures and replays the end payload.

Linked Issues

Follow-up to #307. Builds the higher-level ergonomic surface that the streaming-channel API was designed to enable.

Additional context

  • New rpc-generators.test.ts adds 12 integration tests covering happy path, cooperative cancel via getCurrentRpcStream().signal, throw mid-stream, throw before first yield, late-subscriber replay, concurrent isolation, invokeLocalGenerator, and all four validation diagnostics.
  • Streaming guide gains an "Async Generator RPC" section ahead of the manual channel section, framed as the recommended path. Both skills/devframe and skills/vite-devtools-kit get matching guidance.
  • devframe-streaming-chat example adds a :tokenize generator alongside the existing :send action so readers can compare both approaches.
  • Per-stream replayWindow defaults to 256 (floored to 1) to win the client-subscribe-vs-first-yield race; this required threading RpcStreamingChannel.start() to accept a per-stream override on top of the channel default.
  • Out of scope (deferred): streaming-MCP / agent exposure for generators, bidirectional generators, generator return values.
  • Pre-PR checklist (lint + 462 tests + typecheck + build) all green.

🤖 Generated with Claude Code

Add `type: 'generator'` for `defineRpcFunction` so handlers declared as
`async function*` stream their yields to the caller without manual
channel scaffolding. The framework substitutes the user's definition with
an internal action wrapper that allocates a sink on a hidden
`devframe:rpc:generators` channel and returns a stream-id envelope; the
client wrapper unwraps the envelope and `streaming.subscribe()`s
automatically, so `await rpc.call(name, args)` resolves to a ready-to-iterate
`StreamReader<Y>`. Cancellation flows through `getCurrentRpcStream()` —
an AsyncLocalStorage helper that mirrors `getCurrentRpcSession()` and
exposes the sink's `signal`, `streamId`, and originating session inside
the handler body. Server-side callers can iterate without paying for the
streaming round-trip via `invokeLocalGenerator(rpc, name, ...args)`.

Per-stream `replayWindow` defaults to 256 (floored to 1) to win the
client-subscribe-vs-first-yield race; this required threading the
existing `RpcStreamingChannel.start()` helper to accept a per-stream
override on top of the channel default. Generator definitions reject
`agent`, `cacheable`, `dump`, `snapshot`, and `jsonSerializable: true` at
registration via four new diagnostics (DF0033–DF0036). DF0034 fires
at runtime if the handler doesn't return an `AsyncIterable`.

Also fixes a pre-existing replay bug in `node/rpc-streaming.ts`: when a
producer closed with an error and a subscriber arrived during the
`closedStreamRetention` window, the late subscriber received a clean
close instead of the original error. The streaming record now captures
the end payload and replays it on subscribe.

Migrations & dogfooding:
- Streaming guide gains an "Async Generator RPC" section ahead of the
  manual channel section, framed as the recommended path.
- `devframe-streaming-chat` example adds a `:tokenize` generator
  alongside the existing `:send` action so readers can compare both
  approaches in one place.
- `skills/devframe` and `skills/vite-devtools-kit` get matching guidance.
- 12 new integration tests (`rpc-generators.test.ts`) covering happy
  path, cooperative cancel, throw mid-stream, throw before first yield,
  late-subscriber replay, concurrent isolation, `invokeLocalGenerator`,
  and all four validation diagnostics. tsnapi snapshots updated for the
  new public exports (`getCurrentRpcStream`, `invokeLocalGenerator`,
  `attachRpcGenerators`, `RpcGeneratorStreamContext`,
  `RpcGeneratorFunctionDefinition`).

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@antfu antfu closed this May 7, 2026
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.

1 participant