devframe 0.9.5 → 0.9.7
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.
- package/README.md +1 -1
- package/dist/{_shared-CsBsnhuO.d.mts → _shared-BJ6AmwTu.d.mts} +1 -1
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +1 -1
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +3 -2
- package/dist/adapters/dev.d.mts +4 -4
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/initiate.d.mts +3 -3
- package/dist/adapters/initiate.mjs +1 -1
- package/dist/adapters/mcp.d.mts +30 -26
- package/dist/adapters/mcp.mjs +559 -1
- package/dist/cli/main.mjs +1 -1
- package/dist/client/index.d.mts +5 -5
- package/dist/client/index.mjs +1 -64
- package/dist/constants.d.mts +13 -1
- package/dist/constants.mjs +1 -1
- package/dist/{context-c0OlSc12.d.mts → context-BhBzOXVa.d.mts} +3 -3
- package/dist/{context-DO0Qu1O8.d.mts → context-HOnG-WvG.d.mts} +1 -1
- package/dist/{context-Cx9V9Grq.mjs → context-KUqAfawQ.mjs} +1 -1
- package/dist/{context-Bc5S40Qt.mjs → context-omlma_1n.mjs} +3 -150
- package/dist/{dev-BRQA42Lh.mjs → dev-Czit5IP4.mjs} +3 -3
- package/dist/{events-DzolaBEs.mjs → events-4daw0uK9.mjs} +16 -2
- package/dist/in-page-channel/index.d.mts +306 -0
- package/dist/in-page-channel/index.mjs +816 -0
- package/dist/{index-DOg4S5wk.d.mts → index-BzyopqxV.d.mts} +1 -1
- package/dist/{index-B_DPNIT4.d.mts → index-DcKxIMoI.d.mts} +2 -2
- package/dist/index.d.mts +4 -4
- package/dist/{instance-shell-CPuYY8pw.d.mts → instance-shell-Bh4l9Vz_.d.mts} +3 -3
- package/dist/{instance-shell-fB5PyVtR.mjs → instance-shell-CpVW4zG0.mjs} +15 -2
- package/dist/internal/index.d.mts +16 -6
- package/dist/internal/index.mjs +3 -3
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/auth.mjs +1 -1
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +1 -1
- package/dist/node/index.d.mts +3 -3
- package/dist/node/index.mjs +1 -1
- package/dist/recipes/common-rpc-functions.d.mts +3 -3
- package/dist/recipes/interactive-auth.d.mts +13 -2
- package/dist/recipes/interactive-auth.mjs +5 -1
- package/dist/{revoke-PihtvVil.mjs → revoke-COLW0cOi.mjs} +1 -1
- package/dist/rpc/dump.d.mts +1 -1
- package/dist/rpc/index.d.mts +2 -2
- package/dist/rpc/transports/sse-client.d.mts +1 -1
- package/dist/rpc/transports/sse-server.d.mts +2 -2
- package/dist/rpc/transports/ws-bun.d.mts +2 -2
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-deno.d.mts +2 -2
- package/dist/rpc/transports/ws-server.d.mts +1 -1
- package/dist/{rpc-core-Oyz-cJBC.d.mts → rpc-core-BHUsDl7s.d.mts} +3 -3
- package/dist/{host-agent-DbPWo0Bl.mjs → services-install-CqqQVls5.mjs} +150 -2
- package/dist/{sse-client-CdGD_B8_.d.mts → sse-client-DwX021ST.d.mts} +1 -1
- package/dist/types/index.d.mts +3 -3
- package/dist/{types-BBLeGfpt.d.mts → types-CMdPYhOx.d.mts} +1 -1
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/serve-static.mjs +1 -1
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/{ws-client-Bo2t6hES.d.mts → ws-client-7_lZi-B9.d.mts} +1 -1
- package/dist/{ws-server-DLtEoLdR.d.mts → ws-server-Di2oqr4q.d.mts} +1 -1
- package/package.json +2 -1
- package/skills/devframe/SKILL.md +86 -82
- package/dist/http-D7wWuy7-.mjs +0 -569
- package/dist/transports-vhizgqXM.mjs +0 -15
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import { t as DEVFRAME_EVENTS } from "../events-4daw0uK9.mjs";
|
|
2
|
+
import { createEventEmitter } from "../utils/events.mjs";
|
|
3
|
+
import { t as createSharedState } from "../shared-state-DoXutg_U.mjs";
|
|
4
|
+
import { nanoid } from "../utils/nanoid.mjs";
|
|
5
|
+
import { createBirpc } from "birpc";
|
|
6
|
+
//#region src/in-page-channel/protocol.ts
|
|
7
|
+
/**
|
|
8
|
+
* Wire protocol of the in-page channel handshake and its port-level control
|
|
9
|
+
* frames. The envelope is transport-neutral by design — it identifies the
|
|
10
|
+
* protocol (`channel` tag + `v`), the user channel (`name`), and the peers
|
|
11
|
+
* (`panelId`, `instanceId`) — so a future cross-tab transport (e.g. a
|
|
12
|
+
* `BroadcastChannel`) can reuse it unchanged.
|
|
13
|
+
*/
|
|
14
|
+
/** `postMessage` tag every handshake message carries. */
|
|
15
|
+
const IN_PAGE_CHANNEL_TAG = DEVFRAME_EVENTS.postMessage.inPageChannel;
|
|
16
|
+
function isControlFrame(data) {
|
|
17
|
+
return !!data && typeof data === "object" && "__dfIpc" in data;
|
|
18
|
+
}
|
|
19
|
+
function isHandshakeMessage(data) {
|
|
20
|
+
if (!data || typeof data !== "object") return false;
|
|
21
|
+
const message = data;
|
|
22
|
+
return message.channel === IN_PAGE_CHANNEL_TAG && typeof message.name === "string" && typeof message.panelId === "string" && (message.kind === "hello" || message.kind === "grant");
|
|
23
|
+
}
|
|
24
|
+
const INSTANCE_STORAGE_KEY = "devframe:in-page-channel:instance";
|
|
25
|
+
let memoryInstanceId;
|
|
26
|
+
/**
|
|
27
|
+
* The page context's instance id: one nanoid per browser tab, persisted in
|
|
28
|
+
* `sessionStorage` so it survives page-script reloads. It scopes handshakes
|
|
29
|
+
* when the same app is open in several tabs — a panel pinned to an instance
|
|
30
|
+
* id ignores grants from every other tab's page script.
|
|
31
|
+
*/
|
|
32
|
+
function resolveInstanceId(win) {
|
|
33
|
+
try {
|
|
34
|
+
const storage = win?.sessionStorage;
|
|
35
|
+
if (storage) {
|
|
36
|
+
let id = storage.getItem(INSTANCE_STORAGE_KEY);
|
|
37
|
+
if (!id) {
|
|
38
|
+
id = nanoid();
|
|
39
|
+
storage.setItem(INSTANCE_STORAGE_KEY, id);
|
|
40
|
+
}
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
memoryInstanceId ??= nanoid();
|
|
45
|
+
return memoryInstanceId;
|
|
46
|
+
}
|
|
47
|
+
/** Resolve the origins accepted during the handshake (default: same-origin). */
|
|
48
|
+
function resolveAllowedOrigins(allowedOrigins, win) {
|
|
49
|
+
if (allowedOrigins?.length) return allowedOrigins;
|
|
50
|
+
const origin = win?.location?.origin;
|
|
51
|
+
return origin && origin !== "null" ? [origin] : ["*"];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Default handshake targets of a panel: its ancestor chain plus its
|
|
55
|
+
* `opener` — every same-tab window a page script can live in. `WindowProxy`
|
|
56
|
+
* references stay valid across navigations, so hellos posted to these reach
|
|
57
|
+
* a page script even after the host page reloads.
|
|
58
|
+
*/
|
|
59
|
+
function defaultHandshakeTargets(win) {
|
|
60
|
+
const targets = [];
|
|
61
|
+
try {
|
|
62
|
+
let current = win;
|
|
63
|
+
while (current.parent && current.parent !== current) {
|
|
64
|
+
targets.push(current.parent);
|
|
65
|
+
current = current.parent;
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
try {
|
|
69
|
+
const opener = win.opener;
|
|
70
|
+
if (opener && opener !== win) targets.push(opener);
|
|
71
|
+
} catch {}
|
|
72
|
+
return targets;
|
|
73
|
+
}
|
|
74
|
+
const DEFAULT_HEARTBEAT = {
|
|
75
|
+
intervalMs: 5e3,
|
|
76
|
+
timeoutMs: 12e3
|
|
77
|
+
};
|
|
78
|
+
/** Resolve the heartbeat option against its defaults. */
|
|
79
|
+
function resolveHeartbeat(option) {
|
|
80
|
+
return option === false ? void 0 : {
|
|
81
|
+
...DEFAULT_HEARTBEAT,
|
|
82
|
+
...option
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** A coded in-page channel error; every failure mode carries a stable `code`. */
|
|
86
|
+
var InPageChannelError = class extends Error {
|
|
87
|
+
code;
|
|
88
|
+
name = "InPageChannelError";
|
|
89
|
+
constructor(code, message, options) {
|
|
90
|
+
super(message, options);
|
|
91
|
+
this.code = code;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const warned = /* @__PURE__ */ new Set();
|
|
95
|
+
/**
|
|
96
|
+
* `console.warn` once per distinct message — handshake noise (foreign
|
|
97
|
+
* origins, version mismatches, missing targets) repeats on every retry tick.
|
|
98
|
+
*/
|
|
99
|
+
function warnOnce(message) {
|
|
100
|
+
if (!warned.has(message)) {
|
|
101
|
+
warned.add(message);
|
|
102
|
+
console.warn(`[devframe] ${message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function jsonViolation(value, path, seen) {
|
|
106
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return void 0;
|
|
107
|
+
if (typeof value === "number") return Number.isFinite(value) ? void 0 : `${path} is ${value}`;
|
|
108
|
+
if (typeof value !== "object") return `${path} is ${value === void 0 ? "`undefined`" : `a ${typeof value}`}`;
|
|
109
|
+
if (seen.has(value)) return `${path} is circular`;
|
|
110
|
+
seen.add(value);
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
for (let i = 0; i < value.length; i++) {
|
|
113
|
+
const violation = jsonViolation(value[i], `${path}[${i}]`, seen);
|
|
114
|
+
if (violation) return violation;
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const proto = Object.getPrototypeOf(value);
|
|
119
|
+
if (proto !== Object.prototype && proto !== null) return `${path} is an instance of ${value.constructor?.name ?? "an exotic class"}`;
|
|
120
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
121
|
+
const violation = jsonViolation(entry, `${path}.${key}`, seen);
|
|
122
|
+
if (violation) return violation;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Enforce a `jsonSerializable: true` contract: throws code
|
|
127
|
+
* `not-serializable` naming the offending path when the value contains
|
|
128
|
+
* anything strict JSON can't represent — surfacing the bug at the offending
|
|
129
|
+
* call instead of a silent coercion later.
|
|
130
|
+
*/
|
|
131
|
+
function assertJsonSerializable(value, what, functionName) {
|
|
132
|
+
const violation = jsonViolation(value, what, /* @__PURE__ */ new Set());
|
|
133
|
+
if (violation) throw new InPageChannelError("not-serializable", `in-page function "${functionName}" is declared jsonSerializable, but ${violation}`);
|
|
134
|
+
}
|
|
135
|
+
function formatIssues(issues) {
|
|
136
|
+
return issues.map((issue) => {
|
|
137
|
+
const path = issue.path?.map((s) => typeof s === "object" ? s.key : s).join(".");
|
|
138
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
139
|
+
}).join("; ");
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Validate positional arguments against their Standard Schemas (mirrors the
|
|
143
|
+
* RPC layer's `validateRpcArgs`): only indices with a schema are checked,
|
|
144
|
+
* values pass through unchanged, the first failure rejects with code
|
|
145
|
+
* `invalid-args`.
|
|
146
|
+
*/
|
|
147
|
+
async function validateArgs(name, schemas, args) {
|
|
148
|
+
for (let index = 0; index < schemas.length; index++) {
|
|
149
|
+
const schema = schemas[index];
|
|
150
|
+
if (!schema) continue;
|
|
151
|
+
const result = await schema["~standard"].validate(args[index]);
|
|
152
|
+
if (result.issues) throw new InPageChannelError("invalid-args", `in-page function "${name}" rejected argument ${index}: ${formatIssues(result.issues)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function serializeArgs(codec, args) {
|
|
156
|
+
return codec.serialize ? args.map(codec.serialize) : args;
|
|
157
|
+
}
|
|
158
|
+
function deserializeResult(codec, result) {
|
|
159
|
+
return codec.deserialize && result !== void 0 ? codec.deserialize(result) : result;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* An endpoint's local function table, resolved by name when the remote side
|
|
163
|
+
* calls in. Each handler is wrapped with the receive pipeline: deserialize
|
|
164
|
+
* hook, `jsonSerializable` enforcement, Standard-Schema argument validation,
|
|
165
|
+
* then serialize hook + `jsonSerializable` enforcement on the result.
|
|
166
|
+
*/
|
|
167
|
+
function createLocalFunctionRegistry(codec) {
|
|
168
|
+
const wrapped = /* @__PURE__ */ new Map();
|
|
169
|
+
return {
|
|
170
|
+
register(definition) {
|
|
171
|
+
wrapped.set(definition.name, async (...rawArgs) => {
|
|
172
|
+
const args = codec.deserialize ? rawArgs.map(codec.deserialize) : rawArgs;
|
|
173
|
+
if (definition.jsonSerializable) assertJsonSerializable(args, "its arguments", definition.name);
|
|
174
|
+
if (definition.args?.length) await validateArgs(definition.name, definition.args, args);
|
|
175
|
+
const result = await definition.handler(...args);
|
|
176
|
+
if (definition.jsonSerializable) assertJsonSerializable(result, "its return value", definition.name);
|
|
177
|
+
return codec.serialize && result !== void 0 ? codec.serialize(result) : result;
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
resolve: (name) => wrapped.get(name)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function wrapPostError(error) {
|
|
184
|
+
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 });
|
|
185
|
+
return error;
|
|
186
|
+
}
|
|
187
|
+
function attachChannelPort(port, options) {
|
|
188
|
+
let birpcHandler;
|
|
189
|
+
let disposed = false;
|
|
190
|
+
const attached = {
|
|
191
|
+
rpc: void 0,
|
|
192
|
+
lastActivity: Date.now(),
|
|
193
|
+
postControl(kind) {
|
|
194
|
+
try {
|
|
195
|
+
port.postMessage({ __dfIpc: kind });
|
|
196
|
+
} catch {}
|
|
197
|
+
},
|
|
198
|
+
dispose(disposeOptions) {
|
|
199
|
+
if (disposed) return;
|
|
200
|
+
disposed = true;
|
|
201
|
+
if (disposeOptions?.bye) attached.postControl("bye");
|
|
202
|
+
const error = new InPageChannelError("closed", `in-page channel call dropped: ${disposeOptions?.reason ?? "the in-page channel port was closed"}`);
|
|
203
|
+
attached.rpc.$rejectPendingCalls(({ reject }) => reject(error));
|
|
204
|
+
attached.rpc.$close();
|
|
205
|
+
port.removeEventListener("message", onMessage);
|
|
206
|
+
port.removeEventListener("close", onClose);
|
|
207
|
+
try {
|
|
208
|
+
port.close();
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
function onMessage(event) {
|
|
213
|
+
attached.lastActivity = Date.now();
|
|
214
|
+
const data = event.data;
|
|
215
|
+
if (isControlFrame(data)) {
|
|
216
|
+
if (data.__dfIpc === "bye") options.onPeerClosed();
|
|
217
|
+
else options.onControl(data.__dfIpc);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
birpcHandler?.(data);
|
|
221
|
+
}
|
|
222
|
+
function onClose() {
|
|
223
|
+
options.onPeerClosed();
|
|
224
|
+
}
|
|
225
|
+
attached.rpc = createBirpc({}, {
|
|
226
|
+
post: (data) => {
|
|
227
|
+
try {
|
|
228
|
+
port.postMessage(data);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
throw wrapPostError(error);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
on: (fn) => {
|
|
234
|
+
birpcHandler = fn;
|
|
235
|
+
},
|
|
236
|
+
off: () => {
|
|
237
|
+
birpcHandler = void 0;
|
|
238
|
+
},
|
|
239
|
+
timeout: -1,
|
|
240
|
+
proxify: false,
|
|
241
|
+
resolver: (name, resolved) => resolved ?? options.resolveLocal(name)
|
|
242
|
+
});
|
|
243
|
+
port.addEventListener("message", onMessage);
|
|
244
|
+
port.addEventListener("close", onClose);
|
|
245
|
+
port.start?.();
|
|
246
|
+
return attached;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Race a call against its deadline, rejecting with a status-aware error
|
|
250
|
+
* (code `timeout`). `ms <= 0` disables the deadline.
|
|
251
|
+
*/
|
|
252
|
+
function withCallDeadline(promise, ms, describeTimeout) {
|
|
253
|
+
if (ms <= 0) return promise;
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const timer = setTimeout(() => reject(new InPageChannelError("timeout", describeTimeout())), ms);
|
|
256
|
+
promise.then((value) => {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
resolve(value);
|
|
259
|
+
}, (error) => {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
reject(error);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/in-page-channel/state.ts
|
|
267
|
+
/**
|
|
268
|
+
* The channel shared-state layer — `createSharedState` pumped over the
|
|
269
|
+
* in-page channel, mirroring the RPC shared-state wire design with the page
|
|
270
|
+
* script playing the server's role: it owns the canonical value, replays a
|
|
271
|
+
* snapshot to every (re)subscribing panel, and fans syncId-deduplicated
|
|
272
|
+
* updates out to the panels subscribed to each key.
|
|
273
|
+
*
|
|
274
|
+
* Request endpoints (panel → page script) are defined here at their
|
|
275
|
+
* handlers, like the RPC layer's `devframe:rpc:server-state:*`; the
|
|
276
|
+
* page-script → panel notifications live in `DEVFRAME_EVENTS.inPageChannel`.
|
|
277
|
+
*/
|
|
278
|
+
const IN_PAGE_STATE_RPC = {
|
|
279
|
+
/** Subscribe to a key; returns the authority's current snapshot. */
|
|
280
|
+
subscribe: "devframe:in-page:page-state:subscribe",
|
|
281
|
+
/** Replace a key's value (panel mutation forwarded up). */
|
|
282
|
+
set: "devframe:in-page:page-state:set",
|
|
283
|
+
/** Patch a key's value (panel mutation forwarded up). */
|
|
284
|
+
patch: "devframe:in-page:page-state:patch"
|
|
285
|
+
};
|
|
286
|
+
/** The page-script (authority) half of the channel shared-state layer. */
|
|
287
|
+
function createPageScriptStateHost(peers) {
|
|
288
|
+
const states = /* @__PURE__ */ new Map();
|
|
289
|
+
return {
|
|
290
|
+
get: async (key, options) => {
|
|
291
|
+
const existing = states.get(key);
|
|
292
|
+
if (existing) return existing;
|
|
293
|
+
if (options?.initialValue === void 0) throw new InPageChannelError("state-uninitialized", `in-page shared state "${key}" was accessed before initialization — the page script is the authority, so its first \`sharedState.get("${key}")\` must provide \`initialValue\``);
|
|
294
|
+
const state = createSharedState({
|
|
295
|
+
initialValue: options.initialValue,
|
|
296
|
+
enablePatches: true
|
|
297
|
+
});
|
|
298
|
+
states.set(key, state);
|
|
299
|
+
state.on("updated", (fullState, patches, syncId) => {
|
|
300
|
+
for (const peer of peers()) {
|
|
301
|
+
if (!peer.subscribedStates.has(key)) continue;
|
|
302
|
+
if (patches) peer.callEventRaw(DEVFRAME_EVENTS.inPageChannel.panelStatePatch, [
|
|
303
|
+
key,
|
|
304
|
+
patches,
|
|
305
|
+
syncId
|
|
306
|
+
]);
|
|
307
|
+
else peer.callEventRaw(DEVFRAME_EVENTS.inPageChannel.panelStateUpdated, [
|
|
308
|
+
key,
|
|
309
|
+
fullState,
|
|
310
|
+
syncId
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return state;
|
|
315
|
+
},
|
|
316
|
+
createPeerHandlers(peer) {
|
|
317
|
+
return {
|
|
318
|
+
[IN_PAGE_STATE_RPC.subscribe]: (key) => {
|
|
319
|
+
peer.subscribedStates.add(key);
|
|
320
|
+
return states.get(key)?.value();
|
|
321
|
+
},
|
|
322
|
+
[IN_PAGE_STATE_RPC.set]: (key, fullState, syncId) => {
|
|
323
|
+
const state = states.get(key);
|
|
324
|
+
if (state && !state.syncIds.has(syncId)) state.mutate(() => fullState, syncId);
|
|
325
|
+
},
|
|
326
|
+
[IN_PAGE_STATE_RPC.patch]: (key, patches, syncId) => {
|
|
327
|
+
states.get(key)?.patch(patches, syncId);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/** The panel (mirror) half of the channel shared-state layer. */
|
|
334
|
+
function createPanelStateHost(options) {
|
|
335
|
+
const states = /* @__PURE__ */ new Map();
|
|
336
|
+
const seeded = /* @__PURE__ */ new Set();
|
|
337
|
+
const seedWaiters = /* @__PURE__ */ new Map();
|
|
338
|
+
/**
|
|
339
|
+
* SyncIds of snapshot adoptions. Adopting the authority's replay must not
|
|
340
|
+
* echo straight back up as a `set` — the authority already has the value.
|
|
341
|
+
*/
|
|
342
|
+
const adoptedSyncIds = /* @__PURE__ */ new Set();
|
|
343
|
+
function markSeeded(key) {
|
|
344
|
+
seeded.add(key);
|
|
345
|
+
const waiters = seedWaiters.get(key);
|
|
346
|
+
if (waiters) {
|
|
347
|
+
seedWaiters.delete(key);
|
|
348
|
+
for (const resolve of waiters) resolve();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function adopt(key, snapshot) {
|
|
352
|
+
const state = states.get(key);
|
|
353
|
+
if (state && snapshot !== void 0) {
|
|
354
|
+
const syncId = nanoid();
|
|
355
|
+
adoptedSyncIds.add(syncId);
|
|
356
|
+
state.mutate(() => snapshot, syncId);
|
|
357
|
+
}
|
|
358
|
+
markSeeded(key);
|
|
359
|
+
}
|
|
360
|
+
function subscribeNow(key) {
|
|
361
|
+
options.call(IN_PAGE_STATE_RPC.subscribe, [key]).then((snapshot) => adopt(key, snapshot)).catch((error) => {
|
|
362
|
+
console.warn(`[devframe] in-page shared state "${key}": subscribe failed`, error);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
handlers: {
|
|
367
|
+
[DEVFRAME_EVENTS.inPageChannel.panelStateUpdated]: (key, fullState, syncId) => {
|
|
368
|
+
const state = states.get(key);
|
|
369
|
+
if (state && !state.syncIds.has(syncId)) state.mutate(() => fullState, syncId);
|
|
370
|
+
markSeeded(key);
|
|
371
|
+
},
|
|
372
|
+
[DEVFRAME_EVENTS.inPageChannel.panelStatePatch]: (key, patches, syncId) => {
|
|
373
|
+
states.get(key)?.patch(patches, syncId);
|
|
374
|
+
markSeeded(key);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
resubscribe() {
|
|
378
|
+
for (const key of states.keys()) subscribeNow(key);
|
|
379
|
+
},
|
|
380
|
+
get: (key, getOptions) => {
|
|
381
|
+
const existing = states.get(key);
|
|
382
|
+
if (existing) return Promise.resolve(existing);
|
|
383
|
+
const state = createSharedState({
|
|
384
|
+
initialValue: getOptions?.initialValue,
|
|
385
|
+
enablePatches: true
|
|
386
|
+
});
|
|
387
|
+
states.set(key, state);
|
|
388
|
+
state.on("updated", (fullState, patches, syncId) => {
|
|
389
|
+
if (adoptedSyncIds.delete(syncId)) return;
|
|
390
|
+
if (!options.isConnected()) return;
|
|
391
|
+
if (patches) options.callEvent(IN_PAGE_STATE_RPC.patch, [
|
|
392
|
+
key,
|
|
393
|
+
patches,
|
|
394
|
+
syncId
|
|
395
|
+
]);
|
|
396
|
+
else options.callEvent(IN_PAGE_STATE_RPC.set, [
|
|
397
|
+
key,
|
|
398
|
+
fullState,
|
|
399
|
+
syncId
|
|
400
|
+
]);
|
|
401
|
+
});
|
|
402
|
+
if (options.isConnected()) subscribeNow(key);
|
|
403
|
+
if (getOptions?.initialValue !== void 0) return Promise.resolve(state);
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
if (seeded.has(key)) {
|
|
406
|
+
resolve(state);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const waiters = seedWaiters.get(key) ?? [];
|
|
410
|
+
waiters.push(() => resolve(state));
|
|
411
|
+
seedWaiters.set(key, waiters);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/in-page-channel/page-script.ts
|
|
418
|
+
/**
|
|
419
|
+
* Create the page-script endpoint of an in-page channel.
|
|
420
|
+
*
|
|
421
|
+
* It listens for panel hellos on the host page's window and answers each
|
|
422
|
+
* with a dedicated `MessageChannel` port (same-origin enforced both ways),
|
|
423
|
+
* keeping one live peer per panel — dock iframe, popup, and Document-PiP
|
|
424
|
+
* panels all handshake the same way, and a panel reload is simply a new
|
|
425
|
+
* handshake. No server is involved at any point.
|
|
426
|
+
*/
|
|
427
|
+
function createPageScriptChannel(options) {
|
|
428
|
+
const { name } = options;
|
|
429
|
+
const callTimeoutMs = options.callTimeoutMs ?? 15e3;
|
|
430
|
+
const win = options.window === false ? void 0 : options.window ?? (typeof window === "undefined" ? void 0 : window);
|
|
431
|
+
const allowedOrigins = resolveAllowedOrigins(options.allowedOrigins, win);
|
|
432
|
+
const instanceId = resolveInstanceId(win);
|
|
433
|
+
const heartbeat = resolveHeartbeat(options.heartbeat);
|
|
434
|
+
const codec = {
|
|
435
|
+
serialize: options.serialize,
|
|
436
|
+
deserialize: options.deserialize
|
|
437
|
+
};
|
|
438
|
+
const events = createEventEmitter();
|
|
439
|
+
const peers = /* @__PURE__ */ new Map();
|
|
440
|
+
let closed = false;
|
|
441
|
+
let heartbeatTimer;
|
|
442
|
+
const registry = createLocalFunctionRegistry(codec);
|
|
443
|
+
for (const definition of options.functions ?? []) registry.register(definition);
|
|
444
|
+
const stateHost = createPageScriptStateHost(function* () {
|
|
445
|
+
for (const peer of peers.values()) yield {
|
|
446
|
+
subscribedStates: peer.subscribedStates,
|
|
447
|
+
callEventRaw: (method, args) => {
|
|
448
|
+
peer.attached.rpc.$callRaw({
|
|
449
|
+
method,
|
|
450
|
+
args,
|
|
451
|
+
event: true,
|
|
452
|
+
optional: true
|
|
453
|
+
}).catch(() => {});
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
function removePeer(id, disposeOptions) {
|
|
458
|
+
const peer = peers.get(id);
|
|
459
|
+
if (!peer) return;
|
|
460
|
+
peers.delete(id);
|
|
461
|
+
peer.attached.dispose(disposeOptions);
|
|
462
|
+
if (peers.size === 0 && heartbeatTimer) {
|
|
463
|
+
clearInterval(heartbeatTimer);
|
|
464
|
+
heartbeatTimer = void 0;
|
|
465
|
+
}
|
|
466
|
+
events.emit("panel:disconnected", peer.peer);
|
|
467
|
+
}
|
|
468
|
+
function addPeer(port, id) {
|
|
469
|
+
removePeer(id, { reason: "the panel re-connected" });
|
|
470
|
+
const internal = {
|
|
471
|
+
id,
|
|
472
|
+
subscribedStates: /* @__PURE__ */ new Set()
|
|
473
|
+
};
|
|
474
|
+
internal.internalHandlers = stateHost.createPeerHandlers({
|
|
475
|
+
subscribedStates: internal.subscribedStates,
|
|
476
|
+
callEventRaw: (method, args) => {
|
|
477
|
+
internal.attached.rpc.$callRaw({
|
|
478
|
+
method,
|
|
479
|
+
args,
|
|
480
|
+
event: true,
|
|
481
|
+
optional: true
|
|
482
|
+
}).catch(() => {});
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
internal.attached = attachChannelPort(port, {
|
|
486
|
+
resolveLocal: (fnName) => internal.internalHandlers[fnName] ?? registry.resolve(fnName),
|
|
487
|
+
onControl: (kind) => {
|
|
488
|
+
if (kind === "ping") internal.attached.postControl("pong");
|
|
489
|
+
},
|
|
490
|
+
onPeerClosed: () => removePeer(id, { reason: "the panel disconnected" })
|
|
491
|
+
});
|
|
492
|
+
internal.peer = {
|
|
493
|
+
id,
|
|
494
|
+
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`),
|
|
495
|
+
close: () => removePeer(id, {
|
|
496
|
+
bye: true,
|
|
497
|
+
reason: "the page script closed this panel"
|
|
498
|
+
})
|
|
499
|
+
};
|
|
500
|
+
peers.set(id, internal);
|
|
501
|
+
if (heartbeat && !heartbeatTimer) heartbeatTimer = setInterval(() => {
|
|
502
|
+
const now = Date.now();
|
|
503
|
+
for (const peer of [...peers.values()]) if (now - peer.attached.lastActivity > heartbeat.timeoutMs) removePeer(peer.id, { reason: "the panel went silent (heartbeat timeout)" });
|
|
504
|
+
}, heartbeat.intervalMs);
|
|
505
|
+
events.emit("panel:connected", internal.peer);
|
|
506
|
+
return internal.peer;
|
|
507
|
+
}
|
|
508
|
+
const onWindowMessage = (event) => {
|
|
509
|
+
if (closed) return;
|
|
510
|
+
const data = event.data;
|
|
511
|
+
if (!isHandshakeMessage(data) || data.kind !== "hello" || data.name !== name) return;
|
|
512
|
+
if (data.v !== 1) {
|
|
513
|
+
warnOnce(`in-page channel "${name}": ignoring a hello with protocol version ${data.v} (this side speaks 1) — align the devframe versions of the page script and the panel`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (!allowedOrigins.includes("*") && !allowedOrigins.includes(event.origin)) {
|
|
517
|
+
warnOnce(`in-page channel "${name}": ignoring a hello from disallowed origin "${event.origin}"`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (data.instanceId && data.instanceId !== instanceId) return;
|
|
521
|
+
const source = event.source;
|
|
522
|
+
if (!source || typeof source.postMessage !== "function") return;
|
|
523
|
+
const messageChannel = new MessageChannel();
|
|
524
|
+
addPeer(messageChannel.port1, data.panelId);
|
|
525
|
+
const grant = {
|
|
526
|
+
channel: IN_PAGE_CHANNEL_TAG,
|
|
527
|
+
v: 1,
|
|
528
|
+
kind: "grant",
|
|
529
|
+
name,
|
|
530
|
+
panelId: data.panelId,
|
|
531
|
+
instanceId
|
|
532
|
+
};
|
|
533
|
+
try {
|
|
534
|
+
source.postMessage(grant, allowedOrigins.includes("*") ? "*" : event.origin, [messageChannel.port2]);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.warn(`[devframe] in-page channel "${name}": failed to grant a port`, error);
|
|
537
|
+
removePeer(data.panelId, { reason: "the grant could not be delivered" });
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
win?.addEventListener("message", onWindowMessage);
|
|
541
|
+
return {
|
|
542
|
+
name,
|
|
543
|
+
instanceId,
|
|
544
|
+
get panels() {
|
|
545
|
+
return [...peers.values()].map((peer) => peer.peer);
|
|
546
|
+
},
|
|
547
|
+
events: {
|
|
548
|
+
on: events.on,
|
|
549
|
+
once: events.once
|
|
550
|
+
},
|
|
551
|
+
callEvent: (fnName, ...args) => {
|
|
552
|
+
const wireArgs = serializeArgs(codec, args);
|
|
553
|
+
for (const peer of peers.values()) peer.attached.rpc.$callRaw({
|
|
554
|
+
method: fnName,
|
|
555
|
+
args: wireArgs,
|
|
556
|
+
event: true,
|
|
557
|
+
optional: true
|
|
558
|
+
}).catch(() => {});
|
|
559
|
+
},
|
|
560
|
+
sharedState: stateHost,
|
|
561
|
+
addPanelPort: (port) => addPeer(port, `transport:${nanoid(8)}`),
|
|
562
|
+
close: () => {
|
|
563
|
+
if (closed) return;
|
|
564
|
+
closed = true;
|
|
565
|
+
win?.removeEventListener("message", onWindowMessage);
|
|
566
|
+
for (const id of [...peers.keys()]) removePeer(id, {
|
|
567
|
+
bye: true,
|
|
568
|
+
reason: "the page script closed the channel"
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/in-page-channel/panel.ts
|
|
575
|
+
const DEFAULT_HELLO_INTERVAL_MS = 300;
|
|
576
|
+
const HELLO_INTERVAL_CAP_MS = 3e3;
|
|
577
|
+
const DEFAULT_EVENT_BUFFER_LIMIT = 64;
|
|
578
|
+
/**
|
|
579
|
+
* Connect the panel endpoint of an in-page channel.
|
|
580
|
+
*
|
|
581
|
+
* The panel initiates: it posts a versioned hello to every window a
|
|
582
|
+
* same-tab page script can live in (its ancestor chain and its `opener`),
|
|
583
|
+
* retrying with backoff until one answers with a dedicated port — so boot
|
|
584
|
+
* order never matters, and a reload of either side is just a re-handshake
|
|
585
|
+
* (`WindowProxy` references survive navigations). While `connecting`,
|
|
586
|
+
* outgoing calls and events are buffered; when no page script exists at all
|
|
587
|
+
* (e.g. the panel opened standalone), the endpoint stays `connecting` and
|
|
588
|
+
* the UI can key a fallback state off `status` / `whenConnected()`.
|
|
589
|
+
*/
|
|
590
|
+
function connectPanelChannel(options) {
|
|
591
|
+
const { name } = options;
|
|
592
|
+
const callTimeoutMs = options.callTimeoutMs ?? 15e3;
|
|
593
|
+
const eventBufferLimit = options.eventBufferLimit ?? DEFAULT_EVENT_BUFFER_LIMIT;
|
|
594
|
+
const win = options.window === false ? void 0 : options.window ?? (typeof window === "undefined" ? void 0 : window);
|
|
595
|
+
const allowedOrigins = resolveAllowedOrigins(options.allowedOrigins, win);
|
|
596
|
+
const heartbeat = resolveHeartbeat(options.heartbeat);
|
|
597
|
+
const codec = {
|
|
598
|
+
serialize: options.serialize,
|
|
599
|
+
deserialize: options.deserialize
|
|
600
|
+
};
|
|
601
|
+
const panelId = nanoid();
|
|
602
|
+
const targets = options.targets ?? (win ? defaultHandshakeTargets(win) : []);
|
|
603
|
+
const canHandshake = !!win && targets.length > 0;
|
|
604
|
+
const events = createEventEmitter();
|
|
605
|
+
const registry = createLocalFunctionRegistry(codec);
|
|
606
|
+
for (const definition of options.functions ?? []) registry.register(definition);
|
|
607
|
+
let status = "connecting";
|
|
608
|
+
let attached;
|
|
609
|
+
let pageScriptInfo;
|
|
610
|
+
let helloTimer;
|
|
611
|
+
let heartbeatTimer;
|
|
612
|
+
let droppedEventsWarned = false;
|
|
613
|
+
const pendingCalls = [];
|
|
614
|
+
const eventBuffer = [];
|
|
615
|
+
const connectedWaiters = [];
|
|
616
|
+
function setStatus(next) {
|
|
617
|
+
if (status !== next) {
|
|
618
|
+
status = next;
|
|
619
|
+
events.emit("status:updated", next);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const stateHost = createPanelStateHost({
|
|
623
|
+
isConnected: () => status === "connected",
|
|
624
|
+
callEvent: (method, args) => sendEvent(method, args),
|
|
625
|
+
call: (method, args) => enqueueCall(method, args)
|
|
626
|
+
});
|
|
627
|
+
function sendEventNow(method, args) {
|
|
628
|
+
attached?.rpc.$callRaw({
|
|
629
|
+
method,
|
|
630
|
+
args,
|
|
631
|
+
event: true,
|
|
632
|
+
optional: true
|
|
633
|
+
}).catch(() => {});
|
|
634
|
+
}
|
|
635
|
+
function sendEvent(method, args) {
|
|
636
|
+
if (status === "closed") return;
|
|
637
|
+
if (status === "connected" && attached) {
|
|
638
|
+
sendEventNow(method, args);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (eventBuffer.length >= eventBufferLimit) {
|
|
642
|
+
eventBuffer.shift();
|
|
643
|
+
if (!droppedEventsWarned) {
|
|
644
|
+
droppedEventsWarned = true;
|
|
645
|
+
console.warn(`[devframe] in-page channel "${name}": event buffer overflowed while connecting — oldest events are being dropped (limit ${eventBufferLimit})`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
eventBuffer.push({
|
|
649
|
+
method,
|
|
650
|
+
args
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function enqueueCall(method, args) {
|
|
654
|
+
if (status === "closed") return Promise.reject(new InPageChannelError("closed", `in-page channel "${name}": call "${method}" rejected — the channel is closed`));
|
|
655
|
+
return withCallDeadline(new Promise((resolve, reject) => {
|
|
656
|
+
const run = () => {
|
|
657
|
+
attached.rpc.$call(method, ...args).then((result) => resolve(deserializeResult(codec, result)), reject);
|
|
658
|
+
};
|
|
659
|
+
if (status === "connected" && attached) run();
|
|
660
|
+
else pendingCalls.push({
|
|
661
|
+
run,
|
|
662
|
+
reject
|
|
663
|
+
});
|
|
664
|
+
}), callTimeoutMs, () => `in-page channel "${name}": call "${method}" timed out after ${callTimeoutMs}ms (status: ${status}${status === "connecting" ? " — is the page script loaded?" : ""})`);
|
|
665
|
+
}
|
|
666
|
+
function adoptPort(port, info) {
|
|
667
|
+
attached?.dispose({
|
|
668
|
+
bye: true,
|
|
669
|
+
reason: "the panel adopted a newer port"
|
|
670
|
+
});
|
|
671
|
+
attached = attachChannelPort(port, {
|
|
672
|
+
resolveLocal: (fnName) => stateHost.handlers[fnName] ?? registry.resolve(fnName),
|
|
673
|
+
onControl: (kind) => {
|
|
674
|
+
if (kind === "ping") attached?.postControl("pong");
|
|
675
|
+
},
|
|
676
|
+
onPeerClosed: () => handleDisconnect("the page script went away")
|
|
677
|
+
});
|
|
678
|
+
pageScriptInfo = info;
|
|
679
|
+
stopTimers();
|
|
680
|
+
setStatus("connected");
|
|
681
|
+
if (heartbeat) heartbeatTimer = setInterval(() => {
|
|
682
|
+
if (!attached) return;
|
|
683
|
+
if (Date.now() - attached.lastActivity > heartbeat.timeoutMs) handleDisconnect("the page script went silent (heartbeat timeout)");
|
|
684
|
+
else attached.postControl("ping");
|
|
685
|
+
}, heartbeat.intervalMs);
|
|
686
|
+
for (const call of pendingCalls.splice(0)) call.run();
|
|
687
|
+
for (const { method, args } of eventBuffer.splice(0)) sendEventNow(method, args);
|
|
688
|
+
stateHost.resubscribe();
|
|
689
|
+
for (const waiter of connectedWaiters.splice(0)) waiter.resolve();
|
|
690
|
+
}
|
|
691
|
+
function stopTimers() {
|
|
692
|
+
if (helloTimer) {
|
|
693
|
+
clearTimeout(helloTimer);
|
|
694
|
+
helloTimer = void 0;
|
|
695
|
+
}
|
|
696
|
+
if (heartbeatTimer) {
|
|
697
|
+
clearInterval(heartbeatTimer);
|
|
698
|
+
heartbeatTimer = void 0;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function handleDisconnect(reason) {
|
|
702
|
+
if (status === "closed") return;
|
|
703
|
+
attached?.dispose({ reason });
|
|
704
|
+
attached = void 0;
|
|
705
|
+
pageScriptInfo = void 0;
|
|
706
|
+
stopTimers();
|
|
707
|
+
setStatus("connecting");
|
|
708
|
+
if (canHandshake) startHelloLoop();
|
|
709
|
+
else warnOnce(`in-page channel "${name}": transport lost (${reason}) and the panel has no handshake targets — staying disconnected`);
|
|
710
|
+
}
|
|
711
|
+
function startHelloLoop() {
|
|
712
|
+
if (helloTimer || !canHandshake || status !== "connecting") return;
|
|
713
|
+
let delay = options.helloIntervalMs ?? DEFAULT_HELLO_INTERVAL_MS;
|
|
714
|
+
const tick = () => {
|
|
715
|
+
const hello = {
|
|
716
|
+
channel: IN_PAGE_CHANNEL_TAG,
|
|
717
|
+
v: 1,
|
|
718
|
+
kind: "hello",
|
|
719
|
+
name,
|
|
720
|
+
panelId,
|
|
721
|
+
instanceId: options.instanceId
|
|
722
|
+
};
|
|
723
|
+
for (const target of targets) for (const origin of allowedOrigins) try {
|
|
724
|
+
target.postMessage(hello, origin);
|
|
725
|
+
} catch {}
|
|
726
|
+
delay = Math.min(delay * 1.5, HELLO_INTERVAL_CAP_MS);
|
|
727
|
+
helloTimer = setTimeout(tick, delay);
|
|
728
|
+
};
|
|
729
|
+
tick();
|
|
730
|
+
}
|
|
731
|
+
const onWindowMessage = (event) => {
|
|
732
|
+
if (status === "closed") return;
|
|
733
|
+
const data = event.data;
|
|
734
|
+
if (!isHandshakeMessage(data) || data.kind !== "grant" || data.name !== name || data.panelId !== panelId) return;
|
|
735
|
+
if (data.v !== 1) {
|
|
736
|
+
warnOnce(`in-page channel "${name}": ignoring a grant with protocol version ${data.v} (this side speaks 1) — align the devframe versions of the page script and the panel`);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (!allowedOrigins.includes("*") && !allowedOrigins.includes(event.origin)) {
|
|
740
|
+
warnOnce(`in-page channel "${name}": ignoring a grant from disallowed origin "${event.origin}"`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (options.instanceId && data.instanceId !== options.instanceId) return;
|
|
744
|
+
const port = event.ports?.[0];
|
|
745
|
+
if (port) adoptPort(port, { instanceId: data.instanceId });
|
|
746
|
+
};
|
|
747
|
+
win?.addEventListener("message", onWindowMessage);
|
|
748
|
+
if (options.transport) adoptPort(options.transport);
|
|
749
|
+
else if (canHandshake) startHelloLoop();
|
|
750
|
+
else warnOnce(`in-page channel "${name}": the panel has no handshake targets (not embedded, no opener) and no transport — calls will buffer until a transport appears or the channel is closed`);
|
|
751
|
+
return {
|
|
752
|
+
name,
|
|
753
|
+
get status() {
|
|
754
|
+
return status;
|
|
755
|
+
},
|
|
756
|
+
get pageScript() {
|
|
757
|
+
return pageScriptInfo;
|
|
758
|
+
},
|
|
759
|
+
events: {
|
|
760
|
+
on: events.on,
|
|
761
|
+
once: events.once
|
|
762
|
+
},
|
|
763
|
+
whenConnected: (timeoutMs) => {
|
|
764
|
+
if (status === "connected") return Promise.resolve();
|
|
765
|
+
if (status === "closed") return Promise.reject(new InPageChannelError("closed", `in-page channel "${name}" is closed`));
|
|
766
|
+
return new Promise((resolve, reject) => {
|
|
767
|
+
const waiter = {
|
|
768
|
+
resolve,
|
|
769
|
+
reject
|
|
770
|
+
};
|
|
771
|
+
connectedWaiters.push(waiter);
|
|
772
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) setTimeout(() => {
|
|
773
|
+
const index = connectedWaiters.indexOf(waiter);
|
|
774
|
+
if (index >= 0) {
|
|
775
|
+
connectedWaiters.splice(index, 1);
|
|
776
|
+
reject(new InPageChannelError("timeout", `in-page channel "${name}": no page script answered within ${timeoutMs}ms — it may not be loaded in this context (render a fallback state)`));
|
|
777
|
+
}
|
|
778
|
+
}, timeoutMs);
|
|
779
|
+
});
|
|
780
|
+
},
|
|
781
|
+
call: (fnName, ...args) => enqueueCall(fnName, serializeArgs(codec, args)),
|
|
782
|
+
callEvent: (fnName, ...args) => sendEvent(fnName, serializeArgs(codec, args)),
|
|
783
|
+
sharedState: stateHost,
|
|
784
|
+
close: () => {
|
|
785
|
+
if (status === "closed") return;
|
|
786
|
+
setStatus("closed");
|
|
787
|
+
stopTimers();
|
|
788
|
+
win?.removeEventListener("message", onWindowMessage);
|
|
789
|
+
attached?.dispose({
|
|
790
|
+
bye: true,
|
|
791
|
+
reason: "the panel closed the channel"
|
|
792
|
+
});
|
|
793
|
+
attached = void 0;
|
|
794
|
+
pageScriptInfo = void 0;
|
|
795
|
+
const closedError = new InPageChannelError("closed", `in-page channel "${name}" was closed`);
|
|
796
|
+
for (const call of pendingCalls.splice(0)) call.reject(closedError);
|
|
797
|
+
eventBuffer.length = 0;
|
|
798
|
+
for (const waiter of connectedWaiters.splice(0)) waiter.reject(closedError);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/in-page-channel/index.ts
|
|
804
|
+
/**
|
|
805
|
+
* Define one in-page channel function — the `defineRpcFunction` authoring
|
|
806
|
+
* shape narrowed to the browser (see {@link InPageFunctionDefinition}).
|
|
807
|
+
* Pure identity: it only types the definition. Functions live in
|
|
808
|
+
* side-specific files and are passed to their endpoint via `functions`; the
|
|
809
|
+
* shared protocol file carries only the contract type and the channel-name
|
|
810
|
+
* constant.
|
|
811
|
+
*/
|
|
812
|
+
function defineChannelFunction(definition) {
|
|
813
|
+
return definition;
|
|
814
|
+
}
|
|
815
|
+
//#endregion
|
|
816
|
+
export { InPageChannelError, connectPanelChannel, createPageScriptChannel, defineChannelFunction };
|