Skip to content

Commit 91178a5

Browse files
committed
refactor: require in-page channel functions
1 parent 4e1634b commit 91178a5

5 files changed

Lines changed: 61 additions & 20 deletions

File tree

docs/content/1.guide/12.in-page-channel.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Channel names are namespaced with the devframe id, like RPC ids. Function names
5454

5555
## The page script endpoint
5656

57-
Functions use the same authoring metadata as `defineRpcFunction` (`type`, Standard-Schema `args`/`returns`, `jsonSerializable`, `handler`), narrowed to the browser. The `functions` object's keys are the function names, and every function on that endpoint's protocol side is required when the object is provided. Each handler is contextually typed from its key and the corresponding function in the protocol. `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.
57+
Functions use the same authoring metadata as `defineRpcFunction` (`type`, Standard-Schema `args`/`returns`, `jsonSerializable`, `handler`), narrowed to the browser. The required `functions` object's keys are the function names, and it implements every function on that endpoint's protocol side. Each handler is contextually typed from its key and the corresponding function in the protocol. `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.
5858

5959
```ts
6060
import type { MyChannelProtocol } from '../shared/protocol'
@@ -94,7 +94,14 @@ import type { MyChannelProtocol } from '../shared/protocol'
9494
import { connectPanelChannel } from 'devframe/in-page-channel'
9595
import { MY_CHANNEL } from '../shared/protocol'
9696

97-
const channel = connectPanelChannel<MyChannelProtocol>({ name: MY_CHANNEL })
97+
const channel = connectPanelChannel<MyChannelProtocol>({
98+
name: MY_CHANNEL,
99+
functions: {
100+
flash: {
101+
handler: message => showFlash(message),
102+
},
103+
},
104+
})
98105

99106
channel.callEvent('highlight', '.hero') // buffered until connected
100107
const size = await channel.call('measure', '.hero')

packages/devframe/src/in-page-channel/in-page-channel.test.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function createLinkedPair(options?: {
7373
name: 'devframes:test',
7474
...noHandshake,
7575
transport: port2,
76+
functions: defaultPanelFunctions,
7677
...options?.panel,
7778
})
7879
return {
@@ -167,6 +168,7 @@ describe('in-page channel over bring-your-own ports', () => {
167168
name: 'devframes:test',
168169
...noHandshake,
169170
transport: port2,
171+
functions: defaultPanelFunctions,
170172
})
171173
try {
172174
await expect(panel.call('note', 'fine')).resolves.toBeUndefined()
@@ -185,6 +187,7 @@ describe('in-page channel over bring-your-own ports', () => {
185187
const pageScript = createPageScriptChannel<TestProtocol>({
186188
name: 'devframes:test',
187189
...noHandshake,
190+
functions: defaultPageScriptFunctions,
188191
})
189192
pageScript.addPanelPort(a.port1)
190193
pageScript.addPanelPort(b.port1)
@@ -200,11 +203,12 @@ describe('in-page channel over bring-your-own ports', () => {
200203
} },
201204
},
202205
})
203-
// Panel B deliberately implements nothing.
204-
const panelB = connectPanelChannel<TestProtocol>({
206+
// Panel B deliberately has no local functions in its protocol.
207+
const panelB = connectPanelChannel<InPageChannelProtocol>({
205208
name: 'devframes:test',
206209
...noHandshake,
207210
transport: b.port2,
211+
functions: {},
208212
})
209213
try {
210214
expect(pageScript.panels).toHaveLength(2)
@@ -224,6 +228,7 @@ describe('in-page channel over bring-your-own ports', () => {
224228
const pageScript = createPageScriptChannel<TestProtocol>({
225229
name: 'devframes:test',
226230
...noHandshake,
231+
functions: defaultPageScriptFunctions,
227232
})
228233
pageScript.addPanelPort(port1)
229234
const panel = connectPanelChannel<TestProtocol>({
@@ -254,6 +259,7 @@ describe('in-page channel over bring-your-own ports', () => {
254259
name: 'devframes:test',
255260
...noHandshake,
256261
transport: port2,
262+
functions: defaultPanelFunctions,
257263
// Unwrap a fake reactivity wrapper on the way out, tag on the way in.
258264
serialize: value => (value && typeof value === 'object' && '__wrapped' in (value as any))
259265
? (value as any).__wrapped
@@ -274,6 +280,7 @@ describe('in-page channel over bring-your-own ports', () => {
274280
const pageScript = createPageScriptChannel<TestProtocol>({
275281
name: 'devframes:test',
276282
...noHandshake,
283+
functions: defaultPageScriptFunctions,
277284
})
278285
const connected: string[] = []
279286
const disconnected: string[] = []
@@ -284,6 +291,7 @@ describe('in-page channel over bring-your-own ports', () => {
284291
name: 'devframes:test',
285292
...noHandshake,
286293
transport: port2,
294+
functions: defaultPanelFunctions,
287295
})
288296
try {
289297
expect(connected).toHaveLength(1)
@@ -324,11 +332,12 @@ describe('in-page channel shared state', () => {
324332
const pageScript = createPageScriptChannel<TestProtocol>({
325333
name: 'devframes:test',
326334
...noHandshake,
335+
functions: defaultPageScriptFunctions,
327336
})
328337
pageScript.addPanelPort(a.port1)
329338
pageScript.addPanelPort(b.port1)
330-
const panelA = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: a.port2 })
331-
const panelB = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: b.port2 })
339+
const panelA = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: a.port2, functions: defaultPanelFunctions })
340+
const panelB = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: b.port2, functions: defaultPanelFunctions })
332341
try {
333342
const authority = await pageScript.sharedState.get('doc', { initialValue: { count: 0 } })
334343
const mirrorA = await panelA.sharedState.get('doc')
@@ -349,7 +358,7 @@ describe('in-page channel shared state', () => {
349358

350359
it('seeds a late-joining panel with the current value', async () => {
351360
const { port1, port2 } = new MessageChannel()
352-
const pageScript = createPageScriptChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake })
361+
const pageScript = createPageScriptChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, functions: defaultPageScriptFunctions })
353362
const authority = await pageScript.sharedState.get('doc', { initialValue: { count: 0 } })
354363
authority.mutate((draft) => {
355364
draft.count = 41
@@ -359,7 +368,7 @@ describe('in-page channel shared state', () => {
359368
})
360369

361370
pageScript.addPanelPort(port1)
362-
const panel = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: port2 })
371+
const panel = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: port2, functions: defaultPanelFunctions })
363372
try {
364373
const mirror = await panel.sharedState.get('doc')
365374
expect(mirror.value()).toEqual({ count: 42 })
@@ -458,6 +467,7 @@ describe('in-page channel handshake', () => {
458467
window: panelWin as unknown as Window,
459468
targets: [hostWin as unknown as Window],
460469
...fastHello,
470+
functions: defaultPanelFunctions,
461471
})
462472
try {
463473
await panel.whenConnected(2000)
@@ -500,6 +510,7 @@ describe('in-page channel handshake', () => {
500510
window: panelWin as unknown as Window,
501511
targets: [hostWin as unknown as Window],
502512
...fastHello,
513+
functions: defaultPanelFunctions,
503514
})
504515
const early = panel.call('echo', 'early')
505516
panel.callEvent('note', 'buffered')
@@ -533,6 +544,7 @@ describe('in-page channel handshake', () => {
533544
name: 'devframes:test-origin',
534545
window: hostWin as unknown as Window,
535546
heartbeat: false,
547+
functions: defaultPageScriptFunctions,
536548
})
537549
try {
538550
hostWin.__dispatch({
@@ -563,6 +575,7 @@ describe('in-page channel handshake', () => {
563575
name: 'devframes:test-version',
564576
window: hostWin as unknown as Window,
565577
heartbeat: false,
578+
functions: defaultPageScriptFunctions,
566579
})
567580
try {
568581
hostWin.__dispatch({
@@ -592,13 +605,15 @@ describe('in-page channel handshake', () => {
592605
name: 'devframes:test',
593606
window: hostWin as unknown as Window,
594607
heartbeat: false,
608+
functions: defaultPageScriptFunctions,
595609
})
596610
const pinnedElsewhere = connectPanelChannel<TestProtocol>({
597611
name: 'devframes:test',
598612
window: panelWin as unknown as Window,
599613
targets: [hostWin as unknown as Window],
600614
instanceId: 'some-other-tab',
601615
...fastHello,
616+
functions: defaultPanelFunctions,
602617
})
603618
try {
604619
await expect(pinnedElsewhere.whenConnected(100)).rejects.toMatchObject({ code: 'timeout' })
@@ -609,6 +624,7 @@ describe('in-page channel handshake', () => {
609624
targets: [hostWin as unknown as Window],
610625
instanceId: pageScript.instanceId,
611626
...fastHello,
627+
functions: defaultPanelFunctions,
612628
})
613629
try {
614630
await pinnedHere.whenConnected(2000)
@@ -629,6 +645,7 @@ describe('in-page channel handshake', () => {
629645
name: `devframes:test-lonely-${Math.random()}`,
630646
window: false,
631647
heartbeat: false,
648+
functions: defaultPanelFunctions,
632649
})
633650
try {
634651
expect(lonely.status).toBe('connecting')
@@ -647,6 +664,7 @@ describe('in-page channel handshake', () => {
647664
window: false,
648665
heartbeat: false,
649666
callTimeoutMs: 50,
667+
functions: defaultPanelFunctions,
650668
})
651669
try {
652670
const rejection = await lonely.call('echo', 'nobody').catch(error => error)
@@ -664,6 +682,7 @@ describe('in-page channel handshake', () => {
664682
name: `devframes:test-lonely-${Math.random()}`,
665683
window: false,
666684
heartbeat: false,
685+
functions: defaultPanelFunctions,
667686
})
668687
const pending = lonely.call('echo', 'never')
669688
const waiting = lonely.whenConnected()

packages/devframe/src/in-page-channel/types.test-d.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ describe('in-page channel function definitions', () => {
5959
})
6060

6161
it('requires every function from the local protocol side', () => {
62+
// @ts-expect-error `functions` is required.
63+
createPageScriptChannel<TestProtocol>({ name: 'devframes:test' })
64+
// @ts-expect-error `functions` is required.
65+
connectPanelChannel<TestProtocol>({ name: 'devframes:test' })
66+
6267
createPageScriptChannel<TestProtocol>({
6368
name: 'devframes:test',
6469
// @ts-expect-error `sum` and `save` are required.
@@ -97,9 +102,16 @@ describe('in-page channel function definitions', () => {
97102
})
98103

99104
describe('page-script channel types', () => {
100-
it('types calls to panel functions', () => {
101-
const channel = createPageScriptChannel<TestProtocol>({ name: 'devframes:test' })
105+
const channel = createPageScriptChannel<TestProtocol>({
106+
name: 'devframes:test',
107+
functions: {
108+
echo: { handler: value => value },
109+
sum: { handler: (a, b) => a + b },
110+
save: { handler: () => {} },
111+
},
112+
})
102113

114+
it('types calls to panel functions', () => {
103115
expectTypeOf(channel.callEvent('notify', 'ready')).toEqualTypeOf<void>()
104116

105117
// @ts-expect-error Page-script functions cannot be called on panels.
@@ -113,7 +125,6 @@ describe('page-script channel types', () => {
113125
})
114126

115127
it('types connected panel peers and their calls', () => {
116-
const channel = createPageScriptChannel<TestProtocol>({ name: 'devframes:test' })
117128
const unsubscribe = channel.events.on('panel:connected', (panel) => {
118129
expectTypeOf(panel.id).toEqualTypeOf<string>()
119130
expectTypeOf(panel.call('notify', 'ready')).toEqualTypeOf<Promise<void>>()
@@ -129,7 +140,6 @@ describe('page-script channel types', () => {
129140
})
130141

131142
it('types disconnected panel peers and one-time listeners', () => {
132-
const channel = createPageScriptChannel<TestProtocol>({ name: 'devframes:test' })
133143
const unsubscribe = channel.events.once('panel:disconnected', (panel) => {
134144
expectTypeOf(panel.id).toEqualTypeOf<string>()
135145
expectTypeOf(panel.call('notify', 'bye')).toEqualTypeOf<Promise<void>>()
@@ -143,9 +153,14 @@ describe('page-script channel types', () => {
143153
})
144154

145155
describe('panel channel types', () => {
146-
it('types calls and their resolved results', () => {
147-
const channel = connectPanelChannel<TestProtocol>({ name: 'devframes:test' })
156+
const channel = connectPanelChannel<TestProtocol>({
157+
name: 'devframes:test',
158+
functions: {
159+
notify: { handler: () => {} },
160+
},
161+
})
148162

163+
it('types calls and their resolved results', () => {
149164
expectTypeOf(channel.call('echo', 'hello')).toEqualTypeOf<Promise<string>>()
150165
expectTypeOf(channel.call('sum', 1, 2)).toEqualTypeOf<Promise<number>>()
151166
expectTypeOf(channel.call('save', 'draft')).toEqualTypeOf<Promise<void>>()
@@ -161,8 +176,6 @@ describe('panel channel types', () => {
161176
})
162177

163178
it('types fire-and-forget calls to page-script functions', () => {
164-
const channel = connectPanelChannel<TestProtocol>({ name: 'devframes:test' })
165-
166179
expectTypeOf(channel.callEvent('echo', 'hello')).toEqualTypeOf<void>()
167180
expectTypeOf(channel.callEvent('sum', 1, 2)).toEqualTypeOf<void>()
168181
expectTypeOf(channel.callEvent('save', 'draft')).toEqualTypeOf<void>()
@@ -176,7 +189,6 @@ describe('panel channel types', () => {
176189
})
177190

178191
it('types status listeners and channel state', () => {
179-
const channel = connectPanelChannel<TestProtocol>({ name: 'devframes:test' })
180192
const unsubscribe = channel.events.on('status:updated', (status) => {
181193
expectTypeOf(status).toEqualTypeOf<'connecting' | 'connected' | 'closed'>()
182194
})

packages/devframe/src/in-page-channel/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ interface InPageChannelCommonOptions {
174174
/** Options for {@link createPageScriptChannel}. */
175175
export interface CreatePageScriptChannelOptions<Protocol extends InPageChannelProtocol = InPageChannelProtocol> extends InPageChannelCommonOptions {
176176
/** Implementations of the protocol's page-script functions. */
177-
functions?: CreatePageScriptChannelOptionsFunctions<Protocol>
177+
functions: CreatePageScriptChannelOptionsFunctions<Protocol>
178178
/**
179179
* Window whose `message` events carry panel hellos. Defaults to the
180180
* global `window`; pass `false` to skip the handshake listener entirely
@@ -186,7 +186,7 @@ export interface CreatePageScriptChannelOptions<Protocol extends InPageChannelPr
186186
/** Options for {@link connectPanelChannel}. */
187187
export interface ConnectPanelChannelOptions<Protocol extends InPageChannelProtocol = InPageChannelProtocol> extends InPageChannelCommonOptions {
188188
/** Implementations of the protocol's panel functions. */
189-
functions?: ConnectPanelChannelOptionsFunctions<Protocol>
189+
functions: ConnectPanelChannelOptionsFunctions<Protocol>
190190
/**
191191
* The panel's own window (listens for the handshake grant). Defaults to
192192
* the global `window`; pass `false` with `transport` to skip the handshake.

plugins/a11y/src/spa/lib/channel.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ export function createA11yChannel(): A11yChannel {
4949
// authoritative flag inside `A11yState` takes over on the next update.
5050
const [localScanning, setLocalScanning] = createSignal(false)
5151

52-
const channel = connectPanelChannel<A11yChannelProtocol>({ name: A11Y_CHANNEL })
52+
const channel = connectPanelChannel<A11yChannelProtocol>({
53+
name: A11Y_CHANNEL,
54+
functions: {},
55+
})
5356
channel.events.on('status:updated', status => setPageScriptReady(status === 'connected'))
5457

5558
void channel.sharedState.get('state').then((shared) => {

0 commit comments

Comments
 (0)