devframe 0.9.16 → 0.9.18
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,19 +184,25 @@ 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);
|
|
188
|
+
},
|
|
189
|
+
registerInternal(method, handler) {
|
|
190
|
+
definitions.set(method, {
|
|
191
|
+
name: method,
|
|
192
|
+
handler
|
|
193
|
+
});
|
|
184
194
|
},
|
|
185
195
|
on(name, listener) {
|
|
186
|
-
|
|
187
|
-
let registered = listeners.get(
|
|
196
|
+
const key = channelMethod("event", name);
|
|
197
|
+
let registered = listeners.get(key);
|
|
188
198
|
if (!registered) {
|
|
189
199
|
registered = /* @__PURE__ */ new Set();
|
|
190
|
-
listeners.set(
|
|
200
|
+
listeners.set(key, registered);
|
|
191
201
|
}
|
|
192
202
|
registered.add(listener);
|
|
193
203
|
return () => {
|
|
194
204
|
registered.delete(listener);
|
|
195
|
-
if (registered.size === 0) listeners.delete(
|
|
205
|
+
if (registered.size === 0) listeners.delete(key);
|
|
196
206
|
};
|
|
197
207
|
},
|
|
198
208
|
resolve(name) {
|
|
@@ -205,12 +215,28 @@ function createLocalFunctionRegistry(codec) {
|
|
|
205
215
|
if (definition?.args?.length) await validateArgs(definition.name, definition.args, args);
|
|
206
216
|
const result = await definition?.handler?.(...args);
|
|
207
217
|
for (const listener of [...listeners.get(name) ?? []]) listener(...args);
|
|
208
|
-
if (definition?.
|
|
218
|
+
if (definition?.type === "event") return void 0;
|
|
219
|
+
if (definition?.jsonSerializable && result !== void 0) assertJsonSerializable(result, "its return value", definition.name);
|
|
209
220
|
return codec.serialize && result !== void 0 ? codec.serialize(result) : result;
|
|
210
221
|
};
|
|
211
222
|
}
|
|
212
223
|
};
|
|
213
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Consult each registry in order and, only once none owns the name, fall back
|
|
227
|
+
* to the coded "not registered" error for a `function:` call (events stay
|
|
228
|
+
* silent). The fallback lives here, after every registry, so chaining a
|
|
229
|
+
* state registry ahead of the user registry never masks a real handler.
|
|
230
|
+
*/
|
|
231
|
+
function resolveLocalHandler(name, registries) {
|
|
232
|
+
for (const registry of registries) {
|
|
233
|
+
const handler = registry(name);
|
|
234
|
+
if (handler) return handler;
|
|
235
|
+
}
|
|
236
|
+
if (name.startsWith(FUNCTION_METHOD_PREFIX)) return () => {
|
|
237
|
+
throw diagnostics.DF0077({ name: name.slice(FUNCTION_METHOD_PREFIX.length) });
|
|
238
|
+
};
|
|
239
|
+
}
|
|
214
240
|
function wrapPostError(error) {
|
|
215
241
|
if (error instanceof DOMException && error.name === "DataCloneError") return new InPageChannelError("not-cloneable", `a payload could not be structured-cloned across the in-page channel: ${error.message}. Strip non-cloneable values (functions, DOM nodes, framework reactivity proxies) before sending: declare the function \`jsonSerializable: true\` for a precise error, or provide a \`serialize\` hook.`, { cause: error });
|
|
216
242
|
return error;
|
|
@@ -482,13 +508,18 @@ function createPageScriptChannel(options) {
|
|
|
482
508
|
...definition,
|
|
483
509
|
name: fnName
|
|
484
510
|
});
|
|
511
|
+
for (const [eventName, definition] of Object.entries(options.events ?? {})) registry.register({
|
|
512
|
+
...definition,
|
|
513
|
+
name: eventName,
|
|
514
|
+
type: "event"
|
|
515
|
+
});
|
|
485
516
|
const stateHost = createPageScriptStateHost(function* () {
|
|
486
517
|
for (const peer of peers.values()) yield {
|
|
487
518
|
subscribedStates: peer.subscribedStates,
|
|
488
519
|
callEventRaw: (method, args) => {
|
|
489
520
|
peer.attached.rpc.$callRaw({
|
|
490
521
|
method,
|
|
491
|
-
args,
|
|
522
|
+
args: serializeArgs(codec, args),
|
|
492
523
|
event: true,
|
|
493
524
|
optional: true
|
|
494
525
|
}).catch(() => {});
|
|
@@ -517,14 +548,16 @@ function createPageScriptChannel(options) {
|
|
|
517
548
|
callEventRaw: (method, args) => {
|
|
518
549
|
internal.attached.rpc.$callRaw({
|
|
519
550
|
method,
|
|
520
|
-
args,
|
|
551
|
+
args: serializeArgs(codec, args),
|
|
521
552
|
event: true,
|
|
522
553
|
optional: true
|
|
523
554
|
}).catch(() => {});
|
|
524
555
|
}
|
|
525
556
|
});
|
|
557
|
+
const stateRegistry = createLocalFunctionRegistry(codec);
|
|
558
|
+
for (const [method, handler] of Object.entries(internal.internalHandlers)) stateRegistry.registerInternal(method, handler);
|
|
526
559
|
internal.attached = attachChannelPort(port, {
|
|
527
|
-
resolveLocal: (fnName) =>
|
|
560
|
+
resolveLocal: (fnName) => resolveLocalHandler(fnName, [stateRegistry.resolve, registry.resolve]),
|
|
528
561
|
onControl: (kind) => {
|
|
529
562
|
if (kind === "ping") internal.attached.postControl("pong");
|
|
530
563
|
},
|
|
@@ -532,7 +565,7 @@ function createPageScriptChannel(options) {
|
|
|
532
565
|
});
|
|
533
566
|
internal.peer = {
|
|
534
567
|
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`),
|
|
568
|
+
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
569
|
close: () => removePeer(id, {
|
|
537
570
|
bye: true,
|
|
538
571
|
reason: "the page script closed this panel"
|
|
@@ -550,8 +583,8 @@ function createPageScriptChannel(options) {
|
|
|
550
583
|
if (closed) return;
|
|
551
584
|
const data = event.data;
|
|
552
585
|
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
|
|
586
|
+
if (data.v !== 2) {
|
|
587
|
+
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
588
|
return;
|
|
556
589
|
}
|
|
557
590
|
if (!allowedOrigins.includes("*") && !allowedOrigins.includes(event.origin)) {
|
|
@@ -565,7 +598,7 @@ function createPageScriptChannel(options) {
|
|
|
565
598
|
addPeer(messageChannel.port1, data.panelId);
|
|
566
599
|
const grant = {
|
|
567
600
|
channel: IN_PAGE_CHANNEL_TAG,
|
|
568
|
-
v:
|
|
601
|
+
v: 2,
|
|
569
602
|
kind: "grant",
|
|
570
603
|
name,
|
|
571
604
|
panelId: data.panelId,
|
|
@@ -582,7 +615,7 @@ function createPageScriptChannel(options) {
|
|
|
582
615
|
const emit = (fnName, ...args) => {
|
|
583
616
|
const wireArgs = serializeArgs(codec, args);
|
|
584
617
|
for (const peer of peers.values()) peer.attached.rpc.$callRaw({
|
|
585
|
-
method: fnName,
|
|
618
|
+
method: channelMethod("event", fnName),
|
|
586
619
|
args: wireArgs,
|
|
587
620
|
event: true,
|
|
588
621
|
optional: true
|
|
@@ -651,6 +684,11 @@ function connectPanelChannel(options) {
|
|
|
651
684
|
...definition,
|
|
652
685
|
name: fnName
|
|
653
686
|
});
|
|
687
|
+
for (const [eventName, definition] of Object.entries(options.events ?? {})) registry.register({
|
|
688
|
+
...definition,
|
|
689
|
+
name: eventName,
|
|
690
|
+
type: "event"
|
|
691
|
+
});
|
|
654
692
|
let status = "connecting";
|
|
655
693
|
let attached;
|
|
656
694
|
let pageScriptInfo;
|
|
@@ -668,9 +706,11 @@ function connectPanelChannel(options) {
|
|
|
668
706
|
}
|
|
669
707
|
const stateHost = createPanelStateHost({
|
|
670
708
|
isConnected: () => status === "connected",
|
|
671
|
-
callEvent: (method, args) => sendEvent(method, args),
|
|
672
|
-
call: (method, args) => enqueueCall(method, args)
|
|
709
|
+
callEvent: (method, args) => sendEvent(method, serializeArgs(codec, args)),
|
|
710
|
+
call: (method, args) => enqueueCall(method, serializeArgs(codec, args))
|
|
673
711
|
});
|
|
712
|
+
const stateRegistry = createLocalFunctionRegistry(codec);
|
|
713
|
+
for (const [method, handler] of Object.entries(stateHost.handlers)) stateRegistry.registerInternal(method, handler);
|
|
674
714
|
function sendEventNow(method, args) {
|
|
675
715
|
attached?.rpc.$callRaw({
|
|
676
716
|
method,
|
|
@@ -716,7 +756,7 @@ function connectPanelChannel(options) {
|
|
|
716
756
|
reason: "the panel adopted a newer port"
|
|
717
757
|
});
|
|
718
758
|
attached = attachChannelPort(port, {
|
|
719
|
-
resolveLocal: (fnName) =>
|
|
759
|
+
resolveLocal: (fnName) => resolveLocalHandler(fnName, [stateRegistry.resolve, registry.resolve]),
|
|
720
760
|
onControl: (kind) => {
|
|
721
761
|
if (kind === "ping") attached?.postControl("pong");
|
|
722
762
|
},
|
|
@@ -761,7 +801,7 @@ function connectPanelChannel(options) {
|
|
|
761
801
|
const tick = () => {
|
|
762
802
|
const hello = {
|
|
763
803
|
channel: IN_PAGE_CHANNEL_TAG,
|
|
764
|
-
v:
|
|
804
|
+
v: 2,
|
|
765
805
|
kind: "hello",
|
|
766
806
|
name,
|
|
767
807
|
panelId,
|
|
@@ -779,8 +819,8 @@ function connectPanelChannel(options) {
|
|
|
779
819
|
if (status === "closed") return;
|
|
780
820
|
const data = event.data;
|
|
781
821
|
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
|
|
822
|
+
if (data.v !== 2) {
|
|
823
|
+
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
824
|
return;
|
|
785
825
|
}
|
|
786
826
|
if (!allowedOrigins.includes("*") && !allowedOrigins.includes(event.origin)) {
|
|
@@ -825,9 +865,9 @@ function connectPanelChannel(options) {
|
|
|
825
865
|
}, timeoutMs);
|
|
826
866
|
});
|
|
827
867
|
},
|
|
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)),
|
|
868
|
+
call: (fnName, ...args) => enqueueCall(channelMethod("function", fnName), serializeArgs(codec, args)),
|
|
869
|
+
emit: (fnName, ...args) => sendEvent(channelMethod("event", fnName), serializeArgs(codec, args)),
|
|
870
|
+
callEvent: (fnName, ...args) => sendEvent(channelMethod("event", fnName), serializeArgs(codec, args)),
|
|
831
871
|
on: (fnName, listener) => registry.on(fnName, listener),
|
|
832
872
|
sharedState: stateHost,
|
|
833
873
|
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
|
|