@dimina-kit/devtools 0.4.0-dev.20260718143821 → 0.4.0-dev.20260728063215
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/dist/main/app/app.js +67 -1
- package/dist/main/app/lifecycle.js +9 -0
- package/dist/main/index.bundle.js +928 -162
- package/dist/main/ipc/bridge-router.d.ts +36 -0
- package/dist/main/ipc/bridge-router.js +56 -0
- package/dist/main/ipc/index.d.ts +1 -0
- package/dist/main/ipc/index.js +1 -0
- package/dist/main/ipc/internal-devtools.d.ts +15 -0
- package/dist/main/ipc/internal-devtools.js +9 -0
- package/dist/main/services/compile-standby.js +4 -0
- package/dist/main/services/console-forward/cdp-inject.d.ts +18 -0
- package/dist/main/services/console-forward/cdp-inject.js +30 -0
- package/dist/main/services/console-forward/global-console-mirror.d.ts +33 -0
- package/dist/main/services/console-forward/global-console-mirror.js +71 -0
- package/dist/main/services/console-forward/global-diagnostics-mirror.d.ts +33 -0
- package/dist/main/services/console-forward/global-diagnostics-mirror.js +73 -0
- package/dist/main/services/console-forward/index.d.ts +15 -1
- package/dist/main/services/console-forward/index.js +42 -2
- package/dist/main/services/console-forward/internal-log.d.ts +11 -0
- package/dist/main/services/console-forward/internal-log.js +23 -0
- package/dist/main/services/console-forward/open-gated-relay.d.ts +67 -0
- package/dist/main/services/console-forward/open-gated-relay.js +55 -0
- package/dist/main/services/diagnostics/index.d.ts +10 -0
- package/dist/main/services/elements-forward/index.js +14 -96
- package/dist/main/services/network-forward/frontend-dispatch.d.ts +64 -0
- package/dist/main/services/network-forward/frontend-dispatch.js +113 -8
- package/dist/main/services/network-forward/global-body-gate.d.ts +46 -0
- package/dist/main/services/network-forward/global-body-gate.js +93 -0
- package/dist/main/services/network-forward/global-mirror-test-fixtures.d.ts +48 -0
- package/dist/main/services/network-forward/global-mirror-test-fixtures.js +96 -0
- package/dist/main/services/network-forward/index.d.ts +31 -0
- package/dist/main/services/network-forward/index.js +256 -8
- package/dist/main/services/network-forward/user-facing.d.ts +21 -0
- package/dist/main/services/network-forward/user-facing.js +50 -0
- package/dist/main/services/views/console-filter.d.ts +145 -0
- package/dist/main/services/views/console-filter.js +233 -0
- package/dist/main/services/views/frontend-bootstrap-gate.d.ts +59 -0
- package/dist/main/services/views/frontend-bootstrap-gate.js +117 -0
- package/dist/main/services/views/native-simulator-devtools-host.js +138 -30
- package/dist/main/services/views/view-manager-devtools-host-test-fixtures.d.ts +76 -0
- package/dist/main/services/views/view-manager-devtools-host-test-fixtures.js +182 -0
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/main/utils/theme.d.ts +10 -6
- package/dist/main/utils/theme.js +10 -6
- package/dist/main/windows/internal-devtools-window/index.d.ts +66 -0
- package/dist/main/windows/internal-devtools-window/index.js +160 -0
- package/dist/native-host/render/render.js +89 -73
- package/dist/native-host/service/service.js +2 -2
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +1 -1
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/renderer/assets/index-M5NDq_vi.js +49 -0
- package/dist/renderer/assets/{input-c2OcrrZy.js → input-dsgeTmVX.js} +2 -2
- package/dist/renderer/assets/ipc-transport-BgFdT9bT.js +9 -0
- package/dist/renderer/assets/{popover-kv2qspWz.js → popover-Disfu1cx.js} +2 -2
- package/dist/renderer/assets/{select-DKqzMtTh.js → select-DAw1hzkl.js} +2 -2
- package/dist/renderer/assets/{settings-dJNKTGr8.js → settings-M_8GFW2M.js} +2 -2
- package/dist/renderer/assets/settings-api-DFR5eDl_.js +2 -0
- package/dist/renderer/assets/{workbenchSettings-DQ_VP5-W.js → workbenchSettings-CxdXeoOu.js} +2 -2
- package/dist/renderer/entries/main/index.html +5 -5
- package/dist/renderer/entries/popover/index.html +4 -4
- package/dist/renderer/entries/settings/index.html +4 -4
- package/dist/renderer/entries/workbench-settings/index.html +3 -3
- package/dist/shared/ipc-channels.d.ts +3 -0
- package/dist/shared/ipc-channels.js +6 -0
- package/package.json +8 -8
- package/dist/renderer/assets/index-DfDIvgvK.js +0 -49
- package/dist/renderer/assets/ipc-transport-CuDJ07aE.js +0 -9
- package/dist/renderer/assets/settings-api-qpXKDRXt.js +0 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
2
|
+
/**
|
|
3
|
+
* Generic open/close-gated replay-subscription primitive shared by
|
|
4
|
+
* `createGlobalConsoleMirror` and `createGlobalDiagnosticsMirror` — both
|
|
5
|
+
* gate their subscription to a buffered+replayable source
|
|
6
|
+
* (`ConsoleForwarder`/`DiagnosticsBus`) on whether the standalone "debug the
|
|
7
|
+
* whole Electron app" window is currently open (`onHostChanged`): each open
|
|
8
|
+
* re-subscribes with `{replay:true}` so history is never lost even if the
|
|
9
|
+
* window opens long after boot, and each close disposes that subscription.
|
|
10
|
+
*
|
|
11
|
+
* ── Dedup (the e2e-confirmed bug this fixes) ────────────────────────────────
|
|
12
|
+
* Naively re-subscribing with `{replay:true}` on every reopen double-injects
|
|
13
|
+
* history: Chromium's own per-frame `ConsoleMessageStorage` is NOT cleared by
|
|
14
|
+
* closing DevTools (only by navigation), so it natively re-delivers entries
|
|
15
|
+
* already shown during a PREVIOUS open to the freshly-attached front-end —
|
|
16
|
+
* and if this relay ALSO blindly replays and re-injects the same buffered
|
|
17
|
+
* objects, every entry shown once ends up displayed twice on reopen, the
|
|
18
|
+
* duplicate carrying a fresh (wrong) "just reopened" timestamp instead of
|
|
19
|
+
* when it actually happened.
|
|
20
|
+
*
|
|
21
|
+
* Fix: `state` is a `WeakMap` keyed by OBJECT REFERENCE (never content — two
|
|
22
|
+
* content-identical-but-distinct entries must each still get injected once)
|
|
23
|
+
* that persists across the relay's ENTIRE lifetime, not reset by open/close.
|
|
24
|
+
* A replay that re-delivers an already-CONFIRMED-injected object is skipped;
|
|
25
|
+
* only entries never physically (and successfully) passed to `inject()`
|
|
26
|
+
* before (e.g. ones that arrived while the window was closed) actually call
|
|
27
|
+
* `inject()`. Chromium's own native re-delivery is left to show the
|
|
28
|
+
* once-injected ones — that is what makes reopening not double them up. This
|
|
29
|
+
* also composes correctly with the source's own bounded ring buffer: an
|
|
30
|
+
* evicted entry is simply absent from the next replay batch entirely, so it
|
|
31
|
+
* can never be double-counted regardless of what `state` holds.
|
|
32
|
+
*
|
|
33
|
+
* ── inject() is fallible — only a CONFIRMED success may ever mark "done" ───
|
|
34
|
+
* `inject()` wraps an async `executeJavaScript` against a possibly-destroyed
|
|
35
|
+
* or not-yet-settled target, so it reports whether the entry was ACTUALLY
|
|
36
|
+
* delivered via `boolean | Promise<boolean>`. Marking an entry "injected"
|
|
37
|
+
* before that outcome is known (the original bug here) means a single
|
|
38
|
+
* transient failure — target destroyed, front-end not settled, a rejected
|
|
39
|
+
* `executeJavaScript` — permanently black-holes that entry: every future
|
|
40
|
+
* replay skips it forever, even though it was never actually shown anywhere
|
|
41
|
+
* (violates this repo's "state must be marked at the moment the fact
|
|
42
|
+
* actually happens" principle — this was marking INTENT, not the fact). So
|
|
43
|
+
* `state` tracks three phases per entry: absent (never attempted, or a prior
|
|
44
|
+
* attempt failed and was cleared — eligible for `inject()`), `'pending'`
|
|
45
|
+
* (an `inject()` call is in flight — a concurrent replay of the SAME entry
|
|
46
|
+
* must not trigger a second concurrent call), `'done'` (confirmed success —
|
|
47
|
+
* permanently skipped from here on). A `false` result or a rejection resets
|
|
48
|
+
* the entry back to absent, so the next replay (e.g. the next window reopen)
|
|
49
|
+
* gets a real retry instead of a silent, permanent loss.
|
|
50
|
+
*/
|
|
51
|
+
export interface OpenGatedRelayOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Bounds how long ONE in-flight `inject()` attempt may hold an entry
|
|
54
|
+
* `'pending'`. An `inject()` promise that never settles (real incident:
|
|
55
|
+
* `executeJavaScript` against a wc that is simultaneously the inspected
|
|
56
|
+
* side of `setDevToolsWebContents` AND attached by an external CDP client
|
|
57
|
+
* hangs forever) would otherwise black-hole the entry permanently — never
|
|
58
|
+
* shown, never retried. Past this bound the entry becomes retryable again
|
|
59
|
+
* on the next replay. The timed-out attempt's promise may still settle
|
|
60
|
+
* LATE; its settlement is generation-stamped and ignored when stale.
|
|
61
|
+
*/
|
|
62
|
+
injectTimeoutMs?: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function createOpenGatedRelay<TEntry extends object, THost = unknown>(onHostChanged: (handler: (host: THost | null) => void) => () => void, subscribe: (sink: (entry: TEntry) => void, opts: {
|
|
65
|
+
replay: true;
|
|
66
|
+
}) => Disposable, inject: (entry: TEntry) => boolean | Promise<boolean>, opts?: OpenGatedRelayOptions): Disposable;
|
|
67
|
+
//# sourceMappingURL=open-gated-relay.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_INJECT_TIMEOUT_MS = 10_000;
|
|
2
|
+
export function createOpenGatedRelay(onHostChanged, subscribe, inject, opts) {
|
|
3
|
+
const injectTimeoutMs = opts?.injectTimeoutMs ?? DEFAULT_INJECT_TIMEOUT_MS;
|
|
4
|
+
// Generation stamp per attempt: only the attempt matching the entry's
|
|
5
|
+
// CURRENT pending generation may act on settling — a stale (timed-out,
|
|
6
|
+
// superseded) attempt's late result must neither mark the entry done nor
|
|
7
|
+
// disturb a newer attempt's pending status.
|
|
8
|
+
const state = new WeakMap();
|
|
9
|
+
let generationSeq = 0;
|
|
10
|
+
const inFlightTimers = new Set();
|
|
11
|
+
let live = null;
|
|
12
|
+
function deliver(entry) {
|
|
13
|
+
if (state.has(entry))
|
|
14
|
+
return;
|
|
15
|
+
const generation = ++generationSeq;
|
|
16
|
+
state.set(entry, { phase: 'pending', generation });
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
inFlightTimers.delete(timer);
|
|
19
|
+
const cur = state.get(entry);
|
|
20
|
+
if (cur && cur.phase === 'pending' && cur.generation === generation)
|
|
21
|
+
state.delete(entry);
|
|
22
|
+
}, injectTimeoutMs);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
inFlightTimers.add(timer);
|
|
25
|
+
const settle = (ok) => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
inFlightTimers.delete(timer);
|
|
28
|
+
const cur = state.get(entry);
|
|
29
|
+
if (!cur || cur.phase !== 'pending' || cur.generation !== generation)
|
|
30
|
+
return;
|
|
31
|
+
if (ok)
|
|
32
|
+
state.set(entry, { phase: 'done', generation });
|
|
33
|
+
else
|
|
34
|
+
state.delete(entry);
|
|
35
|
+
};
|
|
36
|
+
Promise.resolve()
|
|
37
|
+
.then(() => inject(entry))
|
|
38
|
+
.then((ok) => settle(ok === true), () => settle(false));
|
|
39
|
+
}
|
|
40
|
+
const unregister = onHostChanged((host) => {
|
|
41
|
+
live?.dispose();
|
|
42
|
+
live = host !== null ? subscribe(deliver, { replay: true }) : null;
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
dispose: () => {
|
|
46
|
+
for (const timer of inFlightTimers)
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
inFlightTimers.clear();
|
|
49
|
+
live?.dispose();
|
|
50
|
+
live = null;
|
|
51
|
+
unregister();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=open-gated-relay.js.map
|
|
@@ -20,6 +20,15 @@ export interface Diagnostic {
|
|
|
20
20
|
message: string;
|
|
21
21
|
appSessionId?: string;
|
|
22
22
|
ts: number;
|
|
23
|
+
/**
|
|
24
|
+
* Who this diagnostic is for. `'internal'` means devtools-tooling-only
|
|
25
|
+
* state (e.g. compile-standby's warm-pool lifecycle) that must never reach
|
|
26
|
+
* the per-project service-host console the right-panel CDP is attached to
|
|
27
|
+
* — see `console-forward/index.ts`'s `handleDiagnostic` gate. `'user'` or
|
|
28
|
+
* omitted means the existing behavior: a real diagnostic about the
|
|
29
|
+
* inspected mini-program, injected into that project's Console panel.
|
|
30
|
+
*/
|
|
31
|
+
audience?: 'user' | 'internal';
|
|
23
32
|
}
|
|
24
33
|
export interface DiagnosticsBus {
|
|
25
34
|
/** Record one diagnostic: buffers it, mirrors it to the main-process console, and synchronously notifies every live subscriber. No-op after `dispose()`. */
|
|
@@ -28,6 +37,7 @@ export interface DiagnosticsBus {
|
|
|
28
37
|
code: string;
|
|
29
38
|
message: string;
|
|
30
39
|
appSessionId?: string;
|
|
40
|
+
audience?: 'user' | 'internal';
|
|
31
41
|
}): void;
|
|
32
42
|
/**
|
|
33
43
|
* Register a sink. With `replay` (default true) the sink is first called,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
2
2
|
import { VIRTUAL_REQUEST_ID_PREFIX } from '../network-forward/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { buildSingleDispatchScript, createFrontendReplyChannel, answerNetworkBodyCommand, drainOutboundBatch } from '../network-forward/frontend-dispatch.js';
|
|
4
4
|
import { createCdpSessionBroker } from '../cdp-session/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* CDP domains that target the RENDER GUEST'S document tree. A command in one of
|
|
@@ -153,18 +153,10 @@ function buildReconcileScript() {
|
|
|
153
153
|
+ `})()`;
|
|
154
154
|
}
|
|
155
155
|
// ── main → front-end dispatch (shared transport with network-forward) ───────
|
|
156
|
-
/** Dispatch ONE small CDP message (response or event) into the front-end. */
|
|
157
|
-
function buildDispatchScript(message) {
|
|
158
|
-
return `(()=>{try{`
|
|
159
|
-
+ `if(!${PROBE_DEVTOOLS_API})return false;`
|
|
160
|
-
+ `window.DevToolsAPI.dispatchMessage(JSON.parse(${JSON.stringify(message)}));`
|
|
161
|
-
+ `return true;`
|
|
162
|
-
+ `}catch(_){return false}})()`;
|
|
163
|
-
}
|
|
164
156
|
/** Script that nudges the front-end to discard its document and lazily re-pull. */
|
|
165
157
|
function buildDocumentUpdatedScript() {
|
|
166
158
|
const msg = JSON.stringify({ method: 'DOM.documentUpdated', params: {} });
|
|
167
|
-
return
|
|
159
|
+
return buildSingleDispatchScript(msg);
|
|
168
160
|
}
|
|
169
161
|
// Reconcile cadence: each tick re-asserts the hook (idempotent) and drains the
|
|
170
162
|
// outbound queue. Fast enough that a freshly reloaded front-end is re-hooked
|
|
@@ -241,47 +233,12 @@ export function installElementsForward(deps) {
|
|
|
241
233
|
catch { /* already gone */ }
|
|
242
234
|
}
|
|
243
235
|
};
|
|
244
|
-
// ── main → front-end dispatch
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// there — and the executeJavaScript would queue one did-stop-loading waiter
|
|
251
|
-
// per push (an activePage burst piles them). The post-settle tick re-primes.
|
|
252
|
-
if (!isFrontendSettled(devtoolsWc))
|
|
253
|
-
return;
|
|
254
|
-
let json;
|
|
255
|
-
try {
|
|
256
|
-
json = JSON.stringify(message);
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (json.length > MAX_SINGLE_DISPATCH_CHARS) {
|
|
262
|
-
const chunks = [];
|
|
263
|
-
for (let i = 0; i < json.length; i += CHUNK_CHARS) {
|
|
264
|
-
chunks.push(json.slice(i, i + CHUNK_CHARS));
|
|
265
|
-
}
|
|
266
|
-
let script;
|
|
267
|
-
try {
|
|
268
|
-
script = buildChunkedDispatchScript(chunks, json.length);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
devtoolsWc.executeJavaScript(script, true).catch(() => { });
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
let script;
|
|
277
|
-
try {
|
|
278
|
-
script = buildDispatchScript(json);
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
devtoolsWc.executeJavaScript(script, true).catch(() => { });
|
|
284
|
-
}
|
|
236
|
+
// ── main → front-end dispatch (shared transport, see frontend-dispatch.js) ──
|
|
237
|
+
// Reply to a front-end command id, passing the ORIGINAL id + sessionId
|
|
238
|
+
// straight through (no mapping) — the same settled-gate + chunking +
|
|
239
|
+
// reply-shape contract network-forward's global body gate uses, so the two
|
|
240
|
+
// outbound-gate features can never drift.
|
|
241
|
+
const { dispatchToFrontend, replyResult, replyError } = createFrontendReplyChannel(devtoolsWc, () => disposed);
|
|
285
242
|
/** Tell the front-end its document is stale → it re-pulls lazily (routed). */
|
|
286
243
|
function pushDocumentUpdated() {
|
|
287
244
|
if (disposed || devtoolsWc.isDestroyed())
|
|
@@ -299,22 +256,6 @@ export function installElementsForward(deps) {
|
|
|
299
256
|
}
|
|
300
257
|
devtoolsWc.executeJavaScript(script, true).catch(() => { });
|
|
301
258
|
}
|
|
302
|
-
/**
|
|
303
|
-
* Reply to a front-end command id, passing the ORIGINAL id + sessionId straight
|
|
304
|
-
* through (no mapping). Only the front-end-supplied sessionId is echoed back.
|
|
305
|
-
*/
|
|
306
|
-
function replyResult(cmd, result) {
|
|
307
|
-
const msg = { id: cmd.id, result };
|
|
308
|
-
if (cmd.sessionId)
|
|
309
|
-
msg.sessionId = cmd.sessionId;
|
|
310
|
-
dispatchToFrontend(msg);
|
|
311
|
-
}
|
|
312
|
-
function replyError(cmd, message) {
|
|
313
|
-
const msg = { id: cmd.id, error: { code: -32000, message } };
|
|
314
|
-
if (cmd.sessionId)
|
|
315
|
-
msg.sessionId = cmd.sessionId;
|
|
316
|
-
dispatchToFrontend(msg);
|
|
317
|
-
}
|
|
318
259
|
// ── render-guest CDP session (via the shared broker) ────────────────────────
|
|
319
260
|
/** The active render guest, re-resolved fresh (pool/page swaps go stale). */
|
|
320
261
|
function activeRenderWc() {
|
|
@@ -450,46 +391,23 @@ export function installElementsForward(deps) {
|
|
|
450
391
|
/**
|
|
451
392
|
* Answer a network-routed command from the provider. Never touches the render
|
|
452
393
|
* guest: the body lives in the network forwarder's prefetch cache (or
|
|
453
|
-
* nowhere).
|
|
454
|
-
*
|
|
455
|
-
* failure state instead of leaking a forever-pending request.
|
|
394
|
+
* nowhere). Shared with network-forward's dedicated global body gate — see
|
|
395
|
+
* answerNetworkBodyCommand's doc comment for the not-found/reply contract.
|
|
456
396
|
*/
|
|
457
397
|
function answerNetworkCommand(cmd) {
|
|
458
|
-
|
|
459
|
-
const requestId = cmd.params?.requestId;
|
|
460
|
-
if (!provider || typeof requestId !== 'string') {
|
|
461
|
-
replyError(cmd, 'No resource with given identifier found');
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
const lookup = cmd.method === 'Network.getRequestPostData'
|
|
465
|
-
? provider.getRequestPostData(requestId)
|
|
466
|
-
: provider.getResponseBody(requestId);
|
|
467
|
-
lookup.then((result) => {
|
|
468
|
-
if (!disposed)
|
|
469
|
-
replyResult(cmd, result);
|
|
470
|
-
}, (err) => {
|
|
471
|
-
if (!disposed)
|
|
472
|
-
replyError(cmd, err instanceof Error ? err.message : String(err));
|
|
473
|
-
});
|
|
398
|
+
answerNetworkBodyCommand(cmd, deps.network, { replyResult, replyError });
|
|
474
399
|
}
|
|
475
400
|
/** Process a drained batch of front-end commands, splitting by route tag. */
|
|
476
401
|
function handleOutbound(batch) {
|
|
477
|
-
|
|
478
|
-
return;
|
|
479
|
-
for (const raw of batch) {
|
|
480
|
-
if (!raw || typeof raw !== 'object')
|
|
481
|
-
continue;
|
|
482
|
-
const cmd = raw;
|
|
483
|
-
if (typeof cmd.method !== 'string')
|
|
484
|
-
continue;
|
|
402
|
+
drainOutboundBatch(batch, (cmd) => {
|
|
485
403
|
if (cmd.route === 'network') {
|
|
486
404
|
answerNetworkCommand(cmd);
|
|
487
|
-
|
|
405
|
+
return;
|
|
488
406
|
}
|
|
489
407
|
// 'render' — and any untagged legacy payload, which only ever carried
|
|
490
408
|
// render-domain commands.
|
|
491
409
|
routeCommand(cmd);
|
|
492
|
-
}
|
|
410
|
+
});
|
|
493
411
|
}
|
|
494
412
|
// ── follow the active guest across page switches ───────────────────────────
|
|
495
413
|
const onRenderEvent = (event) => {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* same transport contract; a single copy means the two can never drift on the
|
|
7
7
|
* chunk-continuation protocol.
|
|
8
8
|
*/
|
|
9
|
+
import type { WebContents } from 'electron';
|
|
9
10
|
/** Probe the front-end realm exposes `DevToolsAPI.dispatchMessage`. */
|
|
10
11
|
export declare const PROBE_DEVTOOLS_API = "(window.DevToolsAPI && typeof window.DevToolsAPI.dispatchMessage === 'function')";
|
|
11
12
|
/** A single dispatch payload may not exceed this many UTF-16 chars; chunk above. */
|
|
@@ -24,4 +25,67 @@ export declare const CHUNK_CHARS: number;
|
|
|
24
25
|
* when the API isn't available.
|
|
25
26
|
*/
|
|
26
27
|
export declare function buildChunkedDispatchScript(chunks: string[], totalSize: number): string;
|
|
28
|
+
/** Build the `executeJavaScript` source that dispatches ONE small CDP message
|
|
29
|
+
* (a response or event) into the front-end. Above `MAX_SINGLE_DISPATCH_CHARS`,
|
|
30
|
+
* callers must use {@link buildChunkedDispatchScript} instead. */
|
|
31
|
+
export declare function buildSingleDispatchScript(message: string): string;
|
|
32
|
+
/** One CDP command awaiting a reply — the minimal shape every outbound-gate
|
|
33
|
+
* (elements-forward, network-forward's global body gate) reads to answer. */
|
|
34
|
+
export interface ReplyableCommand {
|
|
35
|
+
id: number | null;
|
|
36
|
+
sessionId: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface FrontendReplyChannel {
|
|
39
|
+
/** Push an arbitrary CDP-shaped message (response OR event) into the
|
|
40
|
+
* front-end, auto-chunking above the single-dispatch size cap. */
|
|
41
|
+
dispatchToFrontend(message: unknown): void;
|
|
42
|
+
/** Reply to a front-end command id with a successful `result`. */
|
|
43
|
+
replyResult(cmd: ReplyableCommand, result: unknown): void;
|
|
44
|
+
/** Reply to a front-end command id with the canonical CDP not-found-style
|
|
45
|
+
* error shape, so the front-end renders its normal failure state instead
|
|
46
|
+
* of leaking a forever-pending request. */
|
|
47
|
+
replyError(cmd: ReplyableCommand, message: string): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Shared main → front-end reply transport for every outbound CDP gate that
|
|
51
|
+
* answers commands it intercepted from `InspectorFrontendHost.sendMessageToBackend`
|
|
52
|
+
* (elements-forward's render/network routing, network-forward's dedicated
|
|
53
|
+
* global body gate). A single copy means the two features can never drift on
|
|
54
|
+
* the settled-gate check, the chunking threshold, or the CDP reply shape.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createFrontendReplyChannel(hostWc: WebContents, isDisposed: () => boolean): FrontendReplyChannel;
|
|
57
|
+
/**
|
|
58
|
+
* Validate + iterate a drained outbound-command batch (the front-end →
|
|
59
|
+
* main half of every poll-based outbound CDP gate): skips non-array batches,
|
|
60
|
+
* skips malformed entries, and only ever hands `handle` an item whose
|
|
61
|
+
* `method` is a string. Shared by elements-forward's render/network router
|
|
62
|
+
* and network-forward's dedicated global body gate so the two can never
|
|
63
|
+
* silently diverge on what counts as "well-formed".
|
|
64
|
+
*/
|
|
65
|
+
export declare function drainOutboundBatch<T extends {
|
|
66
|
+
method: unknown;
|
|
67
|
+
}>(batch: unknown, handle: (cmd: T) => void): void;
|
|
68
|
+
/** Minimal structural shape `answerNetworkBodyCommand` needs — satisfied by
|
|
69
|
+
* the real `NetworkBodyProvider` (network-forward/index.ts) without either
|
|
70
|
+
* module importing the other (that would be circular: index.ts already
|
|
71
|
+
* imports from this file). */
|
|
72
|
+
export interface NetworkBodyLookup {
|
|
73
|
+
getResponseBody(requestId: string): Promise<unknown>;
|
|
74
|
+
getRequestPostData(requestId: string): Promise<unknown>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Answer one drained front-end command that requested a virtual requestId's
|
|
78
|
+
* response body / post data — the shared "network" branch every outbound CDP
|
|
79
|
+
* gate that intercepts `Network.getResponseBody`/`Network.getRequestPostData`
|
|
80
|
+
* needs (elements-forward's render/network router, network-forward's
|
|
81
|
+
* dedicated global body gate). A missing provider or a rejected lookup
|
|
82
|
+
* settles with the canonical CDP not-found error so the front-end renders
|
|
83
|
+
* its normal failure state instead of leaking a forever-pending request.
|
|
84
|
+
* `reply.replyResult`/`replyError` (from {@link createFrontendReplyChannel})
|
|
85
|
+
* already no-op once disposed, so this never needs its own disposed guard.
|
|
86
|
+
*/
|
|
87
|
+
export declare function answerNetworkBodyCommand(cmd: ReplyableCommand & {
|
|
88
|
+
method: string;
|
|
89
|
+
params: unknown;
|
|
90
|
+
}, provider: NetworkBodyLookup | undefined, reply: Pick<FrontendReplyChannel, 'replyResult' | 'replyError'>): void;
|
|
27
91
|
//# sourceMappingURL=frontend-dispatch.d.ts.map
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared `executeJavaScript` builders for pushing raw CDP messages into the
|
|
3
|
-
* embedded Chrome DevTools front-end via `window.DevToolsAPI.dispatchMessage`
|
|
4
|
-
* / `dispatchMessageChunk`. Both network-forward (Network tab injection) and
|
|
5
|
-
* elements-forward (Elements tab / render-event re-injection) need the exact
|
|
6
|
-
* same transport contract; a single copy means the two can never drift on the
|
|
7
|
-
* chunk-continuation protocol.
|
|
8
|
-
*/
|
|
1
|
+
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
9
2
|
/** Probe the front-end realm exposes `DevToolsAPI.dispatchMessage`. */
|
|
10
3
|
export const PROBE_DEVTOOLS_API = `(window.DevToolsAPI && typeof window.DevToolsAPI.dispatchMessage === 'function')`;
|
|
11
4
|
/** A single dispatch payload may not exceed this many UTF-16 chars; chunk above. */
|
|
@@ -39,4 +32,116 @@ export function buildChunkedDispatchScript(chunks, totalSize) {
|
|
|
39
32
|
+ `return true;`
|
|
40
33
|
+ `}catch(_){return false}})()`;
|
|
41
34
|
}
|
|
35
|
+
/** Build the `executeJavaScript` source that dispatches ONE small CDP message
|
|
36
|
+
* (a response or event) into the front-end. Above `MAX_SINGLE_DISPATCH_CHARS`,
|
|
37
|
+
* callers must use {@link buildChunkedDispatchScript} instead. */
|
|
38
|
+
export function buildSingleDispatchScript(message) {
|
|
39
|
+
return `(()=>{try{`
|
|
40
|
+
+ `if(!${PROBE_DEVTOOLS_API})return false;`
|
|
41
|
+
+ `window.DevToolsAPI.dispatchMessage(JSON.parse(${JSON.stringify(message)}));`
|
|
42
|
+
+ `return true;`
|
|
43
|
+
+ `}catch(_){return false}})()`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Shared main → front-end reply transport for every outbound CDP gate that
|
|
47
|
+
* answers commands it intercepted from `InspectorFrontendHost.sendMessageToBackend`
|
|
48
|
+
* (elements-forward's render/network routing, network-forward's dedicated
|
|
49
|
+
* global body gate). A single copy means the two features can never drift on
|
|
50
|
+
* the settled-gate check, the chunking threshold, or the CDP reply shape.
|
|
51
|
+
*/
|
|
52
|
+
export function createFrontendReplyChannel(hostWc, isDisposed) {
|
|
53
|
+
function dispatchToFrontend(message) {
|
|
54
|
+
if (isDisposed() || hostWc.isDestroyed())
|
|
55
|
+
return;
|
|
56
|
+
// An unsettled front-end wipes its state on load anyway (message would be
|
|
57
|
+
// meaningless there), and executeJavaScript against it queues one
|
|
58
|
+
// did-stop-loading waiter per push — skip; the next settled tick re-primes.
|
|
59
|
+
if (!isFrontendSettled(hostWc))
|
|
60
|
+
return;
|
|
61
|
+
let json;
|
|
62
|
+
try {
|
|
63
|
+
json = JSON.stringify(message);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (json.length > MAX_SINGLE_DISPATCH_CHARS) {
|
|
69
|
+
const chunks = [];
|
|
70
|
+
for (let i = 0; i < json.length; i += CHUNK_CHARS)
|
|
71
|
+
chunks.push(json.slice(i, i + CHUNK_CHARS));
|
|
72
|
+
let script;
|
|
73
|
+
try {
|
|
74
|
+
script = buildChunkedDispatchScript(chunks, json.length);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
hostWc.executeJavaScript(script, true).catch(() => { });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let script;
|
|
83
|
+
try {
|
|
84
|
+
script = buildSingleDispatchScript(json);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
hostWc.executeJavaScript(script, true).catch(() => { });
|
|
90
|
+
}
|
|
91
|
+
function replyResult(cmd, result) {
|
|
92
|
+
const msg = { id: cmd.id, result };
|
|
93
|
+
if (cmd.sessionId)
|
|
94
|
+
msg.sessionId = cmd.sessionId;
|
|
95
|
+
dispatchToFrontend(msg);
|
|
96
|
+
}
|
|
97
|
+
function replyError(cmd, message) {
|
|
98
|
+
const msg = { id: cmd.id, error: { code: -32000, message } };
|
|
99
|
+
if (cmd.sessionId)
|
|
100
|
+
msg.sessionId = cmd.sessionId;
|
|
101
|
+
dispatchToFrontend(msg);
|
|
102
|
+
}
|
|
103
|
+
return { dispatchToFrontend, replyResult, replyError };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Validate + iterate a drained outbound-command batch (the front-end →
|
|
107
|
+
* main half of every poll-based outbound CDP gate): skips non-array batches,
|
|
108
|
+
* skips malformed entries, and only ever hands `handle` an item whose
|
|
109
|
+
* `method` is a string. Shared by elements-forward's render/network router
|
|
110
|
+
* and network-forward's dedicated global body gate so the two can never
|
|
111
|
+
* silently diverge on what counts as "well-formed".
|
|
112
|
+
*/
|
|
113
|
+
export function drainOutboundBatch(batch, handle) {
|
|
114
|
+
if (!Array.isArray(batch))
|
|
115
|
+
return;
|
|
116
|
+
for (const raw of batch) {
|
|
117
|
+
if (!raw || typeof raw !== 'object')
|
|
118
|
+
continue;
|
|
119
|
+
const cmd = raw;
|
|
120
|
+
if (typeof cmd.method !== 'string')
|
|
121
|
+
continue;
|
|
122
|
+
handle(cmd);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Answer one drained front-end command that requested a virtual requestId's
|
|
127
|
+
* response body / post data — the shared "network" branch every outbound CDP
|
|
128
|
+
* gate that intercepts `Network.getResponseBody`/`Network.getRequestPostData`
|
|
129
|
+
* needs (elements-forward's render/network router, network-forward's
|
|
130
|
+
* dedicated global body gate). A missing provider or a rejected lookup
|
|
131
|
+
* settles with the canonical CDP not-found error so the front-end renders
|
|
132
|
+
* its normal failure state instead of leaking a forever-pending request.
|
|
133
|
+
* `reply.replyResult`/`replyError` (from {@link createFrontendReplyChannel})
|
|
134
|
+
* already no-op once disposed, so this never needs its own disposed guard.
|
|
135
|
+
*/
|
|
136
|
+
export function answerNetworkBodyCommand(cmd, provider, reply) {
|
|
137
|
+
const requestId = cmd.params?.requestId;
|
|
138
|
+
if (!provider || typeof requestId !== 'string') {
|
|
139
|
+
reply.replyError(cmd, 'No resource with given identifier found');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const lookup = cmd.method === 'Network.getRequestPostData'
|
|
143
|
+
? provider.getRequestPostData(requestId)
|
|
144
|
+
: provider.getResponseBody(requestId);
|
|
145
|
+
lookup.then((result) => reply.replyResult(cmd, result), (err) => reply.replyError(cmd, err instanceof Error ? err.message : String(err)));
|
|
146
|
+
}
|
|
42
147
|
//# sourceMappingURL=frontend-dispatch.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedicated, NETWORK-ONLY outbound CDP gate for the standalone internal
|
|
3
|
+
* (app-wide) DevTools window's front-end.
|
|
4
|
+
*
|
|
5
|
+
* ── Why a SEPARATE gate from elements-forward ───────────────────────────────
|
|
6
|
+
* elements-forward's `InspectorFrontendHost.sendMessageToBackend` hook
|
|
7
|
+
* (see ../elements-forward/index.ts) ALSO intercepts DOM/CSS/Overlay commands
|
|
8
|
+
* and redirects them to the active render guest — correct for the RIGHT-panel
|
|
9
|
+
* CDP (which inspects the service host but wants Elements to reflect the
|
|
10
|
+
* render guest's live DOM), but WRONG for the global window: its Elements/
|
|
11
|
+
* Sources/Console should inspect `mainWindow` NATIVELY, with zero redirection.
|
|
12
|
+
* Installing the full elements-forward gate here would silently reroute the
|
|
13
|
+
* global window's Elements panel to the wrong target. This module intercepts
|
|
14
|
+
* ONLY `Network.getResponseBody` / `Network.getRequestPostData` for
|
|
15
|
+
* `dimina:sim:` virtual requestIds; every other command (including all
|
|
16
|
+
* DOM/CSS/Overlay/Runtime/Debugger/…) passes straight through untouched.
|
|
17
|
+
*
|
|
18
|
+
* ── Mechanism (same two-way poll bridge as elements-forward) ───────────────
|
|
19
|
+
* front-end → main: wrap `InspectorFrontendHost.sendMessageToBackend`; a
|
|
20
|
+
* matching command is pushed onto `globalThis.__diminaGlobalNetworkOutbound`
|
|
21
|
+
* and NOT forwarded to the original channel; everything else calls through.
|
|
22
|
+
* main → front-end: answer via `window.DevToolsAPI.dispatchMessage` (chunked
|
|
23
|
+
* for large bodies), reusing the same transport network-forward's primary
|
|
24
|
+
* sink uses.
|
|
25
|
+
*
|
|
26
|
+
* Degradation: hook unavailable / host destroyed / a lookup rejects → the
|
|
27
|
+
* command settles with the standard CDP not-found error (same as an unknown
|
|
28
|
+
* id would get from the real backend) — never throws, never blocks anything
|
|
29
|
+
* else in the window.
|
|
30
|
+
*/
|
|
31
|
+
import type { WebContents } from 'electron';
|
|
32
|
+
import type { NetworkBodyProvider } from './index.js';
|
|
33
|
+
/**
|
|
34
|
+
* The JS injected into the DevTools front-end realm. Idempotent (sentinel
|
|
35
|
+
* guard, distinct from elements-forward's — the two gates coexist on
|
|
36
|
+
* DIFFERENT host wcs and never share a front-end, but the sentinel is scoped
|
|
37
|
+
* defensively anyway).
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildNetworkOnlyHookScript(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Install the gate on `hostWc` (the global window's front-end host).
|
|
42
|
+
* Returns a disposer that stops the reconcile loop. Best-effort throughout —
|
|
43
|
+
* never throws, degrades to CDP not-found on any failure.
|
|
44
|
+
*/
|
|
45
|
+
export declare function installGlobalNetworkBodyGate(hostWc: WebContents, bodies: NetworkBodyProvider): () => void;
|
|
46
|
+
//# sourceMappingURL=global-body-gate.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
2
|
+
import { VIRTUAL_REQUEST_ID_PREFIX } from './index.js';
|
|
3
|
+
import { createFrontendReplyChannel, answerNetworkBodyCommand, drainOutboundBatch } from './frontend-dispatch.js';
|
|
4
|
+
/** The only two Network.* commands this gate ever intercepts. */
|
|
5
|
+
const NETWORK_BODY_METHODS = ['Network.getResponseBody', 'Network.getRequestPostData'];
|
|
6
|
+
const DRAIN_INTERVAL_MS = 150;
|
|
7
|
+
/**
|
|
8
|
+
* The JS injected into the DevTools front-end realm. Idempotent (sentinel
|
|
9
|
+
* guard, distinct from elements-forward's — the two gates coexist on
|
|
10
|
+
* DIFFERENT host wcs and never share a front-end, but the sentinel is scoped
|
|
11
|
+
* defensively anyway).
|
|
12
|
+
*/
|
|
13
|
+
export function buildNetworkOnlyHookScript() {
|
|
14
|
+
const netMethods = JSON.stringify(NETWORK_BODY_METHODS);
|
|
15
|
+
const vprefix = JSON.stringify(VIRTUAL_REQUEST_ID_PREFIX);
|
|
16
|
+
return `(function(){try{
|
|
17
|
+
if (globalThis.__diminaGlobalNetworkHookInstalled) return 'already';
|
|
18
|
+
var OUT = (globalThis.__diminaGlobalNetworkOutbound = globalThis.__diminaGlobalNetworkOutbound || []);
|
|
19
|
+
var NET_METHODS = ${netMethods};
|
|
20
|
+
var VPREFIX = ${vprefix};
|
|
21
|
+
function isNetworkBody(m){
|
|
22
|
+
if (!m || !m.method) return false;
|
|
23
|
+
if (NET_METHODS.indexOf(m.method) < 0) return false;
|
|
24
|
+
return !!(m.params && typeof m.params.requestId === 'string' && m.params.requestId.indexOf(VPREFIX) === 0);
|
|
25
|
+
}
|
|
26
|
+
var IFH = globalThis.InspectorFrontendHost;
|
|
27
|
+
if (IFH && typeof IFH.sendMessageToBackend === 'function' && !IFH.__diminaGlobalNetworkWrapped){
|
|
28
|
+
var origSend = IFH.sendMessageToBackend.bind(IFH);
|
|
29
|
+
IFH.sendMessageToBackend = function(message){
|
|
30
|
+
try {
|
|
31
|
+
var m = (typeof message === 'string') ? JSON.parse(message) : message;
|
|
32
|
+
if (isNetworkBody(m)){
|
|
33
|
+
OUT.push({ id: (m && typeof m.id === 'number') ? m.id : null,
|
|
34
|
+
method: m.method, params: m.params || {},
|
|
35
|
+
sessionId: (m && m.sessionId) ? m.sessionId : null });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
} catch(_){ /* fall through to original on any parse hiccup */ }
|
|
39
|
+
return origSend(message);
|
|
40
|
+
};
|
|
41
|
+
IFH.__diminaGlobalNetworkWrapped = true;
|
|
42
|
+
globalThis.__diminaGlobalNetworkHookInstalled = true;
|
|
43
|
+
return 'installed';
|
|
44
|
+
}
|
|
45
|
+
return 'partial';
|
|
46
|
+
}catch(e){ return 'error:' + (e && e.message); }})()`;
|
|
47
|
+
}
|
|
48
|
+
function buildReconcileScript() {
|
|
49
|
+
return `(function(){`
|
|
50
|
+
+ `var status; try { status = ${buildNetworkOnlyHookScript()}; } catch(e){ status = 'error:'+(e&&e.message); }`
|
|
51
|
+
+ `var batch = []; try { if (globalThis.__diminaGlobalNetworkOutbound) batch = globalThis.__diminaGlobalNetworkOutbound.splice(0); } catch(_){}`
|
|
52
|
+
+ `return { status: status, batch: batch };`
|
|
53
|
+
+ `})()`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Install the gate on `hostWc` (the global window's front-end host).
|
|
57
|
+
* Returns a disposer that stops the reconcile loop. Best-effort throughout —
|
|
58
|
+
* never throws, degrades to CDP not-found on any failure.
|
|
59
|
+
*/
|
|
60
|
+
export function installGlobalNetworkBodyGate(hostWc, bodies) {
|
|
61
|
+
let disposed = false;
|
|
62
|
+
let reconcileTimer = null;
|
|
63
|
+
const reply = createFrontendReplyChannel(hostWc, () => disposed);
|
|
64
|
+
function handleOutbound(batch) {
|
|
65
|
+
drainOutboundBatch(batch, (cmd) => answerNetworkBodyCommand(cmd, bodies, reply));
|
|
66
|
+
}
|
|
67
|
+
reconcileTimer = setInterval(() => {
|
|
68
|
+
if (disposed || hostWc.isDestroyed()) {
|
|
69
|
+
stop();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!isFrontendSettled(hostWc))
|
|
73
|
+
return;
|
|
74
|
+
hostWc
|
|
75
|
+
.executeJavaScript(buildReconcileScript())
|
|
76
|
+
.then((res) => {
|
|
77
|
+
if (disposed)
|
|
78
|
+
return;
|
|
79
|
+
const r = (res ?? {});
|
|
80
|
+
handleOutbound(r.batch);
|
|
81
|
+
})
|
|
82
|
+
.catch(() => { });
|
|
83
|
+
}, DRAIN_INTERVAL_MS);
|
|
84
|
+
function stop() {
|
|
85
|
+
disposed = true;
|
|
86
|
+
if (reconcileTimer) {
|
|
87
|
+
clearInterval(reconcileTimer);
|
|
88
|
+
reconcileTimer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return stop;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=global-body-gate.js.map
|