Skip to content

feat: add devframe/in-page-channel, server-free page script ↔ panel communication - #302

Merged
antfu merged 4 commits into
mainfrom
feat/in-page-channel
Aug 27, 2026
Merged

feat: add devframe/in-page-channel, server-free page script ↔ panel communication#302
antfu merged 4 commits into
mainfrom
feat/in-page-channel

Conversation

@antfubot

Copy link
Copy Markdown
Collaborator

What

A new core subpath, devframe/in-page-channel, giving every devframe a first-class, typed, server-free communication path between its page script (in the user app's page) and its panels (dock iframe, popup, Document PiP) — the third communication path named on the terms page, now with a real API. The a11y inspector is migrated onto it, deleting its hand-rolled BroadcastChannel protocol.

Why

Tools whose live loop is inspect the page (a11y's scan/highlight, pinia-colada-style store devtools) need the page script and panel to talk without a server, so the loop works identically in dev and static builds. Until now each tool hand-rolled this (a11y's ready/replay dance over an origin-wide BroadcastChannel, which also cross-talked between two tabs of the same app). This lands one well-defined primitive instead.

Design

  • Handshake: panel-initiated versioned hello (posted to its ancestor chain + opener, retried with backoff) answered by a dedicated MessageChannel port per panel — boot order never matters, reloads on either side just re-handshake, same-origin enforced both ways. A per-tab instance id (sessionStorage) scopes handshakes; panels can pin to one instance.
  • Functions: defineChannelFunction reuses the defineRpcFunction authoring shape (name, type, Standard-Schema args/returns, jsonSerializable, handler/setup) narrowed to the browser. type: 'event' fans out to every panel; query/action are request/response (birpc); peer handles give targeted page-script→panel calls plus presence.
  • State: channel.sharedState.get(key) mirrors rpc.sharedState — same SharedState<T> handle — with the page script as authority: automatic replay to late-joining panels, syncId-deduplicated patches.
  • Errors & fallbacks: coded InPageChannelErrors (timeout with status hints, closed, not-serializable naming the offending path, not-cloneable, invalid-args, state-uninitialized); an explicit connecting/connected/closed status machine with call queueing + bounded event buffering; whenConnected(ms) for "page script not loaded" fallback UIs; automatic recovery via port close events + heartbeat.
  • Transport-neutral core: the envelope carries protocol/version/instance ids and every port goes through one seam, so a future cross-tab transport (e.g. BroadcastChannel) slots in behind the same API; { transport: MessagePort } bypasses discovery for tests and custom topologies.

a11y migration

shared/protocol.ts is now a pure contract (A11yChannelProtocol + constants); the page script registers channel functions and owns A11yState as channel shared state (scanning folded in); the Solid panel mirrors it — the startup-race and replay code is gone, and the two-tabs cross-talk bug with it. Verified end-to-end in a browser against the demo (handshake, state replay, pinning).

Docs

New guide (/guide/in-page-channel) including the error/fallback surface and an in-page-channel-vs-RPC table; events reference and terms page updated in lockstep with DEVFRAME_EVENTS; devframe skill updated.

Gate: pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build all green (API snapshots regenerated).


This PR was created with the help of an agent.

…communication

One typed, same-origin channel between a devframe's page script and its
panels (dock iframe, popup, Document PiP), with no server involved:

- panel-initiated postMessage handshake granting a dedicated MessageChannel
  port per panel — boot order never matters, reloads re-handshake, and a
  per-tab instance id keeps multi-tab apps from cross-talking
- functions authored with defineChannelFunction (the defineRpcFunction
  shape narrowed to the browser); type 'event' fans out to every panel,
  query/action are request/response via birpc
- a shared-state layer mirroring rpc.sharedState with the page script as
  authority: automatic replay to late-joining panels, syncId-deduplicated
  patches
- explicit fallback/error surface: coded InPageChannelError (timeout,
  closed, not-serializable, not-cloneable, invalid-args,
  state-uninitialized), connecting/connected/closed status with buffering,
  whenConnected(ms) for no-page-script fallbacks, heartbeat + port-close
  recovery

The a11y inspector's page script and panel now ride this channel instead
of their hand-rolled BroadcastChannel protocol, which also fixes the
two-tabs cross-talk it had.
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
devframe Ready Ready Preview Aug 27, 2026 2:09am

@antfu antfu changed the title feat: add devframe/in-page-channel — server-free page script ↔ panel communication feat: add devframe/in-page-channel, server-free page script ↔ panel communication Aug 27, 2026
Public API down to 13 exports (4 runtime + 9 types): drop the speculative
setup() hook, post-creation register(), PanelPeer.callEvent, the page
script's creation-time transport option (addPanelPort covers it), and stop
exporting wire internals (handshake tag/version/message types,
defaultHandshakeTargets) and derived type helpers. errors/registry/port
merge into one internal module; defineChannelFunction moves into the index.
@antfu
antfu merged commit 87dafe9 into main Aug 27, 2026
11 checks passed
@antfu
antfu deleted the feat/in-page-channel branch August 27, 2026 02:20
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