Skip to content

Commit 23b3ced

Browse files
committed
refactor: require in-page channel function maps
1 parent 474a0fa commit 23b3ced

7 files changed

Lines changed: 114 additions & 99 deletions

File tree

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

Lines changed: 5 additions & 7 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 shape as `defineRpcFunction` (`name`, `type`, Standard-Schema `args`/`returns`, `jsonSerializable`, `handler`), narrowed to the browser. Each inline definition is contextually typed from its `name` and the corresponding function in the protocol. `defineChannelFunction` provides the same shape when defining a function outside an endpoint's options. 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 `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.
5858

5959
```ts
6060
import type { MyChannelProtocol } from '../shared/protocol'
@@ -64,21 +64,19 @@ import { MY_CHANNEL } from '../shared/protocol'
6464

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

8482
channel.callEvent('flash', 'scanning…') // fans out to every connected panel

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

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
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,6 +38,19 @@ 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?: {
4355
pageScript?: Partial<CreatePageScriptChannelOptions<TestProtocol>>
4456
panel?: Partial<ConnectPanelChannelOptions<TestProtocol>>
@@ -47,14 +59,13 @@ function createLinkedPair(options?: {
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)
@@ -142,14 +153,14 @@ describe('in-page channel over bring-your-own ports', () => {
142153
const pageScript = createPageScriptChannel<TestProtocol>({
143154
name: 'devframes:test',
144155
...noHandshake,
145-
functions: [
146-
defineChannelFunction({
147-
name: 'note',
156+
functions: {
157+
...defaultPageScriptFunctions,
158+
note: {
148159
args: [s.string()] as const,
149160
returns: s.void(),
150161
handler: () => {},
151-
}),
152-
],
162+
},
163+
},
153164
})
154165
pageScript.addPanelPort(port1)
155166
const panel = connectPanelChannel<TestProtocol>({
@@ -182,11 +193,12 @@ describe('in-page channel over bring-your-own ports', () => {
182193
name: 'devframes:test',
183194
...noHandshake,
184195
transport: a.port2,
185-
functions: [
186-
defineChannelFunction({ name: 'notify', type: 'event', handler: (value: string) => {
196+
functions: {
197+
...defaultPanelFunctions,
198+
notify: { type: 'event', handler: (value) => {
187199
received.push(`a:${value}`)
188-
} }),
189-
],
200+
} },
201+
},
190202
})
191203
// Panel B deliberately implements nothing.
192204
const panelB = connectPanelChannel<TestProtocol>({
@@ -218,9 +230,7 @@ describe('in-page channel over bring-your-own ports', () => {
218230
name: 'devframes:test',
219231
...noHandshake,
220232
transport: port2,
221-
functions: [
222-
defineChannelFunction({ name: 'ping-panel', handler: (value: string) => `pong:${value}` }),
223-
],
233+
functions: defaultPanelFunctions,
224234
})
225235
try {
226236
const peer = pageScript.panels[0]!
@@ -237,9 +247,7 @@ describe('in-page channel over bring-your-own ports', () => {
237247
const pageScript = createPageScriptChannel<TestProtocol>({
238248
name: 'devframes:test',
239249
...noHandshake,
240-
functions: [
241-
defineChannelFunction({ name: 'echo', handler: (value: any) => value }),
242-
],
250+
functions: defaultPageScriptFunctions,
243251
})
244252
pageScript.addPanelPort(port1)
245253
const panel = connectPanelChannel<TestProtocol>({
@@ -443,7 +451,7 @@ describe('in-page channel handshake', () => {
443451
name: 'devframes:test',
444452
window: hostWin as unknown as Window,
445453
heartbeat: false,
446-
functions: [defineChannelFunction({ name: 'echo', handler: (value: string) => value })],
454+
functions: defaultPageScriptFunctions,
447455
})
448456
const panel = connectPanelChannel<TestProtocol>({
449457
name: 'devframes:test',
@@ -465,7 +473,10 @@ describe('in-page channel handshake', () => {
465473
name: 'devframes:test',
466474
window: hostWin as unknown as Window,
467475
heartbeat: false,
468-
functions: [defineChannelFunction({ name: 'echo', handler: (value: string) => `revived:${value}` })],
476+
functions: {
477+
...defaultPageScriptFunctions,
478+
echo: { handler: value => `revived:${value}` },
479+
},
469480
})
470481
try {
471482
await panel.whenConnected(2000)
@@ -497,12 +508,12 @@ describe('in-page channel handshake', () => {
497508
name: 'devframes:test',
498509
window: hostWin as unknown as Window,
499510
heartbeat: false,
500-
functions: [
501-
defineChannelFunction({ name: 'echo', handler: (value: string) => value }),
502-
defineChannelFunction({ name: 'note', type: 'event', handler: (value: string) => {
511+
functions: {
512+
...defaultPageScriptFunctions,
513+
note: { type: 'event', handler: (value) => {
503514
noted.push(value)
504-
} }),
505-
],
515+
} },
516+
},
506517
})
507518
try {
508519
await expect(early).resolves.toBe('early')

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ export function createPageScriptChannel<P extends InPageChannelProtocol>(
6363
let heartbeatTimer: ReturnType<typeof setInterval> | undefined
6464

6565
const registry = createLocalFunctionRegistry(codec)
66-
for (const definition of options.functions ?? [])
67-
registry.register(definition)
66+
for (const [fnName, definition] of Object.entries(options.functions ?? {}))
67+
registry.register({ ...definition, name: fnName })
6868

6969
const stateHost = createPageScriptStateHost<P>(function* () {
7070
for (const peer of peers.values()) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ export function connectPanelChannel<P extends InPageChannelProtocol>(
6262

6363
const events = createEventEmitter<PanelChannelEvents>()
6464
const registry = createLocalFunctionRegistry(codec)
65-
for (const definition of options.functions ?? [])
66-
registry.register(definition)
65+
for (const [fnName, definition] of Object.entries(options.functions ?? {}))
66+
registry.register({ ...definition, name: fnName })
6767

6868
let status: InPageChannelStatus = 'connecting'
6969
let attached: AttachedChannelPort | undefined

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

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,17 @@ describe('in-page channel function definitions', () => {
1818
it('infers page-script handlers from the protocol name', () => {
1919
createPageScriptChannel<TestProtocol>({
2020
name: 'devframes:test',
21-
functions: [
22-
{
23-
name: 'echo',
21+
functions: {
22+
echo: {
2423
handler: value => value.toUpperCase(),
2524
},
26-
{
27-
name: 'sum',
25+
sum: {
2826
handler: (a, b) => a + b,
2927
},
30-
{
31-
name: 'save',
28+
save: {
3229
handler: () => {},
3330
},
34-
],
31+
},
3532
})
3633
})
3734

@@ -41,39 +38,51 @@ describe('in-page channel function definitions', () => {
4138
name: 'devframes:test',
4239
window: false,
4340
transport: port1,
44-
functions: [{
45-
name: 'notify',
46-
handler: (message) => {
47-
void message
41+
functions: {
42+
notify: {
43+
handler: (message) => {
44+
void message
45+
},
4846
},
49-
}],
47+
},
5048
})
5149
channel.close()
5250
})
5351

54-
it('rejects names from the remote side', () => {
52+
it('requires every function from the local protocol side', () => {
5553
createPageScriptChannel<TestProtocol>({
5654
name: 'devframes:test',
57-
functions: [
58-
{
59-
// @ts-expect-error `notify` is implemented by panels.
60-
name: 'notify',
61-
handler: (message: string) => void message,
62-
},
63-
],
55+
// @ts-expect-error `sum` and `save` are required.
56+
functions: {
57+
echo: { handler: value => value },
58+
},
59+
})
60+
})
61+
62+
it('rejects keys from the remote side', () => {
63+
createPageScriptChannel<TestProtocol>({
64+
name: 'devframes:test',
65+
functions: {
66+
echo: { handler: value => value },
67+
sum: { handler: (a, b) => a + b },
68+
save: { handler: () => {} },
69+
// @ts-expect-error `notify` is implemented by panels.
70+
notify: { handler: (message: string) => void message },
71+
},
6472
})
6573
})
6674

6775
it('rejects handlers incompatible with the named protocol function', () => {
6876
createPageScriptChannel<TestProtocol>({
6977
name: 'devframes:test',
70-
functions: [
71-
// @ts-expect-error `echo` accepts and returns a string.
72-
{
73-
name: 'echo',
78+
functions: {
79+
echo: {
80+
// @ts-expect-error `echo` accepts and returns a string.
7481
handler: (value: number) => value,
7582
},
76-
],
83+
sum: { handler: (a, b) => a + b },
84+
save: { handler: () => {} },
85+
},
7786
})
7887
})
7988
})

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,17 @@ export type InPageFunctionDefinitionFor<
108108
}
109109
}[keyof ProtocolSideFunctions<P, SIDE> & string]
110110

111-
type InPageFunctionOption<
111+
type InPageFunctionOptions<
112112
P extends InPageChannelProtocol,
113113
SIDE extends ProtocolSide,
114114
> = InPageChannelProtocol extends P
115-
? InPageFunctionDefinitionAny
116-
: InPageFunctionDefinitionFor<P, SIDE>
115+
? Record<string, Omit<InPageFunctionDefinitionAny, 'name'>>
116+
: {
117+
[NAME in keyof ProtocolSideFunctions<P, SIDE> & string]: Omit<
118+
Extract<InPageFunctionDefinitionFor<P, SIDE>, { name: NAME }>,
119+
'name'
120+
>
121+
}
117122

118123
/**
119124
* Connection lifecycle of a panel endpoint: `connecting` (handshake retry
@@ -160,7 +165,7 @@ interface InPageChannelCommonOptions {
160165
/** Options for {@link createPageScriptChannel}. */
161166
export interface CreatePageScriptChannelOptions<Protocol extends InPageChannelProtocol = InPageChannelProtocol> extends InPageChannelCommonOptions {
162167
/** Implementations of the protocol's page-script functions. */
163-
functions?: readonly InPageFunctionOption<Protocol, 'pageScript'>[]
168+
functions?: InPageFunctionOptions<Protocol, 'pageScript'>
164169
/**
165170
* Window whose `message` events carry panel hellos. Defaults to the
166171
* global `window`; pass `false` to skip the handshake listener entirely
@@ -172,7 +177,7 @@ export interface CreatePageScriptChannelOptions<Protocol extends InPageChannelPr
172177
/** Options for {@link connectPanelChannel}. */
173178
export interface ConnectPanelChannelOptions<Protocol extends InPageChannelProtocol = InPageChannelProtocol> extends InPageChannelCommonOptions {
174179
/** Implementations of the protocol's panel functions. */
175-
functions?: readonly InPageFunctionOption<Protocol, 'panel'>[]
180+
functions?: InPageFunctionOptions<Protocol, 'panel'>
176181
/**
177182
* The panel's own window (listens for the handshake grant). Defaults to
178183
* the global `window`; pass `false` with `transport` to skip the handshake.

0 commit comments

Comments
 (0)