feat: add runtime in-page channel events - #358
Conversation
◈ PR Lens
Architecture 5 components touched across 2 lanes. Inside the changed components — 1 viewComponent view — In-Page Channel Endpoints and Registry Internal modules powering emit/on messaging and dynamic listener registration across page script and panel endpoints. Data flow
The other flows — 1 sequence
Drill down
|
|
@posva is attempting to deploy a commit to the NuxtLabs Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
🟡 Changes recommended
defineChannelFunction() can unintentionally allow missing handlers when type is omitted due to generic inference, weakening type safety for the helper API.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds runtime event subscription and a clearer emission API to the in-page channel so panels/page scripts can dynamically subscribe/unsubscribe to in-page events, while keeping compatibility with the existing callEvent() API.
Changes:
- Introduces typed
channel.emit()plus runtimechannel.on()(returning an unsubscribe function) on both page-script and panel endpoints; keepscallEvent()as a deprecated alias. - Allows
type: 'event'function declarations to omithandler, enabling purely runtime-driven listeners. - Updates tests, type tests, docs, and the a11y devframe to use
emit()and validate the new runtime subscription behavior.
File summaries
| File | Description |
|---|---|
| tests/snapshots/tsnapi/devframe/in-page-channel.snapshot.d.ts | Updates public type snapshots to include emit()/on() and relaxed event handler requirements. |
| plugins/a11y/app/lib/channel.ts | Migrates a11y panel-side event sending from callEvent() to emit(). |
| packages/devframe/src/in-page-channel/types.ts | Updates public types/docs for protocol semantics; enables event declarations without handlers; adds emit()/on() to endpoint interfaces. |
| packages/devframe/src/in-page-channel/types.test-d.ts | Adds type-level tests for event-without-handler and runtime on() typing. |
| packages/devframe/src/in-page-channel/panel.ts | Adds emit() and on() to the panel endpoint implementation; makes functions required at runtime. |
| packages/devframe/src/in-page-channel/page-script.ts | Adds emit() and on() to the page-script endpoint implementation; deprecates callEvent() to alias emit(). |
| packages/devframe/src/in-page-channel/internal.ts | Extends the local registry to support runtime listeners via on() and resolves handlers accordingly. |
| packages/devframe/src/in-page-channel/index.ts | Adjusts defineChannelFunction generics (ARGS default). |
| packages/devframe/src/in-page-channel/in-page-channel.test.ts | Updates runtime tests to validate emit() + runtime subscription/unsubscribe behavior. |
| docs/content/8.references/5.browser-api.md | Adds an in-page channel endpoint API reference table. |
| docs/content/1.guide/12.in-page-channel.md | Updates guide examples and wording to use emit() + runtime on() subscriptions. |
Review details
Suppressed comments (1)
packages/devframe/src/in-page-channel/index.ts:40
defineChannelFunction()leavesTYPEwithout a default, so calls that omittypecan inferTYPEas the fullInPageFunctionTypeunion. With the new conditional handler optionality for events, that can accidentally makehandleroptional (e.g.defineChannelFunction({ name: 'x' })becomes type-valid), weakening the helper’s compile-time safety.
export function defineChannelFunction<
NAME extends string,
TYPE extends InPageFunctionType,
ARGS extends any[] = [],
RETURN = void,
const AS extends RpcArgsSchema | undefined = undefined,
const RS extends RpcReturnSchema | undefined = undefined,
>(
- Files reviewed: 10/11 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| | `emit(name, ...args)` | Fans an event out to every connected panel. | Sends an event to the page script, buffering while connecting. | | ||
| | `on(name, listener)` | Subscribes to events emitted by a panel. | Subscribes to events emitted by the page script. Returns an unsubscribe function. | | ||
| | `call(name, ...args)` | Available through a specific `PanelPeer`. | Calls a page-script function and awaits its result. | |
| NAME extends string, | ||
| TYPE extends InPageFunctionType, | ||
| ARGS extends any[], | ||
| ARGS extends any[] = [], |
There was a problem hiding this comment.
needed because handler is optional if type is event
There was a problem hiding this comment.
🟡 Changes recommended
The new runtime listener registry currently allows subscribing to undeclared method names and can silently treat missing non-event handlers as no-ops, which risks bypassing intended validation and masking misconfiguration.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
packages/devframe/src/in-page-channel/page-script.ts:67
options.functionsis assumed present at runtime, but if a non-TypeScript consumer passes{}/ omits it,Object.entries(options.functions)will throw a low-signal error. A small runtime assertion would make misconfiguration easier to diagnose.
packages/devframe/src/in-page-channel/panel.ts:66options.functionsis now required by the types, but at runtime (JS usage)Object.entries(options.functions)will throw a genericCannot convert undefined or null to objectif it’s missing. Adding an explicit runtime assertion here would produce a clearer error for consumers who aren’t typechecked.
- Files reviewed: 10/11 changed files
- Comments generated: 2
- Review effort level: Lite
| on(name, listener) { | ||
| let registered = listeners.get(name) | ||
| if (!registered) { | ||
| registered = new Set() | ||
| listeners.set(name, registered) | ||
| } | ||
| registered.add(listener) | ||
| return () => { |
| if (definition?.args?.length) | ||
| await validateArgs(definition.name, definition.args, args) | ||
| const result = await definition.handler(...args) | ||
| if (definition.jsonSerializable) | ||
| const result = await definition?.handler?.(...args) | ||
| for (const listener of [...(listeners.get(name) ?? [])]) | ||
| listener(...args) |
| ...args: FnArgs<PanelFunctions<P>[K]> | ||
| ) => void | ||
| /** Subscribe to an event emitted by a panel. Returns an unsubscribe function. */ | ||
| on: <K extends keyof PageScriptFunctions<P> & string>( |
There was a problem hiding this comment.
this is also wrong: it's probably allowing too many, it should only allow those of type: 'event'
|
I'm realizing some stuff is still not good enough, so marking as draft |
Problem
Event handlers could only be installed on the receiving endpoint through its initial
functionsoption, preventing runtime subscription and cleanup. The sending endpoint also exposedcallEvent(), which reads like an RPC call and obscures that events cross endpoints: page-script emission is handled by a panel, and panel emission is handled by the page script.Change
emit()/on();on()returns an unsubscribe function.type: 'event'declarations to omithandler.channel.eventsand retaincallEvent()as a deprecated alias.Tested with in-page channel tests, package typechecks, build, and API snapshots.