Skip to content

Commit a8e1f00

Browse files
authored
feat: type in-page channel functions from protocol (#314)
1 parent 0972847 commit a8e1f00

11 files changed

Lines changed: 487 additions & 98 deletions

File tree

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

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

5555
## The page script endpoint
5656

57-
Functions are defined with `defineChannelFunction`the same authoring shape as `defineRpcFunction` (`name`, `type`, Standard-Schema `args`/`returns`, `jsonSerializable`, `handler`), narrowed to the browser. 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'
6161
// inject/index.ts — runs in the user app's page
62-
import { createPageScriptChannel, defineChannelFunction } from 'devframe/in-page-channel'
62+
import { createPageScriptChannel } from 'devframe/in-page-channel'
6363
import { MY_CHANNEL } from '../shared/protocol'
6464

6565
const channel = createPageScriptChannel<MyChannelProtocol>({
6666
name: MY_CHANNEL,
67-
functions: [
68-
defineChannelFunction({
69-
name: 'highlight',
67+
functions: {
68+
highlight: {
7069
type: 'event', // fire-and-forget
7170
jsonSerializable: true,
72-
handler: (selector: string) => drawRing(document.querySelector(selector)),
73-
}),
74-
defineChannelFunction({
75-
name: 'measure', // request/response (the default `query` type)
76-
handler: (selector: string) => {
71+
handler: selector => drawRing(document.querySelector(selector)),
72+
},
73+
measure: { // request/response (the default `query` type)
74+
handler: (selector) => {
7775
const rect = document.querySelector(selector)!.getBoundingClientRect()
7876
return { width: rect.width, height: rect.height }
7977
},
80-
}),
81-
],
78+
},
79+
},
8280
})
8381

8482
channel.callEvent('flash', 'scanning…') // fans out to every connected panel
@@ -96,7 +94,14 @@ import type { MyChannelProtocol } from '../shared/protocol'
9694
import { connectPanelChannel } from 'devframe/in-page-channel'
9795
import { MY_CHANNEL } from '../shared/protocol'
9896

99-
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+
})
100105

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

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

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { InPageChannelProtocol, PageScriptChannel, PanelChannel } from './types'
1+
import type { ConnectPanelChannelOptions, CreatePageScriptChannelOptions, InPageChannelProtocol, PageScriptChannel, PanelChannel } from './types'
22
import { describe, expect, it, vi } from 'vitest'
3-
import { defineChannelFunction } from './index'
43
import { InPageChannelError } from './internal'
54
import { createPageScriptChannel } from './page-script'
65
import { connectPanelChannel } from './panel'
@@ -39,29 +38,42 @@ function until(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
3938

4039
const noHandshake = { window: false as const, heartbeat: false as const }
4140

41+
const defaultPageScriptFunctions: NonNullable<CreatePageScriptChannelOptions<TestProtocol>['functions']> = {
42+
echo: { handler: value => value },
43+
sum: { handler: (a, b) => a + b },
44+
boom: { handler: () => {} },
45+
strict: { handler: payload => payload },
46+
note: { type: 'event', handler: () => {} },
47+
}
48+
49+
const defaultPanelFunctions: NonNullable<ConnectPanelChannelOptions<TestProtocol>['functions']> = {
50+
'ping-panel': { handler: value => `pong:${value}` },
51+
'notify': { type: 'event', handler: () => {} },
52+
}
53+
4254
function createLinkedPair(options?: {
43-
pageScript?: Partial<Parameters<typeof createPageScriptChannel>[0]>
44-
panel?: Partial<Parameters<typeof connectPanelChannel>[0]>
55+
pageScript?: Partial<CreatePageScriptChannelOptions<TestProtocol>>
56+
panel?: Partial<ConnectPanelChannelOptions<TestProtocol>>
4557
}): { pageScript: PageScriptChannel<TestProtocol>, panel: PanelChannel<TestProtocol>, dispose: () => void } {
4658
const { port1, port2 } = new MessageChannel()
4759
const pageScript = createPageScriptChannel<TestProtocol>({
4860
name: 'devframes:test',
4961
...noHandshake,
50-
functions: [
51-
defineChannelFunction({ name: 'echo', handler: (value: string) => value }),
52-
defineChannelFunction({ name: 'sum', type: 'query', handler: (a: number, b: number) => a + b }),
53-
defineChannelFunction({ name: 'boom', handler: () => {
62+
functions: {
63+
...defaultPageScriptFunctions,
64+
boom: { handler: () => {
5465
throw new Error('exploded')
55-
} }),
56-
defineChannelFunction({ name: 'strict', jsonSerializable: true, handler: (payload: unknown) => payload }),
57-
],
66+
} },
67+
strict: { jsonSerializable: true, handler: payload => payload },
68+
},
5869
...options?.pageScript,
5970
})
6071
pageScript.addPanelPort(port1)
6172
const panel = connectPanelChannel<TestProtocol>({
6273
name: 'devframes:test',
6374
...noHandshake,
6475
transport: port2,
76+
functions: defaultPanelFunctions,
6577
...options?.panel,
6678
})
6779
return {
@@ -142,20 +154,21 @@ describe('in-page channel over bring-your-own ports', () => {
142154
const pageScript = createPageScriptChannel<TestProtocol>({
143155
name: 'devframes:test',
144156
...noHandshake,
145-
functions: [
146-
defineChannelFunction({
147-
name: 'note',
157+
functions: {
158+
...defaultPageScriptFunctions,
159+
note: {
148160
args: [s.string()] as const,
149161
returns: s.void(),
150162
handler: () => {},
151-
}),
152-
],
163+
},
164+
},
153165
})
154166
pageScript.addPanelPort(port1)
155167
const panel = connectPanelChannel<TestProtocol>({
156168
name: 'devframes:test',
157169
...noHandshake,
158170
transport: port2,
171+
functions: defaultPanelFunctions,
159172
})
160173
try {
161174
await expect(panel.call('note', 'fine')).resolves.toBeUndefined()
@@ -174,6 +187,7 @@ describe('in-page channel over bring-your-own ports', () => {
174187
const pageScript = createPageScriptChannel<TestProtocol>({
175188
name: 'devframes:test',
176189
...noHandshake,
190+
functions: defaultPageScriptFunctions,
177191
})
178192
pageScript.addPanelPort(a.port1)
179193
pageScript.addPanelPort(b.port1)
@@ -182,17 +196,19 @@ describe('in-page channel over bring-your-own ports', () => {
182196
name: 'devframes:test',
183197
...noHandshake,
184198
transport: a.port2,
185-
functions: [
186-
defineChannelFunction({ name: 'notify', type: 'event', handler: (value: string) => {
199+
functions: {
200+
...defaultPanelFunctions,
201+
notify: { type: 'event', handler: (value) => {
187202
received.push(`a:${value}`)
188-
} }),
189-
],
203+
} },
204+
},
190205
})
191-
// Panel B deliberately implements nothing.
192-
const panelB = connectPanelChannel<TestProtocol>({
206+
// Panel B deliberately has no local functions in its protocol.
207+
const panelB = connectPanelChannel<InPageChannelProtocol>({
193208
name: 'devframes:test',
194209
...noHandshake,
195210
transport: b.port2,
211+
functions: {},
196212
})
197213
try {
198214
expect(pageScript.panels).toHaveLength(2)
@@ -212,15 +228,14 @@ describe('in-page channel over bring-your-own ports', () => {
212228
const pageScript = createPageScriptChannel<TestProtocol>({
213229
name: 'devframes:test',
214230
...noHandshake,
231+
functions: defaultPageScriptFunctions,
215232
})
216233
pageScript.addPanelPort(port1)
217234
const panel = connectPanelChannel<TestProtocol>({
218235
name: 'devframes:test',
219236
...noHandshake,
220237
transport: port2,
221-
functions: [
222-
defineChannelFunction({ name: 'ping-panel', handler: (value: string) => `pong:${value}` }),
223-
],
238+
functions: defaultPanelFunctions,
224239
})
225240
try {
226241
const peer = pageScript.panels[0]!
@@ -237,15 +252,14 @@ describe('in-page channel over bring-your-own ports', () => {
237252
const pageScript = createPageScriptChannel<TestProtocol>({
238253
name: 'devframes:test',
239254
...noHandshake,
240-
functions: [
241-
defineChannelFunction({ name: 'echo', handler: (value: any) => value }),
242-
],
255+
functions: defaultPageScriptFunctions,
243256
})
244257
pageScript.addPanelPort(port1)
245258
const panel = connectPanelChannel<TestProtocol>({
246259
name: 'devframes:test',
247260
...noHandshake,
248261
transport: port2,
262+
functions: defaultPanelFunctions,
249263
// Unwrap a fake reactivity wrapper on the way out, tag on the way in.
250264
serialize: value => (value && typeof value === 'object' && '__wrapped' in (value as any))
251265
? (value as any).__wrapped
@@ -266,6 +280,7 @@ describe('in-page channel over bring-your-own ports', () => {
266280
const pageScript = createPageScriptChannel<TestProtocol>({
267281
name: 'devframes:test',
268282
...noHandshake,
283+
functions: defaultPageScriptFunctions,
269284
})
270285
const connected: string[] = []
271286
const disconnected: string[] = []
@@ -276,6 +291,7 @@ describe('in-page channel over bring-your-own ports', () => {
276291
name: 'devframes:test',
277292
...noHandshake,
278293
transport: port2,
294+
functions: defaultPanelFunctions,
279295
})
280296
try {
281297
expect(connected).toHaveLength(1)
@@ -316,11 +332,12 @@ describe('in-page channel shared state', () => {
316332
const pageScript = createPageScriptChannel<TestProtocol>({
317333
name: 'devframes:test',
318334
...noHandshake,
335+
functions: defaultPageScriptFunctions,
319336
})
320337
pageScript.addPanelPort(a.port1)
321338
pageScript.addPanelPort(b.port1)
322-
const panelA = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: a.port2 })
323-
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 })
324341
try {
325342
const authority = await pageScript.sharedState.get('doc', { initialValue: { count: 0 } })
326343
const mirrorA = await panelA.sharedState.get('doc')
@@ -341,7 +358,7 @@ describe('in-page channel shared state', () => {
341358

342359
it('seeds a late-joining panel with the current value', async () => {
343360
const { port1, port2 } = new MessageChannel()
344-
const pageScript = createPageScriptChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake })
361+
const pageScript = createPageScriptChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, functions: defaultPageScriptFunctions })
345362
const authority = await pageScript.sharedState.get('doc', { initialValue: { count: 0 } })
346363
authority.mutate((draft) => {
347364
draft.count = 41
@@ -351,7 +368,7 @@ describe('in-page channel shared state', () => {
351368
})
352369

353370
pageScript.addPanelPort(port1)
354-
const panel = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: port2 })
371+
const panel = connectPanelChannel<TestProtocol>({ name: 'devframes:test', ...noHandshake, transport: port2, functions: defaultPanelFunctions })
355372
try {
356373
const mirror = await panel.sharedState.get('doc')
357374
expect(mirror.value()).toEqual({ count: 42 })
@@ -443,13 +460,14 @@ describe('in-page channel handshake', () => {
443460
name: 'devframes:test',
444461
window: hostWin as unknown as Window,
445462
heartbeat: false,
446-
functions: [defineChannelFunction({ name: 'echo', handler: (value: string) => value })],
463+
functions: defaultPageScriptFunctions,
447464
})
448465
const panel = connectPanelChannel<TestProtocol>({
449466
name: 'devframes:test',
450467
window: panelWin as unknown as Window,
451468
targets: [hostWin as unknown as Window],
452469
...fastHello,
470+
functions: defaultPanelFunctions,
453471
})
454472
try {
455473
await panel.whenConnected(2000)
@@ -465,7 +483,10 @@ describe('in-page channel handshake', () => {
465483
name: 'devframes:test',
466484
window: hostWin as unknown as Window,
467485
heartbeat: false,
468-
functions: [defineChannelFunction({ name: 'echo', handler: (value: string) => `revived:${value}` })],
486+
functions: {
487+
...defaultPageScriptFunctions,
488+
echo: { handler: value => `revived:${value}` },
489+
},
469490
})
470491
try {
471492
await panel.whenConnected(2000)
@@ -489,6 +510,7 @@ describe('in-page channel handshake', () => {
489510
window: panelWin as unknown as Window,
490511
targets: [hostWin as unknown as Window],
491512
...fastHello,
513+
functions: defaultPanelFunctions,
492514
})
493515
const early = panel.call('echo', 'early')
494516
panel.callEvent('note', 'buffered')
@@ -497,12 +519,12 @@ describe('in-page channel handshake', () => {
497519
name: 'devframes:test',
498520
window: hostWin as unknown as Window,
499521
heartbeat: false,
500-
functions: [
501-
defineChannelFunction({ name: 'echo', handler: (value: string) => value }),
502-
defineChannelFunction({ name: 'note', type: 'event', handler: (value: string) => {
522+
functions: {
523+
...defaultPageScriptFunctions,
524+
note: { type: 'event', handler: (value) => {
503525
noted.push(value)
504-
} }),
505-
],
526+
} },
527+
},
506528
})
507529
try {
508530
await expect(early).resolves.toBe('early')
@@ -522,6 +544,7 @@ describe('in-page channel handshake', () => {
522544
name: 'devframes:test-origin',
523545
window: hostWin as unknown as Window,
524546
heartbeat: false,
547+
functions: defaultPageScriptFunctions,
525548
})
526549
try {
527550
hostWin.__dispatch({
@@ -552,6 +575,7 @@ describe('in-page channel handshake', () => {
552575
name: 'devframes:test-version',
553576
window: hostWin as unknown as Window,
554577
heartbeat: false,
578+
functions: defaultPageScriptFunctions,
555579
})
556580
try {
557581
hostWin.__dispatch({
@@ -581,13 +605,15 @@ describe('in-page channel handshake', () => {
581605
name: 'devframes:test',
582606
window: hostWin as unknown as Window,
583607
heartbeat: false,
608+
functions: defaultPageScriptFunctions,
584609
})
585610
const pinnedElsewhere = connectPanelChannel<TestProtocol>({
586611
name: 'devframes:test',
587612
window: panelWin as unknown as Window,
588613
targets: [hostWin as unknown as Window],
589614
instanceId: 'some-other-tab',
590615
...fastHello,
616+
functions: defaultPanelFunctions,
591617
})
592618
try {
593619
await expect(pinnedElsewhere.whenConnected(100)).rejects.toMatchObject({ code: 'timeout' })
@@ -598,6 +624,7 @@ describe('in-page channel handshake', () => {
598624
targets: [hostWin as unknown as Window],
599625
instanceId: pageScript.instanceId,
600626
...fastHello,
627+
functions: defaultPanelFunctions,
601628
})
602629
try {
603630
await pinnedHere.whenConnected(2000)
@@ -618,6 +645,7 @@ describe('in-page channel handshake', () => {
618645
name: `devframes:test-lonely-${Math.random()}`,
619646
window: false,
620647
heartbeat: false,
648+
functions: defaultPanelFunctions,
621649
})
622650
try {
623651
expect(lonely.status).toBe('connecting')
@@ -636,6 +664,7 @@ describe('in-page channel handshake', () => {
636664
window: false,
637665
heartbeat: false,
638666
callTimeoutMs: 50,
667+
functions: defaultPanelFunctions,
639668
})
640669
try {
641670
const rejection = await lonely.call('echo', 'nobody').catch(error => error)
@@ -653,6 +682,7 @@ describe('in-page channel handshake', () => {
653682
name: `devframes:test-lonely-${Math.random()}`,
654683
window: false,
655684
heartbeat: false,
685+
functions: defaultPanelFunctions,
656686
})
657687
const pending = lonely.call('echo', 'never')
658688
const waiting = lonely.whenConnected()

0 commit comments

Comments
 (0)