@dimina-kit/devtools 0.4.0-dev.20260716214527 → 0.4.0-dev.20260718085557
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 +6 -1
- package/dist/main/index.bundle.js +7658 -7306
- package/dist/main/services/cdp-session/index.d.ts +87 -0
- package/dist/main/services/cdp-session/index.js +173 -0
- package/dist/main/services/elements-forward/index.d.ts +132 -15
- package/dist/main/services/elements-forward/index.js +162 -229
- package/dist/main/services/network-forward/body-cache.d.ts +70 -0
- package/dist/main/services/network-forward/body-cache.js +175 -0
- package/dist/main/services/network-forward/frontend-dispatch.d.ts +27 -0
- package/dist/main/services/network-forward/frontend-dispatch.js +42 -0
- package/dist/main/services/network-forward/index.d.ts +80 -9
- package/dist/main/services/network-forward/index.js +419 -149
- package/dist/main/services/render-inspect/index.d.ts +10 -0
- package/dist/main/services/render-inspect/index.js +17 -98
- package/dist/main/services/safe-area/index.d.ts +4 -1
- package/dist/main/services/safe-area/index.js +62 -34
- package/dist/main/services/simulator-appdata/index.d.ts +6 -0
- package/dist/main/services/simulator-appdata/index.js +20 -1
- package/dist/main/services/simulator-storage/index.d.ts +22 -4
- package/dist/main/services/simulator-storage/index.js +51 -64
- package/dist/main/services/views/native-simulator-devtools-host.js +12 -1
- package/dist/main/services/views/native-simulator-view.js +6 -0
- package/dist/main/services/views/view-manager.d.ts +9 -0
- package/dist/main/services/views/view-manager.js +1 -1
- package/dist/main/services/workbench-context.d.ts +9 -0
- package/dist/main/services/workbench-context.js +3 -0
- package/dist/main/utils/ipc-registry.d.ts +2 -1
- package/dist/main/utils/ipc-registry.js +30 -10
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/renderer/assets/index-U9F8MrtV.js +49 -0
- package/dist/renderer/assets/{input-7P0WbXyg.js → input-c2OcrrZy.js} +2 -2
- package/dist/renderer/assets/ipc-transport-Blac3sOj.css +1 -0
- package/dist/renderer/assets/{ipc-transport-DTEmLpxx.js → ipc-transport-CuDJ07aE.js} +2 -2
- package/dist/renderer/assets/{popover-DsOcyIS6.js → popover-kv2qspWz.js} +2 -2
- package/dist/renderer/assets/{select-Bzbhbg9Z.js → select-DKqzMtTh.js} +2 -2
- package/dist/renderer/assets/{settings-api-B1wVFAPB.js → settings-api-qpXKDRXt.js} +2 -2
- package/dist/renderer/assets/{settings-CR1TyBWu.js → settings-dJNKTGr8.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-CFbToT4Q.js → workbenchSettings-DQ_VP5-W.js} +2 -2
- package/dist/renderer/entries/main/index.html +6 -6
- package/dist/renderer/entries/popover/index.html +5 -5
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +4 -4
- package/dist/service-host/appdata-set-data.cjs +39 -0
- package/dist/service-host/preload.cjs +21 -0
- package/dist/shared/ipc-channels.d.ts +9 -0
- package/dist/shared/ipc-channels.js +11 -0
- package/package.json +5 -5
- package/dist/renderer/assets/index-CZd3LyY8.js +0 -49
- package/dist/renderer/assets/ipc-transport-BGtDnv6p.css +0 -1
|
@@ -1,61 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Elements-panel forwarding — reflect the ACTIVE RENDER GUEST's live DOM tree in
|
|
3
|
-
* the right-panel Chrome DevTools front-end, which natively inspects the
|
|
4
|
-
* service-host (logic layer).
|
|
5
|
-
*
|
|
6
|
-
* ── Mechanism (no preload, no broker) ────────────────────────────────────────
|
|
7
|
-
* The DevTools front-end is a `devtools://` page with NO dimina preload, so we
|
|
8
|
-
* drive it through the same two-way poll bridge the network-forward path uses:
|
|
9
|
-
* • front-end → main : we wrap `InspectorFrontendHost.sendMessageToBackend` in
|
|
10
|
-
* the front-end realm. Commands whose method is in the RENDER domain set
|
|
11
|
-
* (DOM/CSS/Overlay/DOMSnapshot/DOMDebugger) are NOT passed to the original
|
|
12
|
-
* embedder channel — they're pushed onto `globalThis.__diminaElementsOutbound`
|
|
13
|
-
* and main drains that array (splice) on a poll. EVERY OTHER method
|
|
14
|
-
* (Runtime/Console/Page/Target/Emulation/…) calls through to the original, so
|
|
15
|
-
* the service-host inspection (Console, Sources) and crucially the safe-area
|
|
16
|
-
* `Emulation.setSafeAreaInsetsOverride` are untouched.
|
|
17
|
-
* • main → front-end : we re-inject responses + render-side EVENTS via
|
|
18
|
-
* `window.DevToolsAPI.dispatchMessage(json)` (chunked for large payloads —
|
|
19
|
-
* `DOM.getDocument` can be big — mirroring network-forward/index.ts).
|
|
20
|
-
*
|
|
21
|
-
* ── Why no broker / no safe-area changes ─────────────────────────────────────
|
|
22
|
-
* `webContents.debugger` is single-owner per wc, and the safe-area service has
|
|
23
|
-
* already `attach('1.3')`-ed each render guest. We REUSE that already-attached
|
|
24
|
-
* session (`sendCommand` + an extra `on('message')` listener on the SAME
|
|
25
|
-
* `wc.debugger`); we never attach a second session and never detach one we don't
|
|
26
|
-
* own. `DOM.enable`/`CSS.enable`/`Overlay.enable` do not reset the safe-area
|
|
27
|
-
* `Emulation` override, so this is purely additive. ONLY when a guest's debugger
|
|
28
|
-
* is NOT attached (safe-area degraded) do we `attach('1.3')` ourselves — tracked
|
|
29
|
-
* in a `Set<wc.id>` so dispose/guest-destroy detaches ONLY the sessions we own;
|
|
30
|
-
* safe-area's are never touched.
|
|
31
|
-
*
|
|
32
|
-
* ── Staleness (active-guest, NOT a generation counter) ───────────────────────
|
|
33
|
-
* A response/event is honoured only while its originating guest is STILL the
|
|
34
|
-
* active render guest — resolved fresh from the bridge per check (`isActiveWcId`),
|
|
35
|
-
* not snapshotted. So a late response or stray render EVENT from a guest that is
|
|
36
|
-
* no longer active is dropped (its in-flight command id is settled with an error
|
|
37
|
-
* so the front-end never leaks a pending request, and stale nodes never bleed into
|
|
38
|
-
* the new tree); and switching away and BACK to a previously-wired guest RESUMES
|
|
39
|
-
* its forwarding (a snapshot would strand it).
|
|
40
|
-
*
|
|
41
|
-
* ── Degradation ──────────────────────────────────────────────────────────────
|
|
42
|
-
* Hook unavailable / `DevToolsAPI` missing / no active guest → routing is simply
|
|
43
|
-
* inert; the Elements panel falls back to the front-end's native service-host
|
|
44
|
-
* DOM (the prior behaviour). Everything is try/catch + feature-detect + bounded
|
|
45
|
-
* polling: it never throws, never blocks, never disturbs Console/Network/safe-area.
|
|
46
|
-
*
|
|
47
|
-
* This is a production feature (no env gate, default on for the native simulator).
|
|
48
|
-
* It deliberately re-implements the small pure helpers it needs (routing table,
|
|
49
|
-
* hook + dispatch scripts) as self-contained code.
|
|
50
|
-
*/
|
|
51
|
-
import { webContents as electronWebContents } from 'electron';
|
|
52
1
|
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
2
|
+
import { VIRTUAL_REQUEST_ID_PREFIX } from '../network-forward/index.js';
|
|
3
|
+
import { PROBE_DEVTOOLS_API, MAX_SINGLE_DISPATCH_CHARS, CHUNK_CHARS, buildChunkedDispatchScript } from '../network-forward/frontend-dispatch.js';
|
|
4
|
+
import { createCdpSessionBroker } from '../cdp-session/index.js';
|
|
53
5
|
/**
|
|
54
6
|
* CDP domains that target the RENDER GUEST'S document tree. A command in one of
|
|
55
7
|
* these is intercepted and sent to the active render guest's debugger instead of
|
|
56
8
|
* the service host the front-end nominally inspects.
|
|
57
9
|
*/
|
|
58
|
-
const RENDER_DOMAIN_PREFIXES = [
|
|
10
|
+
export const RENDER_DOMAIN_PREFIXES = [
|
|
59
11
|
'DOM.',
|
|
60
12
|
'CSS.',
|
|
61
13
|
'Overlay.',
|
|
@@ -87,40 +39,89 @@ export function routeByDomain(method) {
|
|
|
87
39
|
export function isRenderEventMethod(method) {
|
|
88
40
|
return routeByDomain(method) === 'render';
|
|
89
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The Network commands answered from the network forwarder's prefetch cache
|
|
44
|
+
* when they target a virtual requestId. Only these two: they're the panel's
|
|
45
|
+
* body/post-data round-trips. Every other Network.* stays on the service path
|
|
46
|
+
* (a virtual id there errors exactly like an unknown id would — harmless).
|
|
47
|
+
*/
|
|
48
|
+
export const NETWORK_BODY_METHODS = [
|
|
49
|
+
'Network.getResponseBody',
|
|
50
|
+
'Network.getRequestPostData',
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Full outbound routing decision — the SINGLE authority for where a front-end
|
|
54
|
+
* CDP command goes. `routeByDomain` covers the method-only render split; this
|
|
55
|
+
* adds the params-dependent network split. The hook script inlines an
|
|
56
|
+
* equivalent test driven by the same literals, so the two cannot drift.
|
|
57
|
+
*
|
|
58
|
+
* A non-string / missing requestId routes to 'service': only the
|
|
59
|
+
* `dimina:sim:` namespace is ours to answer, and the real backend is the
|
|
60
|
+
* correct authority for its own ids.
|
|
61
|
+
*/
|
|
62
|
+
export function routeOutboundCommand(method, params) {
|
|
63
|
+
if (routeByDomain(method) === 'render')
|
|
64
|
+
return 'render';
|
|
65
|
+
if (NETWORK_BODY_METHODS.includes(method)) {
|
|
66
|
+
const requestId = params?.requestId;
|
|
67
|
+
if (typeof requestId === 'string' && requestId.startsWith(VIRTUAL_REQUEST_ID_PREFIX)) {
|
|
68
|
+
return 'network';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return 'service';
|
|
72
|
+
}
|
|
90
73
|
// ── front-end injection (the front-end → main half of the bridge) ────────────
|
|
91
74
|
/**
|
|
92
75
|
* The JS injected into the DevTools front-end realm. Idempotent (sentinel guard).
|
|
93
|
-
* Wraps `InspectorFrontendHost.sendMessageToBackend
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
76
|
+
* Wraps `InspectorFrontendHost.sendMessageToBackend` — the ONE outbound CDP
|
|
77
|
+
* gate for every command the front-end emits. A command that routes 'render'
|
|
78
|
+
* or 'network' (same decision table as {@link routeOutboundCommand}) is pushed
|
|
79
|
+
* onto `globalThis.__diminaElementsOutbound` tagged with its route and NOT
|
|
80
|
+
* forwarded to the original embedder channel (main answers it — from the
|
|
81
|
+
* render guest or the network prefetch cache respectively); every other
|
|
82
|
+
* command calls through to the original (service-host path).
|
|
97
83
|
*
|
|
98
84
|
* Returns `'installed'` / `'already'` on success, `'partial'` while the embedder
|
|
99
85
|
* global is not yet present (so the caller retries), `'error:…'` on a real fault.
|
|
100
86
|
*/
|
|
101
87
|
export function buildElementsHookScript() {
|
|
102
88
|
const prefixes = JSON.stringify(RENDER_DOMAIN_PREFIXES);
|
|
89
|
+
const netMethods = JSON.stringify(NETWORK_BODY_METHODS);
|
|
90
|
+
const vprefix = JSON.stringify(VIRTUAL_REQUEST_ID_PREFIX);
|
|
103
91
|
return `(function(){try{
|
|
104
92
|
if (globalThis.__diminaElementsHookInstalled) return 'already';
|
|
105
93
|
var OUT = (globalThis.__diminaElementsOutbound = globalThis.__diminaElementsOutbound || []);
|
|
106
94
|
var PREFIXES = ${prefixes};
|
|
95
|
+
var NET_METHODS = ${netMethods};
|
|
96
|
+
var VPREFIX = ${vprefix};
|
|
107
97
|
function isRender(method){
|
|
108
98
|
if (!method) return false;
|
|
109
99
|
for (var i=0;i<PREFIXES.length;i++){ if (method.indexOf(PREFIXES[i])===0) return true; }
|
|
110
100
|
return false;
|
|
111
101
|
}
|
|
102
|
+
function routeOf(m){
|
|
103
|
+
if (!m || !m.method) return 'service';
|
|
104
|
+
if (isRender(m.method)) return 'render';
|
|
105
|
+
if (NET_METHODS.indexOf(m.method) >= 0 && m.params
|
|
106
|
+
&& typeof m.params.requestId === 'string'
|
|
107
|
+
&& m.params.requestId.indexOf(VPREFIX) === 0) return 'network';
|
|
108
|
+
return 'service';
|
|
109
|
+
}
|
|
112
110
|
var IFH = globalThis.InspectorFrontendHost;
|
|
113
111
|
if (IFH && typeof IFH.sendMessageToBackend === 'function' && !IFH.__diminaElementsWrapped){
|
|
114
112
|
var origSend = IFH.sendMessageToBackend.bind(IFH);
|
|
115
113
|
IFH.sendMessageToBackend = function(message){
|
|
116
114
|
try {
|
|
117
115
|
var m = (typeof message === 'string') ? JSON.parse(message) : message;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
116
|
+
var route = routeOf(m);
|
|
117
|
+
if (route !== 'service'){
|
|
118
|
+
// Intercept: hand it to main to answer (render guest / network
|
|
119
|
+
// cache). Do NOT call the original embedder channel — the service
|
|
120
|
+
// host either has the wrong tree or has never heard of the id.
|
|
121
121
|
OUT.push({ id: (m && typeof m.id === 'number') ? m.id : null,
|
|
122
122
|
method: m.method, params: m.params || {},
|
|
123
|
-
sessionId: (m && m.sessionId) ? m.sessionId : null
|
|
123
|
+
sessionId: (m && m.sessionId) ? m.sessionId : null,
|
|
124
|
+
route: route });
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
} catch(_){ /* fall through to original on any parse hiccup */ }
|
|
@@ -151,13 +152,7 @@ function buildReconcileScript() {
|
|
|
151
152
|
+ `return { status: status, batch: batch };`
|
|
152
153
|
+ `})()`;
|
|
153
154
|
}
|
|
154
|
-
// ── main → front-end dispatch (
|
|
155
|
-
// network-forward keeps these file-private and the task forbids touching it, so
|
|
156
|
-
// they're re-implemented here. Keep the chunk contract in sync if that changes.
|
|
157
|
-
const PROBE_DEVTOOLS_API = `(window.DevToolsAPI && typeof window.DevToolsAPI.dispatchMessage === 'function')`;
|
|
158
|
-
/** A single dispatch payload may not exceed this many UTF-16 chars; chunk above. */
|
|
159
|
-
const MAX_SINGLE_DISPATCH_CHARS = 1_000_000;
|
|
160
|
-
const CHUNK_CHARS = 256 * 1024;
|
|
155
|
+
// ── main → front-end dispatch (shared transport with network-forward) ───────
|
|
161
156
|
/** Dispatch ONE small CDP message (response or event) into the front-end. */
|
|
162
157
|
function buildDispatchScript(message) {
|
|
163
158
|
return `(()=>{try{`
|
|
@@ -166,25 +161,6 @@ function buildDispatchScript(message) {
|
|
|
166
161
|
+ `return true;`
|
|
167
162
|
+ `}catch(_){return false}})()`;
|
|
168
163
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Dispatch one LARGE message via `dispatchMessageChunk`: the FIRST chunk carries
|
|
171
|
-
* the total size, every SUBSEQUENT chunk omits the second arg (Chromium's
|
|
172
|
-
* continuation contract). Returns false when the chunk API isn't present yet.
|
|
173
|
-
*/
|
|
174
|
-
function buildChunkedDispatchScript(chunks, totalSize) {
|
|
175
|
-
const arr = JSON.stringify(chunks);
|
|
176
|
-
return `(()=>{try{`
|
|
177
|
-
+ `if(!(window.DevToolsAPI&&typeof window.DevToolsAPI.dispatchMessageChunk==='function'))return false;`
|
|
178
|
-
+ `const cs=JSON.parse(${JSON.stringify(arr)});`
|
|
179
|
-
+ `for(let i=0;i<cs.length;i++){`
|
|
180
|
-
+ `try{`
|
|
181
|
-
+ `if(i===0){window.DevToolsAPI.dispatchMessageChunk(cs[i], ${totalSize})}`
|
|
182
|
-
+ `else{window.DevToolsAPI.dispatchMessageChunk(cs[i])}`
|
|
183
|
-
+ `}catch(_){}`
|
|
184
|
-
+ `}`
|
|
185
|
-
+ `return true;`
|
|
186
|
-
+ `}catch(_){return false}})()`;
|
|
187
|
-
}
|
|
188
164
|
/** Script that nudges the front-end to discard its document and lazily re-pull. */
|
|
189
165
|
function buildDocumentUpdatedScript() {
|
|
190
166
|
const msg = JSON.stringify({ method: 'DOM.documentUpdated', params: {} });
|
|
@@ -195,9 +171,13 @@ function buildDocumentUpdatedScript() {
|
|
|
195
171
|
// within a tick, cheap because the install script short-circuits to 'already'.
|
|
196
172
|
const DRAIN_INTERVAL_MS = 150;
|
|
197
173
|
/**
|
|
198
|
-
* Install Elements forwarding on a DevTools front-end host wc. Returns a
|
|
199
|
-
* (`stop`) that clears timers, unsubscribes render events,
|
|
200
|
-
*
|
|
174
|
+
* Install Elements forwarding on a DevTools front-end host wc. Returns a
|
|
175
|
+
* disposer (`stop`) that clears timers, unsubscribes render events, releases
|
|
176
|
+
* every broker lease this call acquired, and — only when no `deps.broker` was
|
|
177
|
+
* supplied (this call owns its private broker) — disposes that broker too
|
|
178
|
+
* (which detaches ONLY the sessions it self-attached). When `deps.broker` IS
|
|
179
|
+
* supplied (the shared, app-wide instance), `stop()` never disposes it: other
|
|
180
|
+
* consumers may still be using it.
|
|
201
181
|
*
|
|
202
182
|
* Best-effort + defensive throughout: a destroyed wc ends the feature; every
|
|
203
183
|
* executeJavaScript / sendCommand is wrapped so a torn-down guest never throws
|
|
@@ -206,6 +186,10 @@ const DRAIN_INTERVAL_MS = 150;
|
|
|
206
186
|
*/
|
|
207
187
|
export function installElementsForward(deps) {
|
|
208
188
|
const { devtoolsWc, bridge } = deps;
|
|
189
|
+
// Own (and tear down on stop()) a private broker only when the caller didn't
|
|
190
|
+
// supply a shared one — see ElementsForwardDeps.broker.
|
|
191
|
+
const ownsBroker = !deps.broker;
|
|
192
|
+
const broker = deps.broker ?? createCdpSessionBroker({ connections: deps.connections });
|
|
209
193
|
let disposed = false;
|
|
210
194
|
// A single self-healing loop drives BOTH hook (re)install and outbound drain;
|
|
211
195
|
// it never self-terminates so a front-end reload (respawn) is re-hooked.
|
|
@@ -227,26 +211,26 @@ export function installElementsForward(deps) {
|
|
|
227
211
|
const a = activeRenderWc();
|
|
228
212
|
return a != null && a.id === id;
|
|
229
213
|
};
|
|
230
|
-
// Render guests we've
|
|
231
|
-
//
|
|
232
|
-
const
|
|
233
|
-
// Debugger sessions THIS feature attached (safe-area was degraded). dispose /
|
|
234
|
-
// guest-destroy detaches ONLY these; sessions safe-area owns are never touched.
|
|
235
|
-
const selfAttached = new Set();
|
|
214
|
+
// Render guests we've acquired a broker lease for (keyed wc.id) so a
|
|
215
|
+
// re-resolve of the same guest doesn't double-subscribe.
|
|
216
|
+
const guestLeases = new Map();
|
|
236
217
|
const stop = () => {
|
|
237
218
|
disposed = true;
|
|
238
219
|
if (reconcileTimer) {
|
|
239
220
|
clearInterval(reconcileTimer);
|
|
240
221
|
reconcileTimer = null;
|
|
241
222
|
}
|
|
242
|
-
for (const
|
|
223
|
+
for (const lease of guestLeases.values()) {
|
|
243
224
|
try {
|
|
244
|
-
|
|
225
|
+
lease.dispose();
|
|
245
226
|
}
|
|
246
|
-
catch { /*
|
|
227
|
+
catch { /* already gone */ }
|
|
247
228
|
}
|
|
248
|
-
|
|
249
|
-
|
|
229
|
+
guestLeases.clear();
|
|
230
|
+
// Only detach sessions we self-attached if we own the broker's lifecycle —
|
|
231
|
+
// a shared/injected broker keeps serving other consumers past our stop().
|
|
232
|
+
if (ownsBroker)
|
|
233
|
+
broker.dispose();
|
|
250
234
|
// Run the late-wired teardown once (render events, dom-ready, 'closed' sub).
|
|
251
235
|
if (lateCleanup) {
|
|
252
236
|
const c = lateCleanup;
|
|
@@ -257,20 +241,6 @@ export function installElementsForward(deps) {
|
|
|
257
241
|
catch { /* already gone */ }
|
|
258
242
|
}
|
|
259
243
|
};
|
|
260
|
-
/** Detach every session we own; leave safe-area's alone. */
|
|
261
|
-
function detachSelfAttached() {
|
|
262
|
-
for (const wcId of selfAttached) {
|
|
263
|
-
const wc = wcFromId(wcId);
|
|
264
|
-
if (!wc)
|
|
265
|
-
continue;
|
|
266
|
-
try {
|
|
267
|
-
if (!wc.isDestroyed() && wc.debugger.isAttached())
|
|
268
|
-
wc.debugger.detach();
|
|
269
|
-
}
|
|
270
|
-
catch { /* already detached / destroyed */ }
|
|
271
|
-
}
|
|
272
|
-
selfAttached.clear();
|
|
273
|
-
}
|
|
274
244
|
// ── main → front-end dispatch ──────────────────────────────────────────────
|
|
275
245
|
function dispatchToFrontend(message) {
|
|
276
246
|
if (disposed || devtoolsWc.isDestroyed())
|
|
@@ -345,16 +315,7 @@ export function installElementsForward(deps) {
|
|
|
345
315
|
msg.sessionId = cmd.sessionId;
|
|
346
316
|
dispatchToFrontend(msg);
|
|
347
317
|
}
|
|
348
|
-
// ──
|
|
349
|
-
function wcFromId(id) {
|
|
350
|
-
try {
|
|
351
|
-
return electronWebContents?.fromId?.(id) ?? null;
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
// ── render-guest CDP session (reuse safe-area's; self-attach only if needed) ─
|
|
318
|
+
// ── render-guest CDP session (via the shared broker) ────────────────────────
|
|
358
319
|
/** The active render guest, re-resolved fresh (pool/page swaps go stale). */
|
|
359
320
|
function activeRenderWc() {
|
|
360
321
|
try {
|
|
@@ -366,107 +327,42 @@ export function installElementsForward(deps) {
|
|
|
366
327
|
}
|
|
367
328
|
}
|
|
368
329
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return true;
|
|
378
|
-
}
|
|
379
|
-
catch {
|
|
380
|
-
return false;
|
|
381
|
-
}
|
|
382
|
-
try {
|
|
383
|
-
wc.debugger.attach('1.3');
|
|
384
|
-
selfAttached.add(wc.id);
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
// Either someone else just attached (race → it IS usable) or attach truly
|
|
389
|
-
// failed. Treat "already attached" as success, anything else as failure.
|
|
390
|
-
try {
|
|
391
|
-
return wc.debugger.isAttached();
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Add our render-event re-injection listener to a guest's debugger ONCE. DOM./
|
|
400
|
-
* CSS./Overlay./DOMSnapshot./DOMDebugger. EVENTS (no id) from the CURRENT
|
|
401
|
-
* generation are dispatched into the front-end so the Elements panel updates
|
|
402
|
-
* live; events arriving after the generation moved on are dropped. The listener
|
|
403
|
-
* is removed when the guest wc is destroyed (and on stop()).
|
|
330
|
+
* Get-or-create this guest's broker lease and wire our render-event
|
|
331
|
+
* re-injection listener on it ONCE. DOM./CSS./Overlay./DOMSnapshot./
|
|
332
|
+
* DOMDebugger. EVENTS (no id) from the CURRENT generation are dispatched
|
|
333
|
+
* into the front-end so the Elements panel updates live; events arriving
|
|
334
|
+
* after the generation moved on are dropped. `lease.onDetach` — which fires
|
|
335
|
+
* on an external detach OR the guest being destroyed (see cdp-session's
|
|
336
|
+
* design doc) — drops our cache entry so the NEXT call re-acquires fresh
|
|
337
|
+
* instead of operating on a dead lease forever.
|
|
404
338
|
*/
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
339
|
+
function ensureGuestLease(wc) {
|
|
340
|
+
const existing = guestLeases.get(wc.id);
|
|
341
|
+
if (existing)
|
|
342
|
+
return existing;
|
|
343
|
+
const lease = broker.acquire(wc);
|
|
344
|
+
if (!lease)
|
|
345
|
+
return null;
|
|
346
|
+
lease.onMessage((method, params) => {
|
|
409
347
|
if (disposed)
|
|
410
348
|
return;
|
|
411
|
-
// Honour events only while this guest is STILL the active one — checked
|
|
412
|
-
// event, so re-activating a previously-wired guest resumes forwarding.
|
|
349
|
+
// Honour events only while this guest is STILL the active one — checked
|
|
350
|
+
// per event, so re-activating a previously-wired guest resumes forwarding.
|
|
413
351
|
if (!isActiveWcId(wc.id))
|
|
414
352
|
return;
|
|
415
353
|
if (isRenderEventMethod(method)) {
|
|
416
354
|
dispatchToFrontend({ method, params });
|
|
417
355
|
}
|
|
418
|
-
};
|
|
419
|
-
try {
|
|
420
|
-
wc.debugger.on('message', onMessage);
|
|
421
|
-
}
|
|
422
|
-
catch {
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const onDestroyed = () => {
|
|
426
|
-
const cleanup = wiredGuests.get(wc.id);
|
|
427
|
-
if (cleanup) {
|
|
428
|
-
wiredGuests.delete(wc.id);
|
|
429
|
-
try {
|
|
430
|
-
cleanup();
|
|
431
|
-
}
|
|
432
|
-
catch { /* gone */ }
|
|
433
|
-
}
|
|
434
|
-
// A destroyed guest is no longer the active one, so its in-flight commands
|
|
435
|
-
// fail `isActiveWcId` and settle as errors on their own — no global bump
|
|
436
|
-
// (which would wrongly stale OTHER, still-active guests' commands).
|
|
437
|
-
// If we own this wc's session there is nothing left to detach; drop it.
|
|
438
|
-
selfAttached.delete(wc.id);
|
|
439
|
-
};
|
|
440
|
-
let closedSub;
|
|
441
|
-
try {
|
|
442
|
-
// Route the guest teardown through the connection registry's `'closed'`
|
|
443
|
-
// event when present. Render guests are NEVER pool-reset, so `'closed'`
|
|
444
|
-
// (fires only on real wc destroy) is the correct lifetime — and crucially
|
|
445
|
-
// `on('closed')` returns a handle whose `dispose()` REMOVES the listener
|
|
446
|
-
// WITHOUT firing it, matching the original `removeListener` semantics.
|
|
447
|
-
// (`own()` would fire `onDestroyed` on release AND mutate `wiredGuests`
|
|
448
|
-
// mid-drain in stop() — wrong here; see foundation.md §4.3.) Same
|
|
449
|
-
// try/catch guards a fake/minimal wc that lacks once/emitter wiring.
|
|
450
|
-
if (deps.connections)
|
|
451
|
-
closedSub = deps.connections.acquire(wc).on('closed', onDestroyed);
|
|
452
|
-
else
|
|
453
|
-
wc.once('destroyed', onDestroyed);
|
|
454
|
-
}
|
|
455
|
-
catch { /* fake/minimal wc */ }
|
|
456
|
-
wiredGuests.set(wc.id, () => {
|
|
457
|
-
try {
|
|
458
|
-
wc.debugger.removeListener('message', onMessage);
|
|
459
|
-
}
|
|
460
|
-
catch { /* gone */ }
|
|
461
|
-
try {
|
|
462
|
-
closedSub?.dispose();
|
|
463
|
-
}
|
|
464
|
-
catch { /* gone */ }
|
|
465
|
-
try {
|
|
466
|
-
wc.removeListener('destroyed', onDestroyed);
|
|
467
|
-
}
|
|
468
|
-
catch { /* gone */ }
|
|
469
356
|
});
|
|
357
|
+
lease.onDetach(() => {
|
|
358
|
+
// A destroyed/detached guest is no longer the active one, so its
|
|
359
|
+
// in-flight commands fail `isActiveWcId` and settle as errors on their
|
|
360
|
+
// own — no global bump (which would wrongly stale OTHER, still-active
|
|
361
|
+
// guests' commands).
|
|
362
|
+
guestLeases.delete(wc.id);
|
|
363
|
+
});
|
|
364
|
+
guestLeases.set(wc.id, lease);
|
|
365
|
+
return lease;
|
|
470
366
|
}
|
|
471
367
|
/**
|
|
472
368
|
* enable DOM/CSS/Overlay + wire events on a guest. Best-effort (never throws).
|
|
@@ -478,18 +374,24 @@ export function installElementsForward(deps) {
|
|
|
478
374
|
* the Elements-panel hover highlight never paints. Awaiting DOM.enable first
|
|
479
375
|
* guarantees the correct enable order. CSS.enable has no such dependency and is
|
|
480
376
|
* fire-and-forget alongside.
|
|
377
|
+
*
|
|
378
|
+
* This does NOT delegate to `lease.ensureRenderDomains()`: that helper has no
|
|
379
|
+
* concept of "active guest", but a priming sequence started on a guest that
|
|
380
|
+
* stops being active mid-flight must NOT arm Overlay on the now-abandoned
|
|
381
|
+
* tree (checked again below) — a per-consumer concern only elements-forward
|
|
382
|
+
* has, so it keeps its own sequence, just dispatched through the lease.
|
|
481
383
|
*/
|
|
482
384
|
function primeGuest(wc) {
|
|
483
|
-
|
|
385
|
+
const lease = ensureGuestLease(wc);
|
|
386
|
+
if (!lease)
|
|
484
387
|
return;
|
|
485
|
-
|
|
486
|
-
void enableGuestDomains(wc);
|
|
388
|
+
void enableGuestDomains(wc, lease);
|
|
487
389
|
}
|
|
488
390
|
/** Enable the render domains in dependency order. Resolves; never rejects. */
|
|
489
|
-
async function enableGuestDomains(wc) {
|
|
490
|
-
|
|
391
|
+
async function enableGuestDomains(wc, lease) {
|
|
392
|
+
lease.send('CSS.enable').catch(() => { });
|
|
491
393
|
try {
|
|
492
|
-
await
|
|
394
|
+
await lease.send('DOM.enable');
|
|
493
395
|
}
|
|
494
396
|
catch {
|
|
495
397
|
// Guest mid-destroy or DOM domain unavailable; Overlay.enable would only
|
|
@@ -501,7 +403,7 @@ export function installElementsForward(deps) {
|
|
|
501
403
|
// arming Overlay so a stale/dead guest is left untouched.
|
|
502
404
|
if (disposed || !isActiveWcId(wc.id))
|
|
503
405
|
return;
|
|
504
|
-
|
|
406
|
+
lease.send('Overlay.enable').catch(() => { });
|
|
505
407
|
}
|
|
506
408
|
// ── front-end → render command routing ─────────────────────────────────────
|
|
507
409
|
function routeCommand(cmd) {
|
|
@@ -510,18 +412,18 @@ export function installElementsForward(deps) {
|
|
|
510
412
|
replyError(cmd, 'no active render guest');
|
|
511
413
|
return;
|
|
512
414
|
}
|
|
513
|
-
|
|
415
|
+
const lease = ensureGuestLease(wc);
|
|
416
|
+
if (!lease) {
|
|
514
417
|
replyError(cmd, 'render guest debugger unavailable');
|
|
515
418
|
return;
|
|
516
419
|
}
|
|
517
|
-
wireGuestEvents(wc);
|
|
518
420
|
// The wc this command is dispatched to. A response that resolves after the
|
|
519
421
|
// active guest changed (this wc is no longer active) is for a tree the
|
|
520
422
|
// front-end has abandoned: settle the id with an error (no pending leak) but
|
|
521
423
|
// do NOT feed its result into the new tree.
|
|
522
424
|
const cmdWcId = wc.id;
|
|
523
|
-
|
|
524
|
-
.
|
|
425
|
+
lease
|
|
426
|
+
.send(cmd.method, (cmd.params ?? {}))
|
|
525
427
|
.then((result) => {
|
|
526
428
|
if (disposed)
|
|
527
429
|
return;
|
|
@@ -535,7 +437,7 @@ export function installElementsForward(deps) {
|
|
|
535
437
|
// enabled" and the hover highlight stops painting. Re-arm it so the next
|
|
536
438
|
// hover paints — only while this guest is still the active one.
|
|
537
439
|
if (cmd.method === 'Overlay.disable' && isActiveWcId(cmdWcId)) {
|
|
538
|
-
|
|
440
|
+
lease.send('Overlay.enable').catch(() => { });
|
|
539
441
|
}
|
|
540
442
|
replyResult(cmd, result);
|
|
541
443
|
})
|
|
@@ -545,7 +447,32 @@ export function installElementsForward(deps) {
|
|
|
545
447
|
replyError(cmd, err instanceof Error ? err.message : String(err));
|
|
546
448
|
});
|
|
547
449
|
}
|
|
548
|
-
/**
|
|
450
|
+
/**
|
|
451
|
+
* Answer a network-routed command from the provider. Never touches the render
|
|
452
|
+
* guest: the body lives in the network forwarder's prefetch cache (or
|
|
453
|
+
* nowhere). A missing provider or a rejected lookup settles the command with
|
|
454
|
+
* the canonical CDP not-found error so the front-end renders its normal
|
|
455
|
+
* failure state instead of leaking a forever-pending request.
|
|
456
|
+
*/
|
|
457
|
+
function answerNetworkCommand(cmd) {
|
|
458
|
+
const provider = deps.network;
|
|
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
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/** Process a drained batch of front-end commands, splitting by route tag. */
|
|
549
476
|
function handleOutbound(batch) {
|
|
550
477
|
if (!Array.isArray(batch))
|
|
551
478
|
return;
|
|
@@ -555,6 +482,12 @@ export function installElementsForward(deps) {
|
|
|
555
482
|
const cmd = raw;
|
|
556
483
|
if (typeof cmd.method !== 'string')
|
|
557
484
|
continue;
|
|
485
|
+
if (cmd.route === 'network') {
|
|
486
|
+
answerNetworkCommand(cmd);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
// 'render' — and any untagged legacy payload, which only ever carried
|
|
490
|
+
// render-domain commands.
|
|
558
491
|
routeCommand(cmd);
|
|
559
492
|
}
|
|
560
493
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrefetchCache — a bounded, TTL'd "prime now / read later" promise cache.
|
|
3
|
+
*
|
|
4
|
+
* The network forwarder prefetches `Network.getResponseBody` /
|
|
5
|
+
* `Network.getRequestPostData` from the simulator's CDP session the moment a
|
|
6
|
+
* request finishes (the renderer may evict the resource soon after), keyed by
|
|
7
|
+
* the namespaced virtual requestId. A later Response-tab click resolves from
|
|
8
|
+
* here instead of re-issuing a debugger round-trip against a session that may
|
|
9
|
+
* have moved on.
|
|
10
|
+
*
|
|
11
|
+
* Contract:
|
|
12
|
+
* - `prime(id, fetch)` is idempotent per id: while an entry is pending or a
|
|
13
|
+
* settled entry is still live, later primes are ignored.
|
|
14
|
+
* - A fetch rejection — or a resolved value whose `sizeOf` exceeds
|
|
15
|
+
* `perEntryMaxChars` — becomes an ERROR SENTINEL: the id stays known but
|
|
16
|
+
* every lookup rejects with the canonical not-found message (the same
|
|
17
|
+
* answer a real CDP backend gives for an unknown requestId, so the
|
|
18
|
+
* front-end renders its normal failure state).
|
|
19
|
+
* - `lookup(id)` resolves the cached value, awaits a still-pending fetch (a
|
|
20
|
+
* panel click can land before the prefetch settles), and rejects
|
|
21
|
+
* not-found for unknown / expired / evicted / sentinel entries.
|
|
22
|
+
* - Bounds apply to SETTLED entries only: LRU by settle/lookup recency up to
|
|
23
|
+
* `maxEntries`, plus a `maxTotalChars` budget over `sizeOf`. A pending
|
|
24
|
+
* entry counts 0 and is never evicted — evicting an in-flight prefetch
|
|
25
|
+
* would strand its waiters.
|
|
26
|
+
* - TTL is anchored to the SETTLE time (not the prime() call time) and is
|
|
27
|
+
* enforced lazily on the next prime/lookup.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Default per-entry size ceiling (chars). Exported so callers that need to
|
|
31
|
+
* pre-screen a fetch BEFORE issuing it (e.g. skipping a CDP round-trip for a
|
|
32
|
+
* response already known to be oversized) reference the exact same number
|
|
33
|
+
* `PrefetchCache`'s own default enforces — one source of truth, not two
|
|
34
|
+
* independently-configured limits that could silently drift apart.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEFAULT_PER_ENTRY_MAX_CHARS: number;
|
|
37
|
+
export interface PrefetchCacheOptions {
|
|
38
|
+
maxEntries?: number;
|
|
39
|
+
maxTotalChars?: number;
|
|
40
|
+
perEntryMaxChars?: number;
|
|
41
|
+
ttlMs?: number;
|
|
42
|
+
/** Injectable clock for TTL tests. */
|
|
43
|
+
now?: () => number;
|
|
44
|
+
}
|
|
45
|
+
export declare class PrefetchCache<V> {
|
|
46
|
+
private readonly sizeOf;
|
|
47
|
+
private readonly map;
|
|
48
|
+
private readonly maxEntries;
|
|
49
|
+
private readonly maxTotalChars;
|
|
50
|
+
private readonly perEntryMaxChars;
|
|
51
|
+
private readonly ttlMs;
|
|
52
|
+
private readonly now;
|
|
53
|
+
constructor(sizeOf: (v: V) => number, opts?: PrefetchCacheOptions);
|
|
54
|
+
/**
|
|
55
|
+
* Returns `true` when this call actually started a new fetch, `false` when
|
|
56
|
+
* it was a no-op (idempotent re-prime of an id that's already pending or
|
|
57
|
+
* still live). Callers that admission-gate priming (a concurrency cap) need
|
|
58
|
+
* this to know whether they actually consumed a slot — incrementing a
|
|
59
|
+
* counter unconditionally would leak a slot on every no-op re-prime.
|
|
60
|
+
*/
|
|
61
|
+
prime(id: string, fetch: () => Promise<V>): boolean;
|
|
62
|
+
lookup(id: string): Promise<V>;
|
|
63
|
+
clear(): void;
|
|
64
|
+
get size(): number;
|
|
65
|
+
private isExpired;
|
|
66
|
+
private sweepExpired;
|
|
67
|
+
/** Evict oldest SETTLED entries until both bounds hold; pending are exempt. */
|
|
68
|
+
private evict;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=body-cache.d.ts.map
|