devframe 0.9.16 → 0.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -9,10 +9,18 @@ import { BirpcReturn } from "birpc";
|
|
|
9
9
|
* channel-name constant declared next to it.
|
|
10
10
|
*/
|
|
11
11
|
interface InPageChannelProtocol {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
functions?: {
|
|
13
|
+
/** Functions implemented by the page script. */
|
|
14
|
+
pageScript?: Record<string, (...args: any[]) => any>;
|
|
15
|
+
/** Functions implemented by panels. */
|
|
16
|
+
panel?: Record<string, (...args: any[]) => any>;
|
|
17
|
+
};
|
|
18
|
+
events?: {
|
|
19
|
+
/** Events emitted by panels and received by the page script. */
|
|
20
|
+
pageScript?: Record<string, (...args: any[]) => void>;
|
|
21
|
+
/** Events emitted by the page script and received by panels. */
|
|
22
|
+
panel?: Record<string, (...args: any[]) => void>;
|
|
23
|
+
};
|
|
16
24
|
/**
|
|
17
25
|
* Shared-state slots. The page script is the authority: it owns the
|
|
18
26
|
* canonical value; panels are seeded on connect and converge through
|
|
@@ -21,21 +29,14 @@ interface InPageChannelProtocol {
|
|
|
21
29
|
sharedStates?: Record<string, object>;
|
|
22
30
|
}
|
|
23
31
|
type SideFunctions<S> = S extends Record<string, (...args: any[]) => any> ? S : Record<string, never>;
|
|
24
|
-
type
|
|
25
|
-
type
|
|
32
|
+
type SideDeclarations<P extends InPageChannelProtocol, Kind extends 'functions' | 'events', Side extends 'pageScript' | 'panel'> = Kind extends keyof P ? Side extends keyof NonNullable<P[Kind]> ? SideFunctions<NonNullable<NonNullable<P[Kind]>[Side]>> : Record<string, never> : Record<string, never>;
|
|
33
|
+
type PageScriptFunctions<P extends InPageChannelProtocol> = SideDeclarations<P, 'functions', 'pageScript'>;
|
|
34
|
+
type PanelFunctions<P extends InPageChannelProtocol> = SideDeclarations<P, 'functions', 'panel'>;
|
|
26
35
|
type SharedStates<P extends InPageChannelProtocol> = P['sharedStates'] extends Record<string, object> ? P['sharedStates'] : Record<string, never>;
|
|
27
36
|
type FnArgs<F> = F extends ((...args: infer A) => any) ? A : never;
|
|
28
37
|
type FnReturn<F> = F extends ((...args: any[]) => infer R) ? Awaited<R> : never;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* @internal
|
|
32
|
-
*/
|
|
33
|
-
type PageScriptFunctionsEvents<P extends InPageChannelProtocol> = { [K in keyof PageScriptFunctions<P> as FnReturn<PageScriptFunctions<P>[K]> extends void ? K : never]: PageScriptFunctions<P>[K]; };
|
|
34
|
-
/**
|
|
35
|
-
* Panel functions whose resolved return type marks an event.
|
|
36
|
-
* @internal
|
|
37
|
-
*/
|
|
38
|
-
type PanelFunctionsEvents<P extends InPageChannelProtocol> = { [K in keyof PanelFunctions<P> as FnReturn<PanelFunctions<P>[K]> extends void ? K : never]: PanelFunctions<P>[K]; };
|
|
38
|
+
type PageScriptEvents<P extends InPageChannelProtocol> = SideDeclarations<P, 'events', 'pageScript'>;
|
|
39
|
+
type PanelEvents<P extends InPageChannelProtocol> = SideDeclarations<P, 'events', 'panel'>;
|
|
39
40
|
/**
|
|
40
41
|
* Converts a protocol function to its accepted endpoint handler.
|
|
41
42
|
*
|
|
@@ -99,9 +100,10 @@ interface InPageFunctionOptionBase {
|
|
|
99
100
|
jsonSerializable?: boolean;
|
|
100
101
|
}
|
|
101
102
|
interface InPageEventFunctionOption<F> extends InPageFunctionOptionBase {
|
|
102
|
-
type
|
|
103
|
+
type?: 'event';
|
|
103
104
|
handler?: ProtocolHandler<F>;
|
|
104
105
|
}
|
|
106
|
+
type InPageEventOption<F> = [F] extends [never] ? never : InPageEventFunctionOption<F>;
|
|
105
107
|
interface InPageQueryFunctionOption<F> extends InPageFunctionOptionBase {
|
|
106
108
|
type?: 'query';
|
|
107
109
|
handler: ProtocolHandler<F>;
|
|
@@ -110,7 +112,7 @@ interface InPageActionFunctionOption<F> extends InPageFunctionOptionBase {
|
|
|
110
112
|
type: 'action';
|
|
111
113
|
handler: ProtocolHandler<F>;
|
|
112
114
|
}
|
|
113
|
-
type InPageFunctionOption<F> =
|
|
115
|
+
type InPageFunctionOption<F> = InPageQueryFunctionOption<F> | InPageActionFunctionOption<F>;
|
|
114
116
|
/**
|
|
115
117
|
* Functions implemented by {@link createPageScriptChannel}.
|
|
116
118
|
*
|
|
@@ -173,8 +175,10 @@ interface InPageChannelCommonOptions {
|
|
|
173
175
|
}
|
|
174
176
|
/** Options for {@link createPageScriptChannel}. */
|
|
175
177
|
interface CreatePageScriptChannelOptions<Protocol extends InPageChannelProtocol = InPageChannelProtocol> extends InPageChannelCommonOptions {
|
|
176
|
-
/** Every page-script function declaration
|
|
178
|
+
/** Every page-script function declaration, with a required handler. */
|
|
177
179
|
functions: CreatePageScriptChannelOptionsFunctions<Protocol>;
|
|
180
|
+
/** Optional metadata or handlers for incoming events. Listeners may instead subscribe through `channel.on()`. */
|
|
181
|
+
events?: { [NAME in keyof PageScriptEvents<Protocol> & string]?: InPageEventOption<PageScriptEvents<Protocol>[NAME]>; };
|
|
178
182
|
/**
|
|
179
183
|
* Window whose `message` events carry panel hellos. Defaults to the
|
|
180
184
|
* global `window`; pass `false` to skip the handshake listener entirely
|
|
@@ -184,8 +188,10 @@ interface CreatePageScriptChannelOptions<Protocol extends InPageChannelProtocol
|
|
|
184
188
|
}
|
|
185
189
|
/** Options for {@link connectPanelChannel}. */
|
|
186
190
|
interface ConnectPanelChannelOptions<Protocol extends InPageChannelProtocol = InPageChannelProtocol> extends InPageChannelCommonOptions {
|
|
187
|
-
/** Every panel function declaration
|
|
191
|
+
/** Every panel function declaration, with a required handler. */
|
|
188
192
|
functions: ConnectPanelChannelOptionsFunctions<Protocol>;
|
|
193
|
+
/** Optional metadata or handlers for incoming events. Listeners may instead subscribe through `channel.on()`. */
|
|
194
|
+
events?: { [NAME in keyof PanelEvents<Protocol> & string]?: InPageEventOption<PanelEvents<Protocol>[NAME]>; };
|
|
189
195
|
/**
|
|
190
196
|
* The panel's own window (listens for the handshake grant). Defaults to
|
|
191
197
|
* the global `window`; pass `false` with `transport` to skip the handshake.
|
|
@@ -262,11 +268,11 @@ interface PageScriptChannel<P extends InPageChannelProtocol> {
|
|
|
262
268
|
readonly panels: readonly PanelPeer<P>[];
|
|
263
269
|
readonly events: Pick<EventEmitter<PageScriptChannelEvents<P>>, 'on' | 'once'>;
|
|
264
270
|
/** Fan an event out to every connected panel. */
|
|
265
|
-
emit: <K extends keyof
|
|
271
|
+
emit: <K extends keyof PanelEvents<P> & string>(name: K, ...args: FnArgs<PanelEvents<P>[K]>) => void;
|
|
266
272
|
/** @deprecated Use `emit()` instead. */
|
|
267
|
-
callEvent: <K extends keyof
|
|
273
|
+
callEvent: <K extends keyof PanelEvents<P> & string>(name: K, ...args: FnArgs<PanelEvents<P>[K]>) => void;
|
|
268
274
|
/** Subscribe to an event emitted by a panel. Returns an unsubscribe function. */
|
|
269
|
-
on: <K extends keyof
|
|
275
|
+
on: <K extends keyof PageScriptEvents<P> & string>(name: K, listener: (...args: FnArgs<PageScriptEvents<P>[K]>) => void) => () => void;
|
|
270
276
|
/** Page-script-authoritative shared states, replayed to joining panels. */
|
|
271
277
|
readonly sharedState: InPageSharedStateHost<P>;
|
|
272
278
|
/** Adopt a pre-established port as a panel peer (bring-your-own transport). */
|
|
@@ -307,11 +313,11 @@ interface PanelChannel<P extends InPageChannelProtocol> {
|
|
|
307
313
|
* Emit an event to the page script. While `connecting` the event is buffered
|
|
308
314
|
* (up to `eventBufferLimit`) and flushed on connect.
|
|
309
315
|
*/
|
|
310
|
-
emit: <K extends keyof
|
|
316
|
+
emit: <K extends keyof PageScriptEvents<P> & string>(name: K, ...args: FnArgs<PageScriptEvents<P>[K]>) => void;
|
|
311
317
|
/** @deprecated Use `emit()` instead. */
|
|
312
|
-
callEvent: <K extends keyof
|
|
318
|
+
callEvent: <K extends keyof PageScriptEvents<P> & string>(name: K, ...args: FnArgs<PageScriptEvents<P>[K]>) => void;
|
|
313
319
|
/** Subscribe to an event emitted by the page script. Returns an unsubscribe function. */
|
|
314
|
-
on: <K extends keyof
|
|
320
|
+
on: <K extends keyof PanelEvents<P> & string>(name: K, listener: (...args: FnArgs<PanelEvents<P>[K]>) => void) => () => void;
|
|
315
321
|
/** Shared states mirrored from the page-script authority. */
|
|
316
322
|
readonly sharedState: InPageSharedStateHost<P>;
|
|
317
323
|
/** Tear the endpoint down permanently. */
|
|
@@ -8,7 +8,7 @@ const diagnostics = /* #__PURE__ */ defineDiagnostics({
|
|
|
8
8
|
docsBase: "https://devfra.me/errors",
|
|
9
9
|
codes: { DF0077: {
|
|
10
10
|
why: (p) => `In-page channel function "${p.name}" is not registered on this endpoint.`,
|
|
11
|
-
fix: "Declare the function in this endpoint's `functions` option
|
|
11
|
+
fix: "Declare the function in this endpoint's `functions` option."
|
|
12
12
|
} }
|
|
13
13
|
});
|
|
14
14
|
//#endregion
|
|
@@ -169,6 +169,10 @@ function serializeArgs(codec, args) {
|
|
|
169
169
|
function deserializeResult(codec, result) {
|
|
170
170
|
return codec.deserialize && result !== void 0 ? codec.deserialize(result) : result;
|
|
171
171
|
}
|
|
172
|
+
function channelMethod(type, name) {
|
|
173
|
+
return `devframe:in-page:${type === "event" ? "event" : "function"}:${name}`;
|
|
174
|
+
}
|
|
175
|
+
const FUNCTION_METHOD_PREFIX = channelMethod("function", "");
|
|
172
176
|
/**
|
|
173
177
|
* An endpoint's local function table, resolved by name when the remote side
|
|
174
178
|
* calls in. Each handler is wrapped with the receive pipeline: deserialize
|
|
@@ -180,32 +184,38 @@ function createLocalFunctionRegistry(codec) {
|
|
|
180
184
|
const listeners = /* @__PURE__ */ new Map();
|
|
181
185
|
return {
|
|
182
186
|
register(definition) {
|
|
183
|
-
definitions.set(definition.name, definition);
|
|
187
|
+
definitions.set(channelMethod(definition.type, definition.name), definition);
|
|
184
188
|
},
|
|
185
189
|
on(name, listener) {
|
|
186
|
-
|
|
187
|
-
let registered = listeners.get(
|
|
190
|
+
const key = channelMethod("event", name);
|
|
191
|
+
let registered = listeners.get(key);
|
|
188
192
|
if (!registered) {
|
|
189
193
|
registered = /* @__PURE__ */ new Set();
|
|
190
|
-
listeners.set(
|
|
194
|
+
listeners.set(key, registered);
|
|
191
195
|
}
|
|
192
196
|
registered.add(listener);
|
|
193
197
|
return () => {
|
|
194
198
|
registered.delete(listener);
|
|
195
|
-
if (registered.size === 0) listeners.delete(
|
|
199
|
+
if (registered.size === 0) listeners.delete(key);
|
|
196
200
|
};
|
|
197
201
|
},
|
|
198
202
|
resolve(name) {
|
|
199
203
|
const definition = definitions.get(name);
|
|
200
204
|
const registered = listeners.get(name);
|
|
201
|
-
if (!definition && !registered?.size)
|
|
205
|
+
if (!definition && !registered?.size) {
|
|
206
|
+
if (name.startsWith(FUNCTION_METHOD_PREFIX)) return () => {
|
|
207
|
+
throw diagnostics.DF0077({ name: name.slice(FUNCTION_METHOD_PREFIX.length) });
|
|
208
|
+
};
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
202
211
|
return async (...rawArgs) => {
|
|
203
212
|
const args = codec.deserialize ? rawArgs.map(codec.deserialize) : rawArgs;
|
|
204
213
|
if (definition?.jsonSerializable) assertJsonSerializable(args, "its arguments", definition.name);
|
|
205
214
|
if (definition?.args?.length) await validateArgs(definition.name, definition.args, args);
|
|
206
215
|
const result = await definition?.handler?.(...args);
|
|
207
216
|
for (const listener of [...listeners.get(name) ?? []]) listener(...args);
|
|
208
|
-
if (definition?.
|
|
217
|
+
if (definition?.type === "event") return void 0;
|
|
218
|
+
if (definition?.jsonSerializable && result !== void 0) assertJsonSerializable(result, "its return value", definition.name);
|
|
209
219
|
return codec.serialize && result !== void 0 ? codec.serialize(result) : result;
|
|
210
220
|
};
|
|
211
221
|
}
|
|
@@ -482,13 +492,18 @@ function createPageScriptChannel(options) {
|
|
|
482
492
|
...definition,
|
|
483
493
|
name: fnName
|
|
484
494
|
});
|
|
495
|
+
for (const [eventName, definition] of Object.entries(options.events ?? {})) registry.register({
|
|
496
|
+
...definition,
|
|
497
|
+
name: eventName,
|
|
498
|
+
type: "event"
|
|
499
|
+
});
|
|
485
500
|
const stateHost = createPageScriptStateHost(function* () {
|
|
486
501
|
for (const peer of peers.values()) yield {
|
|
487
502
|
subscribedStates: peer.subscribedStates,
|
|
488
503
|
callEventRaw: (method, args) => {
|
|
489
504
|
peer.attached.rpc.$callRaw({
|
|
490
505
|
method,
|
|
491
|
-
args,
|
|
506
|
+
args: serializeArgs(codec, args),
|
|
492
507
|
event: true,
|
|
493
508
|
optional: true
|
|
494
509
|
}).catch(() => {});
|
|
@@ -517,14 +532,19 @@ function createPageScriptChannel(options) {
|
|
|
517
532
|
callEventRaw: (method, args) => {
|
|
518
533
|
internal.attached.rpc.$callRaw({
|
|
519
534
|
method,
|
|
520
|
-
args,
|
|
535
|
+
args: serializeArgs(codec, args),
|
|
521
536
|
event: true,
|
|
522
537
|
optional: true
|
|
523
538
|
}).catch(() => {});
|
|
524
539
|
}
|
|
525
540
|
});
|
|
541
|
+
const stateRegistry = createLocalFunctionRegistry(codec);
|
|
542
|
+
for (const [name, handler] of Object.entries(internal.internalHandlers)) stateRegistry.register({
|
|
543
|
+
name,
|
|
544
|
+
handler
|
|
545
|
+
});
|
|
526
546
|
internal.attached = attachChannelPort(port, {
|
|
527
|
-
resolveLocal: (fnName) =>
|
|
547
|
+
resolveLocal: (fnName) => stateRegistry.resolve(fnName) ?? registry.resolve(fnName),
|
|
528
548
|
onControl: (kind) => {
|
|
529
549
|
if (kind === "ping") internal.attached.postControl("pong");
|
|
530
550
|
},
|
|
@@ -532,7 +552,7 @@ function createPageScriptChannel(options) {
|
|
|
532
552
|
});
|
|
533
553
|
internal.peer = {
|
|
534
554
|
id,
|
|
535
|
-
call: (fnName, ...args) => withCallDeadline(internal.attached.rpc.$call(fnName, ...serializeArgs(codec, args)).then((result) => deserializeResult(codec, result)), callTimeoutMs, () => `in-page channel "${name}": call "${fnName}" to panel "${id}" timed out after ${callTimeoutMs}ms`),
|
|
555
|
+
call: (fnName, ...args) => withCallDeadline(internal.attached.rpc.$call(channelMethod("function", fnName), ...serializeArgs(codec, args)).then((result) => deserializeResult(codec, result)), callTimeoutMs, () => `in-page channel "${name}": call "${fnName}" to panel "${id}" timed out after ${callTimeoutMs}ms`),
|
|
536
556
|
close: () => removePeer(id, {
|
|
537
557
|
bye: true,
|
|
538
558
|
reason: "the page script closed this panel"
|
|
@@ -550,8 +570,8 @@ function createPageScriptChannel(options) {
|
|
|
550
570
|
if (closed) return;
|
|
551
571
|
const data = event.data;
|
|
552
572
|
if (!isHandshakeMessage(data) || data.kind !== "hello" || data.name !== name) return;
|
|
553
|
-
if (data.v !==
|
|
554
|
-
warnOnce(`in-page channel "${name}": ignoring a hello with protocol version ${data.v} (this side speaks
|
|
573
|
+
if (data.v !== 2) {
|
|
574
|
+
warnOnce(`in-page channel "${name}": ignoring a hello with protocol version ${data.v} (this side speaks 2); align the devframe versions of the page script and the panel`);
|
|
555
575
|
return;
|
|
556
576
|
}
|
|
557
577
|
if (!allowedOrigins.includes("*") && !allowedOrigins.includes(event.origin)) {
|
|
@@ -565,7 +585,7 @@ function createPageScriptChannel(options) {
|
|
|
565
585
|
addPeer(messageChannel.port1, data.panelId);
|
|
566
586
|
const grant = {
|
|
567
587
|
channel: IN_PAGE_CHANNEL_TAG,
|
|
568
|
-
v:
|
|
588
|
+
v: 2,
|
|
569
589
|
kind: "grant",
|
|
570
590
|
name,
|
|
571
591
|
panelId: data.panelId,
|
|
@@ -582,7 +602,7 @@ function createPageScriptChannel(options) {
|
|
|
582
602
|
const emit = (fnName, ...args) => {
|
|
583
603
|
const wireArgs = serializeArgs(codec, args);
|
|
584
604
|
for (const peer of peers.values()) peer.attached.rpc.$callRaw({
|
|
585
|
-
method: fnName,
|
|
605
|
+
method: channelMethod("event", fnName),
|
|
586
606
|
args: wireArgs,
|
|
587
607
|
event: true,
|
|
588
608
|
optional: true
|
|
@@ -651,6 +671,11 @@ function connectPanelChannel(options) {
|
|
|
651
671
|
...definition,
|
|
652
672
|
name: fnName
|
|
653
673
|
});
|
|
674
|
+
for (const [eventName, definition] of Object.entries(options.events ?? {})) registry.register({
|
|
675
|
+
...definition,
|
|
676
|
+
name: eventName,
|
|
677
|
+
type: "event"
|
|
678
|
+
});
|
|
654
679
|
let status = "connecting";
|
|
655
680
|
let attached;
|
|
656
681
|
let pageScriptInfo;
|
|
@@ -668,8 +693,13 @@ function connectPanelChannel(options) {
|
|
|
668
693
|
}
|
|
669
694
|
const stateHost = createPanelStateHost({
|
|
670
695
|
isConnected: () => status === "connected",
|
|
671
|
-
callEvent: (method, args) => sendEvent(method, args),
|
|
672
|
-
call: (method, args) => enqueueCall(method, args)
|
|
696
|
+
callEvent: (method, args) => sendEvent(method, serializeArgs(codec, args)),
|
|
697
|
+
call: (method, args) => enqueueCall(method, serializeArgs(codec, args))
|
|
698
|
+
});
|
|
699
|
+
const stateRegistry = createLocalFunctionRegistry(codec);
|
|
700
|
+
for (const [name, handler] of Object.entries(stateHost.handlers)) stateRegistry.register({
|
|
701
|
+
name,
|
|
702
|
+
handler
|
|
673
703
|
});
|
|
674
704
|
function sendEventNow(method, args) {
|
|
675
705
|
attached?.rpc.$callRaw({
|
|
@@ -716,7 +746,7 @@ function connectPanelChannel(options) {
|
|
|
716
746
|
reason: "the panel adopted a newer port"
|
|
717
747
|
});
|
|
718
748
|
attached = attachChannelPort(port, {
|
|
719
|
-
resolveLocal: (fnName) =>
|
|
749
|
+
resolveLocal: (fnName) => stateRegistry.resolve(fnName) ?? registry.resolve(fnName),
|
|
720
750
|
onControl: (kind) => {
|
|
721
751
|
if (kind === "ping") attached?.postControl("pong");
|
|
722
752
|
},
|
|
@@ -761,7 +791,7 @@ function connectPanelChannel(options) {
|
|
|
761
791
|
const tick = () => {
|
|
762
792
|
const hello = {
|
|
763
793
|
channel: IN_PAGE_CHANNEL_TAG,
|
|
764
|
-
v:
|
|
794
|
+
v: 2,
|
|
765
795
|
kind: "hello",
|
|
766
796
|
name,
|
|
767
797
|
panelId,
|
|
@@ -779,8 +809,8 @@ function connectPanelChannel(options) {
|
|
|
779
809
|
if (status === "closed") return;
|
|
780
810
|
const data = event.data;
|
|
781
811
|
if (!isHandshakeMessage(data) || data.kind !== "grant" || data.name !== name || data.panelId !== panelId) return;
|
|
782
|
-
if (data.v !==
|
|
783
|
-
warnOnce(`in-page channel "${name}": ignoring a grant with protocol version ${data.v} (this side speaks
|
|
812
|
+
if (data.v !== 2) {
|
|
813
|
+
warnOnce(`in-page channel "${name}": ignoring a grant with protocol version ${data.v} (this side speaks 2); align the devframe versions of the page script and the panel`);
|
|
784
814
|
return;
|
|
785
815
|
}
|
|
786
816
|
if (!allowedOrigins.includes("*") && !allowedOrigins.includes(event.origin)) {
|
|
@@ -825,9 +855,9 @@ function connectPanelChannel(options) {
|
|
|
825
855
|
}, timeoutMs);
|
|
826
856
|
});
|
|
827
857
|
},
|
|
828
|
-
call: (fnName, ...args) => enqueueCall(fnName, serializeArgs(codec, args)),
|
|
829
|
-
emit: (fnName, ...args) => sendEvent(fnName, serializeArgs(codec, args)),
|
|
830
|
-
callEvent: (fnName, ...args) => sendEvent(fnName, serializeArgs(codec, args)),
|
|
858
|
+
call: (fnName, ...args) => enqueueCall(channelMethod("function", fnName), serializeArgs(codec, args)),
|
|
859
|
+
emit: (fnName, ...args) => sendEvent(channelMethod("event", fnName), serializeArgs(codec, args)),
|
|
860
|
+
callEvent: (fnName, ...args) => sendEvent(channelMethod("event", fnName), serializeArgs(codec, args)),
|
|
831
861
|
on: (fnName, listener) => registry.on(fnName, listener),
|
|
832
862
|
sharedState: stateHost,
|
|
833
863
|
close: () => {
|
package/package.json
CHANGED
package/skills/devframe/SKILL.md
CHANGED
|
@@ -438,7 +438,7 @@ Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(...)` to re
|
|
|
438
438
|
|
|
439
439
|
### In-page channel (page script ↔ panel, server-free)
|
|
440
440
|
|
|
441
|
-
For a live inspect-the-page loop, `devframe/in-page-channel` connects a devframe's **page script** (in the user app's page) to its **panels** entirely in the browser
|
|
441
|
+
For a live inspect-the-page loop, `devframe/in-page-channel` connects a devframe's **page script** (in the user app's page) to its **panels** entirely in the browser, in both dev and static builds. Declare a shared protocol type with separate `functions` and `events` sections, each with `pageScript` and `panel` maps naming the receiving direction. Both `createPageScriptChannel<P>()` and `connectPanelChannel<P>()` require `{ name, functions }` and accept optional `events`: function declarations require handlers; event declarations accept optional handlers or `{}` for dynamic `channel.on()` subscriptions. `call()` awaits functions, including void actions; `emit()` sends declared events. `channel.sharedState.get(key)` mirrors `rpc.sharedState` with the page script as authority and automatic replay to panels. The handshake retries across boot order and reloads; panels expose `status`/`whenConnected(ms)` for page-script availability fallbacks. Channel names follow `devframes:plugin:<slug>`. The a11y inspector's scan/highlight loop is the reference use.
|
|
442
442
|
|
|
443
443
|
## The Hub
|
|
444
444
|
|