-
Notifications
You must be signed in to change notification settings - Fork 15
fix: separate in-page channel events from functions #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e141e51
b05f009
4dad315
c2f2bf5
003dbac
d00bf1b
c8dcb6b
8f02d5c
9894d40
206ac8e
fbccf28
ecb850e
ce7ddbd
a20931f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,14 +37,22 @@ import type { InPageChannelProtocol } from 'devframe/in-page-channel' | |
| export const MY_CHANNEL = 'devframes:plugin:my-tool' | ||
|
|
||
| export interface MyChannelProtocol extends InPageChannelProtocol { | ||
| /** implemented by the page script, callable by panels */ | ||
| pageScript: { | ||
| highlight: (selector: string) => void | ||
| measure: (selector: string) => { width: number, height: number } | ||
| functions: { | ||
| /** implemented by the page script, callable by panels */ | ||
| pageScript: { | ||
| measure: (selector: string) => { width: number, height: number } | ||
| reset: () => Promise<void> | ||
| } | ||
| /** implemented by panels, callable by the page script */ | ||
| panel: { | ||
| echo: (message: string) => Promise<string> | ||
| } | ||
| } | ||
| /** implemented by panels, callable by the page script */ | ||
| panel: { | ||
| flash: (message: string) => void | ||
| events: { | ||
| /** listened to by the page script, emitted by panels */ | ||
| pageScript: { highlight: (selector: string) => void } | ||
| /** listened to by panels, emitted by the page script */ | ||
| panel: { flash: (message: string) => void } | ||
| } | ||
| sharedStates: { | ||
| state: { selections: string[] } | ||
|
|
@@ -56,7 +64,9 @@ Channel names are namespaced with the devframe id, like RPC ids. Function names | |
|
|
||
| ## The page script endpoint | ||
|
|
||
| The required `functions` object declares every function on that endpoint's protocol side, preserving a compile-time completeness check. Request/response declarations require a `handler`; an event declaration uses `type: 'event'`, and the receiving endpoint may provide an optional `handler` or subscribe at runtime with `on()`. Functions use the same Standard-Schema `args`/`returns` and `jsonSerializable` metadata as `defineRpcFunction`, narrowed to the browser. Each handler is contextually typed from its key and the corresponding protocol function. `defineChannelFunction` retains the named definition shape for lower-level authoring. Define each side's functions in that side's source files; the shared protocol file carries only types. | ||
| The required `functions` option and optional `events` option declare every incoming name on the endpoint's protocol side; use `{}` for an empty direction. Functions require a `handler`. Events accept an optional `handler`, and `{}` registers an event for runtime subscriptions through `on()`. Handlers are contextually typed from the shared protocol and support Standard-Schema argument validation and `jsonSerializable` metadata. `defineChannelFunction` retains the named definition shape for lower-level authoring. | ||
|
|
||
| `call()` accepts names from `functions`, including actions returning `void` or `Promise<void>`: callers can await completion and catch errors or timeouts. `emit()`, its deprecated alias `callEvent()`, and `on()` use the names declared in `events`. Function and event names have separate namespaces. | ||
|
|
||
| ```ts | ||
| import type { MyChannelProtocol } from '../shared/protocol' | ||
|
|
@@ -67,26 +77,28 @@ import { MY_CHANNEL } from '../shared/protocol' | |
| const pageChannel = createPageScriptChannel<MyChannelProtocol>({ | ||
| name: MY_CHANNEL, | ||
| functions: { | ||
| highlight: { | ||
| type: 'event', // fire-and-forget | ||
| jsonSerializable: true, | ||
| handler: selector => drawRing(document.querySelector(selector)), | ||
| }, | ||
| reset: { type: 'action', handler: async () => clearSelections() }, | ||
| measure: { // request/response (the default `query` type) | ||
| handler: (selector) => { | ||
| const rect = document.querySelector(selector)!.getBoundingClientRect() | ||
| return { width: rect.width, height: rect.height } | ||
| }, | ||
| }, | ||
| }, | ||
| events: { | ||
| highlight: { | ||
| jsonSerializable: true, | ||
| handler: selector => drawRing(document.querySelector(selector)), | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
| pageChannel.emit('flash', 'scanning…') // received by each panel endpoint | ||
| pageChannel.events.on('panel:connected', panel => console.log(panel.id)) | ||
| pageChannel.events.on('panel:disconnected', () => pauseWorkIfNobodyWatches()) | ||
| ``` | ||
|
|
||
| `emit` on the page-script endpoint is 1:N: it fans out to every connected panel endpoint. Request/response *to* a panel goes through an explicit peer handle: `pageChannel.panels[0].call('flash', '…')`. | ||
| `emit` on the page-script endpoint fans out to every connected panel endpoint. Functions declared under `functions.panel` are called through a specific `pageChannel.panels[0].call()` peer handle. | ||
|
|
||
| ## The panel endpoint | ||
|
|
||
|
|
@@ -98,14 +110,17 @@ import { MY_CHANNEL } from '../shared/protocol' | |
|
|
||
| const panelChannel = connectPanelChannel<MyChannelProtocol>({ | ||
| name: MY_CHANNEL, | ||
| functions: { | ||
| flash: { type: 'event' }, | ||
| functions: {}, | ||
| events: { | ||
| flash: {}, | ||
| }, | ||
| }) | ||
|
|
||
| const offFlash = panelChannel.on('flash', message => showFlash(message)) | ||
| panelChannel.emit('highlight', '.hero') // received by the page-script endpoint | ||
| // defined and received by the page-script endpoint | ||
| panelChannel.emit('highlight', '.hero') | ||
| const size = await panelChannel.call('measure', '.hero') | ||
| await panelChannel.call('reset') | ||
|
|
||
| offFlash() // stop listening | ||
| ``` | ||
|
|
@@ -162,6 +177,8 @@ import { toRaw } from 'vue' | |
| const channel = connectPanelChannel<MyChannelProtocol>({ | ||
| name: MY_CHANNEL, | ||
| serialize: value => toRawDeep(value), // applied to every outgoing argument and result | ||
| functions: {}, | ||
| events: { flash: {} }, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this should just be optional
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right now used to diagnose missing events when emitted, so I feel like it can be useful to diagnose wrong names https://github.com/devframes/devframe/pull/371/changes#diff-c8bbf4963ef4b0c4ae6cc7164bfa1da31233ea66fdb9cda6b4b1c98568f62aa5R7
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ended up making it optional, i feel like it makes more sense for events and they are already type safe (although we do accept any string with |
||
| }) | ||
| ``` | ||
|
|
||
|
|
@@ -172,7 +189,7 @@ Declaring a function `jsonSerializable: true` additionally enforces strict JSON | |
| The same app open in two tabs means two page scripts on one origin. Each page script carries a per-tab instance id (persisted in `sessionStorage`), and handshakes are targeted `postMessage`, so a dock panel always pairs with its own tab's page script. A panel can also pin explicitly: | ||
|
|
||
| ```ts | ||
| connectPanelChannel<MyChannelProtocol>({ name: MY_CHANNEL, instanceId }) | ||
| connectPanelChannel<MyChannelProtocol>({ name: MY_CHANNEL, instanceId, functions: {}, events: { flash: {} } }) | ||
| ``` | ||
|
|
||
| ## Custom transports | ||
|
|
@@ -182,7 +199,7 @@ Both endpoints accept a pre-established `MessagePort` that bypasses the handshak | |
| ```ts | ||
| const { port1, port2 } = new MessageChannel() | ||
| pageScript.addPanelPort(port1) | ||
| const panel = connectPanelChannel<MyChannelProtocol>({ name: MY_CHANNEL, transport: port2 }) | ||
| const panel = connectPanelChannel<MyChannelProtocol>({ name: MY_CHANNEL, transport: port2, functions: {}, events: { flash: {} } }) | ||
| ``` | ||
|
|
||
| ## When to use the in-page channel vs RPC | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import type { PageScriptChannel, PanelChannel } from './types' | ||
| import { expectTypeOf, it } from 'vitest' | ||
|
|
||
| interface Protocol { | ||
| functions: { | ||
| pageScript: { save: (value: string) => void, reset: () => Promise<void> } | ||
| panel: { save: (value: string) => void, reset: () => Promise<void> } | ||
| } | ||
| events: { | ||
| pageScript: { note: (value: string, count?: number) => void } | ||
| panel: { notify: (message: string) => void } | ||
| } | ||
| } | ||
|
|
||
| declare const pageScript: PageScriptChannel<Protocol> | ||
| declare const panel: PanelChannel<Protocol> | ||
|
|
||
| it('distinguishes void actions from declared events in both directions', () => { | ||
| expectTypeOf(panel.call('save', 'draft')).toEqualTypeOf<Promise<void>>() | ||
| expectTypeOf(panel.call('reset')).toEqualTypeOf<Promise<void>>() | ||
| const peer = pageScript.panels[0]! | ||
| expectTypeOf(peer.call('save', 'draft')).toEqualTypeOf<Promise<void>>() | ||
| expectTypeOf(peer.call('reset')).toEqualTypeOf<Promise<void>>() | ||
| expectTypeOf(panel.emit('note', 'hello', 2)).toEqualTypeOf<void>() | ||
| expectTypeOf(pageScript.emit('notify', 'hello')).toEqualTypeOf<void>() | ||
| expectTypeOf(pageScript.on('note', (value, count) => { | ||
| expectTypeOf(value).toEqualTypeOf<string>() | ||
| expectTypeOf(count).toEqualTypeOf<number | undefined>() | ||
| })).toEqualTypeOf<() => void>() | ||
| // @ts-expect-error Events cannot be called as functions. | ||
| panel.call('note', 'hello') | ||
| // @ts-expect-error Events cannot be called on panel peers. | ||
| peer.call('notify', 'hello') | ||
| // @ts-expect-error A void action is still a function. | ||
| panel.emit('save', 'draft') | ||
| // @ts-expect-error An asynchronous void action is still a function. | ||
| panel.emit('reset') | ||
| // @ts-expect-error The deprecated alias has the same restriction. | ||
| panel.callEvent('save', 'draft') | ||
| // @ts-expect-error A panel void action is still a function. | ||
| pageScript.emit('save', 'draft') | ||
| // @ts-expect-error A panel asynchronous void action is still a function. | ||
| pageScript.callEvent('reset') | ||
| // @ts-expect-error Functions cannot receive event listeners. | ||
| pageScript.on('save', () => {}) | ||
| // @ts-expect-error Functions cannot receive event listeners. | ||
| panel.on('reset', () => {}) | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.