@jobshimo/browser-link 0.34.1 → 0.36.0
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/agent-instructions/content.js +19 -6
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/bridge/events.d.ts +1 -1
- package/dist/bridge/events.js +8 -1
- package/dist/bridge/events.js.map +1 -1
- package/dist/bridge/ws-bridge.d.ts +17 -0
- package/dist/bridge/ws-bridge.js +34 -0
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/cdp/inpage/builders.d.ts +70 -3
- package/dist/cdp/inpage/builders.js +197 -36
- package/dist/cdp/inpage/builders.js.map +1 -1
- package/dist/cdp/inpage/observer.d.ts +77 -0
- package/dist/cdp/inpage/observer.js +509 -0
- package/dist/cdp/inpage/observer.js.map +1 -0
- package/dist/cdp/observer-host.d.ts +137 -0
- package/dist/cdp/observer-host.js +188 -0
- package/dist/cdp/observer-host.js.map +1 -0
- package/dist/cdp/transport.js +161 -25
- package/dist/cdp/transport.js.map +1 -1
- package/dist/extension/background.js +183 -33
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/inpage/builders.d.ts +70 -3
- package/dist/extension/inpage/builders.js +196 -35
- package/dist/extension/inpage/builders.js.map +1 -1
- package/dist/extension/inpage/observer.d.ts +88 -0
- package/dist/extension/inpage/observer.js +520 -0
- package/dist/extension/inpage/observer.js.map +1 -0
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/observer-host.d.ts +84 -0
- package/dist/extension/observer-host.js +172 -0
- package/dist/extension/observer-host.js.map +1 -0
- package/dist/tools/browser-definitions.js +16 -6
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.js +14 -2
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +19 -5
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent copy of `packages/extension/src/inpage/observer.ts` — see
|
|
3
|
+
* `./deep-query.ts`'s header comment for why this is a verbatim duplicate
|
|
4
|
+
* rather than a cross-package import.
|
|
5
|
+
*
|
|
6
|
+
* The page observer: a persistent, coalescing MutationObserver that runs in
|
|
7
|
+
* the page's MAIN world (spec v4 §5.1–§5.3). cdp-direct installs it with the
|
|
8
|
+
* same `Runtime.addBinding` + `Page.addScriptToEvaluateOnNewDocument` pair as
|
|
9
|
+
* the extension. Everything below this header must stay byte-identical to the
|
|
10
|
+
* extension original — `cdp/drift.test.ts` enforces it and
|
|
11
|
+
* `cdp/builders-parity.test.ts` checks the built output.
|
|
12
|
+
*/
|
|
13
|
+
/** Bump whenever the in-page protocol (global shape, messages, methods) changes. */
|
|
14
|
+
export const OBSERVER_VERSION = 2;
|
|
15
|
+
/** Name of the `Runtime.addBinding` function the observer pushes messages through. */
|
|
16
|
+
export const OBSERVER_BINDING = '__blPageObserverPush';
|
|
17
|
+
/** Well-known global that holds the installed observer instance. */
|
|
18
|
+
export const OBSERVER_GLOBAL = '__blPageObserver';
|
|
19
|
+
const DEFAULT_DESCRIPTOR_CAP = 20;
|
|
20
|
+
const MAX_DESCRIPTOR_CAP = 100;
|
|
21
|
+
const DEFAULT_RING_SIZE = 64;
|
|
22
|
+
const MAX_RING_SIZE = 1024;
|
|
23
|
+
function clampInt(value, fallback, min, max) {
|
|
24
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
25
|
+
return fallback;
|
|
26
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the observer install source. Valid both as a
|
|
30
|
+
* `Page.addScriptToEvaluateOnNewDocument` source and as a one-off
|
|
31
|
+
* `Runtime.evaluate` expression, which evaluates to
|
|
32
|
+
* `{ ok: true, installed, version, doc_id, coverage }`.
|
|
33
|
+
*
|
|
34
|
+
* The instance also owns element identity (spec v4 §5.6): `refFor(el)` issues
|
|
35
|
+
* `e<N>` (monotonic per document instance, from `e1`) the first time an
|
|
36
|
+
* element is seen and reuses it while the node lives; `resolveRef(ref)`
|
|
37
|
+
* returns the element while it is connected, else `null`. Refs are only valid
|
|
38
|
+
* for the `doc_id` that issued them. Neither method throws. `nextSnapshotId()`
|
|
39
|
+
* issues `<doc_id>:s<N>` (N monotonic per document instance, from 1) for each
|
|
40
|
+
* `snapshot` result and returns `null` once the instance is torn down; the
|
|
41
|
+
* `doc_id` prefix lets a stale id from another document be rejected.
|
|
42
|
+
* `recordSnapshot(id, record)` keeps the last 4 snapshot records per document
|
|
43
|
+
* instance (oldest evicted first) for `snapshot({ since })`; `getSnapshot(id)`
|
|
44
|
+
* returns the record or `null` when unknown or evicted. The record is opaque
|
|
45
|
+
* here. Both are inert after teardown, which clears the store.
|
|
46
|
+
*/
|
|
47
|
+
export function buildObserverInstallJs(opts = {}) {
|
|
48
|
+
const optsJson = JSON.stringify({
|
|
49
|
+
key: OBSERVER_GLOBAL,
|
|
50
|
+
binding: typeof opts.binding === 'string' && opts.binding.length > 0 ? opts.binding : OBSERVER_BINDING,
|
|
51
|
+
version: clampInt(opts.version, OBSERVER_VERSION, 0, Number.MAX_SAFE_INTEGER),
|
|
52
|
+
cap: clampInt(opts.descriptor_cap, DEFAULT_DESCRIPTOR_CAP, 1, MAX_DESCRIPTOR_CAP),
|
|
53
|
+
ringSize: clampInt(opts.ring_size, DEFAULT_RING_SIZE, 1, MAX_RING_SIZE),
|
|
54
|
+
});
|
|
55
|
+
return `
|
|
56
|
+
(() => {
|
|
57
|
+
const opts = ${optsJson};
|
|
58
|
+
const existing = window[opts.key];
|
|
59
|
+
if (existing && typeof existing.version === 'number' && existing.version >= opts.version) {
|
|
60
|
+
return { ok: true, installed: false, version: existing.version, doc_id: existing.doc_id, coverage: existing.coverage };
|
|
61
|
+
}
|
|
62
|
+
if (existing && typeof existing.teardown === 'function') {
|
|
63
|
+
try { existing.teardown(); } catch (e) {}
|
|
64
|
+
}
|
|
65
|
+
let isFrame = true;
|
|
66
|
+
try { isFrame = window !== window.top; } catch (e) {}
|
|
67
|
+
const zeroKinds = () => ({ childList: 0, attributes: 0, characterData: 0 });
|
|
68
|
+
const g = {
|
|
69
|
+
version: opts.version,
|
|
70
|
+
doc_id: Math.random().toString(36).slice(2, 10) + Date.now().toString(36),
|
|
71
|
+
seq: 0,
|
|
72
|
+
ring: [],
|
|
73
|
+
pending: new Map(),
|
|
74
|
+
pendingRecords: 0,
|
|
75
|
+
pendingKinds: zeroKinds(),
|
|
76
|
+
pendingRoots: { document: false, shadow: false },
|
|
77
|
+
t0: 0,
|
|
78
|
+
timer: null,
|
|
79
|
+
rescan: null,
|
|
80
|
+
roots: 0,
|
|
81
|
+
coverage: 'full',
|
|
82
|
+
baselines: new Map(),
|
|
83
|
+
dead: false,
|
|
84
|
+
};
|
|
85
|
+
const observers = [];
|
|
86
|
+
const observed = new WeakSet();
|
|
87
|
+
let refs = new WeakMap();
|
|
88
|
+
const refNodes = new Map();
|
|
89
|
+
let refSeq = 0;
|
|
90
|
+
let snapshotSeq = 0;
|
|
91
|
+
// Last SNAPSHOT_KEEP snapshot records (opaque to the observer; the snapshot
|
|
92
|
+
// builder owns their shape), keyed by snapshot_id in insertion order.
|
|
93
|
+
const SNAPSHOT_KEEP = 4;
|
|
94
|
+
const snapshots = new Map();
|
|
95
|
+
// WeakRef keeps the registry from pinning removed nodes; the fallback only
|
|
96
|
+
// matters on engines without it (Chrome has it since 84).
|
|
97
|
+
const holdWeakly =
|
|
98
|
+
typeof WeakRef === 'function' ? (el) => new WeakRef(el) : (el) => ({ deref: () => el });
|
|
99
|
+
const round = (n) => Math.round(n * 10) / 10;
|
|
100
|
+
|
|
101
|
+
function push(message) {
|
|
102
|
+
const fn = window[opts.binding];
|
|
103
|
+
if (typeof fn !== 'function') return;
|
|
104
|
+
try { fn(JSON.stringify(message)); } catch (e) {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fold(records, kind) {
|
|
108
|
+
for (const rec of records) {
|
|
109
|
+
if (g.pendingRecords === 0) g.t0 = performance.now();
|
|
110
|
+
g.pendingRecords++;
|
|
111
|
+
g.pendingKinds[rec.type]++;
|
|
112
|
+
g.pendingRoots[kind] = true;
|
|
113
|
+
const target = rec.type === 'characterData' ? rec.target.parentNode : rec.target;
|
|
114
|
+
if (!target) continue;
|
|
115
|
+
g.pending.set(target, (g.pending.get(target) || 0) + 1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function drain() {
|
|
120
|
+
for (const o of observers) {
|
|
121
|
+
const recs = o.mo.takeRecords();
|
|
122
|
+
if (recs.length) fold(recs, o.kind);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function segment(el) {
|
|
127
|
+
const tag = el.localName || String(el.nodeName).toLowerCase();
|
|
128
|
+
return el.id ? tag + '#' + el.id : tag;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function pathOf(node) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
let n = node;
|
|
134
|
+
let segments = 0;
|
|
135
|
+
while (n && segments < 6) {
|
|
136
|
+
if (n.nodeType === 1) {
|
|
137
|
+
if (n === document.documentElement) break;
|
|
138
|
+
parts.unshift(segment(n));
|
|
139
|
+
segments++;
|
|
140
|
+
n = n.parentNode;
|
|
141
|
+
} else if (n.nodeType === 11 && n.host) {
|
|
142
|
+
parts.unshift('#shadow');
|
|
143
|
+
n = n.host;
|
|
144
|
+
} else {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return parts.join('>');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function describe(node) {
|
|
152
|
+
if (node.nodeType === 11) {
|
|
153
|
+
return { tag: '#shadow-root', path: node.host ? pathOf(node.host) + '>#shadow' : '#shadow' };
|
|
154
|
+
}
|
|
155
|
+
if (node.nodeType === 9) return { tag: '#document', path: '#document' };
|
|
156
|
+
const d = { tag: node.localName || String(node.nodeName).toLowerCase() };
|
|
157
|
+
if (typeof node.id === 'string' && node.id) d.id = node.id;
|
|
158
|
+
const role = typeof node.getAttribute === 'function' ? node.getAttribute('role') : null;
|
|
159
|
+
if (role) d.role = role;
|
|
160
|
+
d.path = pathOf(node);
|
|
161
|
+
return d;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function flush() {
|
|
165
|
+
if (g.timer !== null) {
|
|
166
|
+
clearTimeout(g.timer);
|
|
167
|
+
g.timer = null;
|
|
168
|
+
}
|
|
169
|
+
drain();
|
|
170
|
+
if (g.pendingRecords === 0) return null;
|
|
171
|
+
const entries = Array.from(g.pending.entries()).sort((a, b) => b[1] - a[1]);
|
|
172
|
+
const top = entries.slice(0, opts.cap);
|
|
173
|
+
const summary = {
|
|
174
|
+
type: 'summary',
|
|
175
|
+
doc_id: g.doc_id,
|
|
176
|
+
seq: ++g.seq,
|
|
177
|
+
t0: round(g.t0),
|
|
178
|
+
t1: round(performance.now()),
|
|
179
|
+
records: g.pendingRecords,
|
|
180
|
+
targets: entries.length,
|
|
181
|
+
kinds: g.pendingKinds,
|
|
182
|
+
roots: { document: g.pendingRoots.document, shadow: g.pendingRoots.shadow, frame: isFrame },
|
|
183
|
+
descriptors: top.map((e) => describe(e[0])),
|
|
184
|
+
overflow: entries.length - top.length,
|
|
185
|
+
};
|
|
186
|
+
g.ring.push({ summary: summary, counts: top.map((e) => e[1]) });
|
|
187
|
+
while (g.ring.length > opts.ringSize) g.ring.shift();
|
|
188
|
+
g.pending = new Map();
|
|
189
|
+
g.pendingRecords = 0;
|
|
190
|
+
g.pendingKinds = zeroKinds();
|
|
191
|
+
g.pendingRoots = { document: false, shadow: false };
|
|
192
|
+
push(summary);
|
|
193
|
+
return summary;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function onTimer() {
|
|
197
|
+
g.timer = null;
|
|
198
|
+
try { flush(); } catch (e) {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function onRecords(kind) {
|
|
202
|
+
return (records) => {
|
|
203
|
+
if (g.dead) return;
|
|
204
|
+
try {
|
|
205
|
+
fold(records, kind);
|
|
206
|
+
if (g.pendingRecords > 0 && g.timer === null) g.timer = setTimeout(onTimer, 16);
|
|
207
|
+
} catch (e) {}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function observe(root, kind) {
|
|
212
|
+
if (!root || observed.has(root)) return;
|
|
213
|
+
observed.add(root);
|
|
214
|
+
try {
|
|
215
|
+
const mo = new MutationObserver(onRecords(kind));
|
|
216
|
+
mo.observe(root, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
217
|
+
observers.push({ mo: mo, kind: kind });
|
|
218
|
+
g.roots++;
|
|
219
|
+
} catch (e) {}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function walk(root) {
|
|
223
|
+
let all;
|
|
224
|
+
try { all = root.querySelectorAll('*'); } catch (e) { return; }
|
|
225
|
+
for (const el of all) {
|
|
226
|
+
const sr = el.shadowRoot;
|
|
227
|
+
if (sr) {
|
|
228
|
+
observe(sr, 'shadow');
|
|
229
|
+
walk(sr);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
observe(document, 'document');
|
|
235
|
+
walk(document);
|
|
236
|
+
|
|
237
|
+
const proto = Element.prototype;
|
|
238
|
+
const originalAttach = proto.attachShadow;
|
|
239
|
+
let patchedAttach = null;
|
|
240
|
+
if (typeof originalAttach === 'function') {
|
|
241
|
+
try {
|
|
242
|
+
const candidate = function attachShadow() {
|
|
243
|
+
const root = originalAttach.apply(this, arguments);
|
|
244
|
+
if (!g.dead) {
|
|
245
|
+
try { observe(root, 'shadow'); } catch (e) {}
|
|
246
|
+
}
|
|
247
|
+
return root;
|
|
248
|
+
};
|
|
249
|
+
proto.attachShadow = candidate;
|
|
250
|
+
if (proto.attachShadow !== candidate) throw new Error('attachShadow not patchable');
|
|
251
|
+
patchedAttach = candidate;
|
|
252
|
+
} catch (e) {
|
|
253
|
+
g.coverage = 'partial';
|
|
254
|
+
g.rescan = setInterval(() => {
|
|
255
|
+
try { walk(document); } catch (err) {}
|
|
256
|
+
}, 2000);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function onNav() {
|
|
261
|
+
if (g.dead) return;
|
|
262
|
+
push({ type: 'nav', doc_id: g.doc_id, url: location.href });
|
|
263
|
+
}
|
|
264
|
+
const hist = window.history;
|
|
265
|
+
const historyPatches = [];
|
|
266
|
+
for (const name of ['pushState', 'replaceState']) {
|
|
267
|
+
const original = hist[name];
|
|
268
|
+
if (typeof original !== 'function') continue;
|
|
269
|
+
const hadOwn = Object.prototype.hasOwnProperty.call(hist, name);
|
|
270
|
+
const wrapped = function () {
|
|
271
|
+
const result = original.apply(this, arguments);
|
|
272
|
+
try { onNav(); } catch (e) {}
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
275
|
+
try {
|
|
276
|
+
hist[name] = wrapped;
|
|
277
|
+
historyPatches.push({ name: name, original: original, wrapped: wrapped, hadOwn: hadOwn });
|
|
278
|
+
} catch (e) {}
|
|
279
|
+
}
|
|
280
|
+
window.addEventListener('popstate', onNav);
|
|
281
|
+
window.addEventListener('hashchange', onNav);
|
|
282
|
+
|
|
283
|
+
g.flush = flush;
|
|
284
|
+
|
|
285
|
+
g.refFor = function (el) {
|
|
286
|
+
try {
|
|
287
|
+
if (g.dead || !el || el.nodeType !== 1) return null;
|
|
288
|
+
let ref = refs.get(el);
|
|
289
|
+
if (!ref) {
|
|
290
|
+
ref = 'e' + ++refSeq;
|
|
291
|
+
refs.set(el, ref);
|
|
292
|
+
}
|
|
293
|
+
// Same node, same ref: re-register one pruned while it was detached.
|
|
294
|
+
if (!refNodes.has(ref)) refNodes.set(ref, holdWeakly(el));
|
|
295
|
+
return ref;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
g.resolveRef = function (ref) {
|
|
302
|
+
try {
|
|
303
|
+
if (typeof ref !== 'string') return null;
|
|
304
|
+
const held = refNodes.get(ref);
|
|
305
|
+
if (!held) return null;
|
|
306
|
+
const el = held.deref();
|
|
307
|
+
if (el && el.isConnected) return el;
|
|
308
|
+
refNodes.delete(ref);
|
|
309
|
+
return null;
|
|
310
|
+
} catch (e) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
g.nextSnapshotId = function () {
|
|
316
|
+
if (g.dead) return null;
|
|
317
|
+
return g.doc_id + ':s' + ++snapshotSeq;
|
|
318
|
+
};
|
|
319
|
+
g.recordSnapshot = function (id, record) {
|
|
320
|
+
if (g.dead || typeof id !== 'string') return;
|
|
321
|
+
snapshots.delete(id);
|
|
322
|
+
snapshots.set(id, record);
|
|
323
|
+
while (snapshots.size > SNAPSHOT_KEEP) snapshots.delete(snapshots.keys().next().value);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
g.getSnapshot = function (id) {
|
|
327
|
+
if (g.dead || typeof id !== 'string') return null;
|
|
328
|
+
const record = snapshots.get(id);
|
|
329
|
+
return record === undefined ? null : record;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
g.mark = function () {
|
|
333
|
+
flush();
|
|
334
|
+
const mark = g.seq;
|
|
335
|
+
g.baselines.delete(mark);
|
|
336
|
+
g.baselines.set(mark, { url: location.href, active: document.activeElement });
|
|
337
|
+
while (g.baselines.size > 16) g.baselines.delete(g.baselines.keys().next().value);
|
|
338
|
+
return { ok: true, doc_id: g.doc_id, mark: mark, url: location.href, has_focus_baseline: true };
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
g.changes = function (mark, docId) {
|
|
342
|
+
if (docId && docId !== g.doc_id) return { ok: false, reason: 'doc-changed' };
|
|
343
|
+
const base = g.baselines.get(mark);
|
|
344
|
+
if (!base) return { ok: false, reason: 'doc-changed' };
|
|
345
|
+
flush();
|
|
346
|
+
const items = g.ring.filter((it) => it.summary.seq > mark);
|
|
347
|
+
const kinds = zeroKinds();
|
|
348
|
+
const roots = { document: false, shadow: false, frame: isFrame };
|
|
349
|
+
const merged = new Map();
|
|
350
|
+
let records = 0;
|
|
351
|
+
let targets = 0;
|
|
352
|
+
let overflow = 0;
|
|
353
|
+
for (const it of items) {
|
|
354
|
+
const s = it.summary;
|
|
355
|
+
records += s.records;
|
|
356
|
+
targets += s.targets;
|
|
357
|
+
overflow += s.overflow;
|
|
358
|
+
kinds.childList += s.kinds.childList;
|
|
359
|
+
kinds.attributes += s.kinds.attributes;
|
|
360
|
+
kinds.characterData += s.kinds.characterData;
|
|
361
|
+
roots.document = roots.document || s.roots.document;
|
|
362
|
+
roots.shadow = roots.shadow || s.roots.shadow;
|
|
363
|
+
s.descriptors.forEach((d, i) => {
|
|
364
|
+
const key = d.tag + '\\u0000' + (d.id || '') + '\\u0000' + d.path;
|
|
365
|
+
const m = merged.get(key);
|
|
366
|
+
if (m) m.n += it.counts[i];
|
|
367
|
+
else merged.set(key, { d: d, n: it.counts[i] });
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const sorted = Array.from(merged.values()).sort((a, b) => b.n - a.n);
|
|
371
|
+
const descriptors = sorted.slice(0, opts.cap).map((m) => m.d);
|
|
372
|
+
overflow += sorted.length - descriptors.length;
|
|
373
|
+
const out = {
|
|
374
|
+
ok: true,
|
|
375
|
+
doc_id: g.doc_id,
|
|
376
|
+
since: mark,
|
|
377
|
+
records: records,
|
|
378
|
+
targets: targets,
|
|
379
|
+
kinds: kinds,
|
|
380
|
+
roots: roots,
|
|
381
|
+
descriptors: descriptors,
|
|
382
|
+
overflow: overflow,
|
|
383
|
+
};
|
|
384
|
+
if (g.seq > mark && (items.length === 0 || items[0].summary.seq !== mark + 1)) out.truncated = true;
|
|
385
|
+
if (location.href !== base.url) out.url_changed = location.href;
|
|
386
|
+
if (document.activeElement !== base.active) out.focus_moved = true;
|
|
387
|
+
return out;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
g.teardown = function () {
|
|
391
|
+
if (g.dead) return;
|
|
392
|
+
g.dead = true;
|
|
393
|
+
if (g.timer !== null) clearTimeout(g.timer);
|
|
394
|
+
if (g.rescan !== null) clearInterval(g.rescan);
|
|
395
|
+
for (const o of observers) {
|
|
396
|
+
try { o.mo.disconnect(); } catch (e) {}
|
|
397
|
+
}
|
|
398
|
+
observers.length = 0;
|
|
399
|
+
if (patchedAttach && proto.attachShadow === patchedAttach) {
|
|
400
|
+
try { proto.attachShadow = originalAttach; } catch (e) {}
|
|
401
|
+
}
|
|
402
|
+
for (const p of historyPatches) {
|
|
403
|
+
if (hist[p.name] !== p.wrapped) continue;
|
|
404
|
+
try {
|
|
405
|
+
if (p.hadOwn) hist[p.name] = p.original;
|
|
406
|
+
else delete hist[p.name];
|
|
407
|
+
} catch (e) {}
|
|
408
|
+
}
|
|
409
|
+
window.removeEventListener('popstate', onNav);
|
|
410
|
+
window.removeEventListener('hashchange', onNav);
|
|
411
|
+
g.baselines.clear();
|
|
412
|
+
refs = new WeakMap();
|
|
413
|
+
refNodes.clear();
|
|
414
|
+
snapshotSeq = 0;
|
|
415
|
+
snapshots.clear();
|
|
416
|
+
if (window[opts.key] === g) {
|
|
417
|
+
try { delete window[opts.key]; } catch (e) {}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
Object.defineProperty(window, opts.key, { value: g, configurable: true, writable: true, enumerable: false });
|
|
422
|
+
push({ type: 'doc_start', doc_id: g.doc_id, url: location.href, version: g.version, coverage: g.coverage });
|
|
423
|
+
return { ok: true, installed: true, version: g.version, doc_id: g.doc_id, coverage: g.coverage };
|
|
424
|
+
})()`;
|
|
425
|
+
}
|
|
426
|
+
const UNAVAILABLE = "{ ok: false, reason: 'observer-unavailable' }";
|
|
427
|
+
function callInstance(call) {
|
|
428
|
+
return `
|
|
429
|
+
(() => {
|
|
430
|
+
const g = window[${JSON.stringify(OBSERVER_GLOBAL)}];
|
|
431
|
+
if (!g || g.dead || typeof g.doc_id !== 'string') return ${UNAVAILABLE};
|
|
432
|
+
try {
|
|
433
|
+
return ${call};
|
|
434
|
+
} catch (e) {
|
|
435
|
+
return ${UNAVAILABLE};
|
|
436
|
+
}
|
|
437
|
+
})()`;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Build the action-mark expression. Forces a flush (including records still
|
|
441
|
+
* queued in the observers) so the mark is exact, then baselines URL and focus.
|
|
442
|
+
* Evaluates to `{ ok: true, doc_id, mark, url, has_focus_baseline: true }` or
|
|
443
|
+
* `{ ok: false, reason: 'observer-unavailable' }`.
|
|
444
|
+
*/
|
|
445
|
+
export function buildObserverMarkJs() {
|
|
446
|
+
return callInstance('g.mark()');
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Build the "changes since mark" expression. Forces a flush, then aggregates
|
|
450
|
+
* the ring summaries with `seq > mark`. `docId`, when given, must match the
|
|
451
|
+
* current document. Evaluates to the aggregated changes (see
|
|
452
|
+
* `SettleChanges` in `@browser-link/shared`) plus `ok`, `doc_id`, `since`,
|
|
453
|
+
* and `url_changed` / `focus_moved` only when they differ from the mark's
|
|
454
|
+
* baseline; or `{ ok: false, reason: 'observer-unavailable' | 'doc-changed' }`.
|
|
455
|
+
*/
|
|
456
|
+
export function buildObserverChangesJs(mark, docId) {
|
|
457
|
+
const markJson = JSON.stringify(clampInt(mark, 0, 0, Number.MAX_SAFE_INTEGER));
|
|
458
|
+
const docJson = typeof docId === 'string' && docId.length > 0 ? jsStringLiteral(docId) : 'null';
|
|
459
|
+
return callInstance(`g.changes(${markJson}, ${docJson})`);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* A string literal safe to embed in JS source. `JSON.stringify` alone leaves
|
|
463
|
+
* U+2028 / U+2029 (line terminators in older JS grammars) and `<` (which can
|
|
464
|
+
* close a surrounding `<script>`) unescaped; this escapes them as `\uXXXX`,
|
|
465
|
+
* which parses back to the same string.
|
|
466
|
+
*/
|
|
467
|
+
function jsStringLiteral(value) {
|
|
468
|
+
// The two separators are written as char codes so this source file never
|
|
469
|
+
// carries them raw; the replacement is the six-character escape sequence.
|
|
470
|
+
const lineSeparator = String.fromCharCode(0x2028);
|
|
471
|
+
const paragraphSeparator = String.fromCharCode(0x2029);
|
|
472
|
+
return JSON.stringify(value)
|
|
473
|
+
.split(lineSeparator)
|
|
474
|
+
.join('\\u2028')
|
|
475
|
+
.split(paragraphSeparator)
|
|
476
|
+
.join('\\u2029')
|
|
477
|
+
.replace(/</g, '\\u003c');
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Build the ref-resolve expression, for tests and debugging. Evaluates to
|
|
481
|
+
* `{ ok: true, found: true, tag }` when `ref` names an element of the current
|
|
482
|
+
* document that is still connected, `{ ok: true, found: false }` when it is
|
|
483
|
+
* unknown or stale, or `{ ok: false, reason: 'observer-unavailable' }`.
|
|
484
|
+
*/
|
|
485
|
+
export function buildObserverRefResolveJs(ref) {
|
|
486
|
+
const refJson = jsStringLiteral(typeof ref === 'string' ? ref : '');
|
|
487
|
+
return callInstance(`((el) => el ? { ok: true, found: true, tag: el.localName || String(el.nodeName).toLowerCase() } : { ok: true, found: false })(g.resolveRef(${refJson}))`);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Build the teardown expression: disconnects every observer, restores the
|
|
491
|
+
* `attachShadow` and `history` patches when they are still the observer's,
|
|
492
|
+
* and removes the global. Idempotent; evaluates to `{ ok: true, removed }`.
|
|
493
|
+
*/
|
|
494
|
+
export function buildObserverTeardownJs() {
|
|
495
|
+
return `
|
|
496
|
+
(() => {
|
|
497
|
+
const key = ${JSON.stringify(OBSERVER_GLOBAL)};
|
|
498
|
+
const g = window[key];
|
|
499
|
+
if (!g) return { ok: true, removed: false };
|
|
500
|
+
if (typeof g.teardown === 'function') {
|
|
501
|
+
try { g.teardown(); } catch (e) {}
|
|
502
|
+
}
|
|
503
|
+
if (window[key] === g) {
|
|
504
|
+
try { delete window[key]; } catch (e) {}
|
|
505
|
+
}
|
|
506
|
+
return { ok: true, removed: true };
|
|
507
|
+
})()`;
|
|
508
|
+
}
|
|
509
|
+
//# sourceMappingURL=observer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.js","sourceRoot":"","sources":["../../../src/cdp/inpage/observer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Per-kind MutationRecord counts reported by the page observer. */
|
|
2
|
+
interface PageObserverKinds {
|
|
3
|
+
childList: number;
|
|
4
|
+
attributes: number;
|
|
5
|
+
characterData: number;
|
|
6
|
+
}
|
|
7
|
+
/** Which kinds of root the counted records came from. `frame` is true when
|
|
8
|
+
* the observer runs in a child frame (`window !== window.top`). */
|
|
9
|
+
interface PageObserverRoots {
|
|
10
|
+
document: boolean;
|
|
11
|
+
shadow: boolean;
|
|
12
|
+
frame: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** One mutated target, as described by the page observer. `path` is a short
|
|
15
|
+
* structural path such as `body>div#app>ul` (at most six element segments,
|
|
16
|
+
* no indexes), with a `#shadow` segment at each shadow-root boundary. A
|
|
17
|
+
* shadow root that was itself the mutation target is reported with tag
|
|
18
|
+
* `#shadow-root`. */
|
|
19
|
+
interface PageObserverDescriptor {
|
|
20
|
+
tag: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
role?: string;
|
|
23
|
+
path: string;
|
|
24
|
+
}
|
|
25
|
+
/** One coalesced flush of the page observer: every MutationRecord seen
|
|
26
|
+
* since the previous flush, folded per target element. */
|
|
27
|
+
interface PageObserverSummary {
|
|
28
|
+
type: 'summary';
|
|
29
|
+
doc_id: string;
|
|
30
|
+
/** Monotonic per document, starting at 1. */
|
|
31
|
+
seq: number;
|
|
32
|
+
/** `performance.now()` at the first record and at the flush. */
|
|
33
|
+
t0: number;
|
|
34
|
+
t1: number;
|
|
35
|
+
records: number;
|
|
36
|
+
/** Distinct target elements in this flush. */
|
|
37
|
+
targets: number;
|
|
38
|
+
kinds: PageObserverKinds;
|
|
39
|
+
roots: PageObserverRoots;
|
|
40
|
+
/** Up to the descriptor cap, ordered by record count (highest first). */
|
|
41
|
+
descriptors: PageObserverDescriptor[];
|
|
42
|
+
/** Targets beyond the descriptor cap. */
|
|
43
|
+
overflow: number;
|
|
44
|
+
}
|
|
45
|
+
/** Pushed once when the page observer installs in a document. */
|
|
46
|
+
interface PageObserverDocStart {
|
|
47
|
+
type: 'doc_start';
|
|
48
|
+
doc_id: string;
|
|
49
|
+
url: string;
|
|
50
|
+
version: number;
|
|
51
|
+
/** `partial` when `attachShadow` could not be patched and the observer
|
|
52
|
+
* falls back to a periodic rescan for open shadow roots. */
|
|
53
|
+
coverage: 'full' | 'partial';
|
|
54
|
+
}
|
|
55
|
+
/** Pushed on `pushState`, `replaceState`, `popstate` and `hashchange`. */
|
|
56
|
+
interface PageObserverNav {
|
|
57
|
+
type: 'nav';
|
|
58
|
+
doc_id: string;
|
|
59
|
+
url: string;
|
|
60
|
+
}
|
|
61
|
+
/** Every message the page observer pushes through its binding (JSON). */
|
|
62
|
+
type PageObserverMessage = PageObserverSummary | PageObserverDocStart | PageObserverNav;
|
|
63
|
+
/** Summaries kept in the host mirror (the page keeps its own, larger ring). */
|
|
64
|
+
export declare const OBSERVER_RECENT_CAP = 32;
|
|
65
|
+
/** Per-tab host state for the page observer of the tab's top document. */
|
|
66
|
+
export interface ObserverMirror {
|
|
67
|
+
/** True once the install commands succeeded on this tab. */
|
|
68
|
+
installed: boolean;
|
|
69
|
+
doc_id?: string;
|
|
70
|
+
url?: string;
|
|
71
|
+
coverage?: 'full' | 'partial';
|
|
72
|
+
last_seq: number;
|
|
73
|
+
recent: PageObserverSummary[];
|
|
74
|
+
}
|
|
75
|
+
export type ObserverMessageOutcome = {
|
|
76
|
+
kind: 'doc_start';
|
|
77
|
+
url: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: 'nav';
|
|
80
|
+
url: string;
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'summary';
|
|
83
|
+
} | {
|
|
84
|
+
kind: 'ignored';
|
|
85
|
+
};
|
|
86
|
+
/** Reason attached as `settle.observer` when `changes` could not be read. */
|
|
87
|
+
export type ObserverChangesFailure = 'observer-unavailable' | 'doc-changed' | 'changes-error';
|
|
88
|
+
/** The settle result as the action callers carry it (`settleSafely`). */
|
|
89
|
+
export type SettleResultLike = Record<string, unknown>;
|
|
90
|
+
export declare function createObserverMirror(): ObserverMirror;
|
|
91
|
+
/**
|
|
92
|
+
* Parse one binding payload. The binding is a page-visible global, so the
|
|
93
|
+
* payload is untrusted: anything that is not valid JSON in one of the three
|
|
94
|
+
* observer message shapes yields `null`.
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseObserverMessage(payload: string): PageObserverMessage | null;
|
|
97
|
+
/**
|
|
98
|
+
* Fold one observer message into the mirror. `doc_start` and `nav` are
|
|
99
|
+
* assumed to come from the top document — the caller confirms that first
|
|
100
|
+
* with `TOP_DOCUMENT_PROBE_JS` / `isTopDocumentMessage`, because the install
|
|
101
|
+
* script also runs in same-process child frames. Summaries from a child
|
|
102
|
+
* frame (`roots.frame`) or from a document other than the mirrored one are
|
|
103
|
+
* ignored. A `nav` that does not change the URL (e.g. a state-only
|
|
104
|
+
* `replaceState`) is ignored so it does not emit a navigation event.
|
|
105
|
+
*/
|
|
106
|
+
export declare function applyObserverMessage(mirror: ObserverMirror, message: PageObserverMessage): ObserverMessageOutcome;
|
|
107
|
+
/**
|
|
108
|
+
* Expression evaluated in the tab's top document (default `Runtime.evaluate`
|
|
109
|
+
* context) to tell whether a `doc_start` / `nav` came from it: evaluates to
|
|
110
|
+
* `{ doc_id }` of the observer installed there, or `{ doc_id: null }`.
|
|
111
|
+
*/
|
|
112
|
+
export declare const TOP_DOCUMENT_PROBE_JS: string;
|
|
113
|
+
/** True when the top-document probe reports the message's `doc_id`. */
|
|
114
|
+
export declare function isTopDocumentMessage(probe: unknown, docId: string): boolean;
|
|
115
|
+
/** The part of an action mark (`buildObserverMarkJs`) the changes read needs. */
|
|
116
|
+
export interface ObserverMark {
|
|
117
|
+
mark: number;
|
|
118
|
+
doc_id: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate the result of the mark evaluation. `null` means there is no
|
|
122
|
+
* usable mark (observer missing, `ok: false`, or a malformed value); the
|
|
123
|
+
* caller then skips the changes read and reports the failure instead.
|
|
124
|
+
*/
|
|
125
|
+
export declare function readObserverMark(value: unknown): ObserverMark | null;
|
|
126
|
+
/**
|
|
127
|
+
* Fold the observer's "changes since mark" into a settle result (spec v4
|
|
128
|
+
* §5.4). With valid changes: attach `changes` (the `SettleChanges` shape),
|
|
129
|
+
* recompute `mutation_count` from the observer's records, and replace
|
|
130
|
+
* `url_changed` / `focus_moved` with the values baselined at the action
|
|
131
|
+
* mark (present only when they changed). Otherwise keep every settle field
|
|
132
|
+
* as `settleSafely` produced it and add `changes: null` plus
|
|
133
|
+
* `observer: <reason>`. A `null` settle (settle disabled) stays `null`.
|
|
134
|
+
* Returns a new object; never throws.
|
|
135
|
+
*/
|
|
136
|
+
export declare function mergeSettleChanges(settle: SettleResultLike | null, changes: unknown): SettleResultLike | null;
|
|
137
|
+
export {};
|