@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
|
@@ -8,6 +8,52 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { DEEP_QUERY_JS } from './deep-query.js';
|
|
10
10
|
import { DOM_HELPERS_JS } from './dom-helpers.js';
|
|
11
|
+
import { OBSERVER_GLOBAL } from './observer.js';
|
|
12
|
+
/**
|
|
13
|
+
* In-page access to the page observer's element identity (spec v4 §5.6),
|
|
14
|
+
* injected verbatim into `buildSnapshotJs`, `buildFindJs` and `buildStateJs`.
|
|
15
|
+
* The observer global is looked up ONCE per evaluation and used only when it
|
|
16
|
+
* is present, not torn down, and exposes `refFor`:
|
|
17
|
+
* - `refOf(el)` returns the observer's `e<N>` ref for `el`, or `null`;
|
|
18
|
+
* - `withDocId(result)` adds `doc_id` to `result` and returns it;
|
|
19
|
+
* - `snapshotStore()` returns the observer when it keeps snapshot records
|
|
20
|
+
* (`recordSnapshot` / `getSnapshot`, used by `snapshot({ since })`), else
|
|
21
|
+
* `null`.
|
|
22
|
+
* Without the observer (injection failed, pre-observer document) all are
|
|
23
|
+
* no-ops, so every result is byte-identical to the pre-ref output.
|
|
24
|
+
*/
|
|
25
|
+
export const PAGE_OBSERVER_JS = `
|
|
26
|
+
const pageObserver = (() => {
|
|
27
|
+
try {
|
|
28
|
+
const g = window[${JSON.stringify(OBSERVER_GLOBAL)}];
|
|
29
|
+
if (g && !g.dead && typeof g.refFor === 'function') return g;
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
return null;
|
|
32
|
+
})();
|
|
33
|
+
function refOf(el) {
|
|
34
|
+
if (!pageObserver) return null;
|
|
35
|
+
try {
|
|
36
|
+
const ref = pageObserver.refFor(el);
|
|
37
|
+
return typeof ref === 'string' ? ref : null;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function withDocId(result) {
|
|
43
|
+
if (pageObserver && typeof pageObserver.doc_id === 'string') result.doc_id = pageObserver.doc_id;
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function snapshotStore() {
|
|
47
|
+
if (
|
|
48
|
+
pageObserver &&
|
|
49
|
+
typeof pageObserver.recordSnapshot === 'function' &&
|
|
50
|
+
typeof pageObserver.getSnapshot === 'function'
|
|
51
|
+
) {
|
|
52
|
+
return pageObserver;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
11
57
|
export function buildSnapshotJs(opts = {}) {
|
|
12
58
|
const optsJson = JSON.stringify({
|
|
13
59
|
withinSelector: typeof opts.within_selector === 'string' ? opts.within_selector : null,
|
|
@@ -16,23 +62,25 @@ export function buildSnapshotJs(opts = {}) {
|
|
|
16
62
|
maxInteractive: typeof opts.max_interactive === 'number' && opts.max_interactive > 0
|
|
17
63
|
? Math.min(opts.max_interactive, 500)
|
|
18
64
|
: 120,
|
|
65
|
+
since: typeof opts.since === 'string' ? opts.since : null,
|
|
19
66
|
});
|
|
20
67
|
return `
|
|
21
68
|
(() => {
|
|
22
69
|
${DEEP_QUERY_JS}
|
|
23
70
|
${DOM_HELPERS_JS}
|
|
71
|
+
${PAGE_OBSERVER_JS}
|
|
24
72
|
const opts = ${optsJson};
|
|
25
73
|
let root = document;
|
|
26
74
|
let notice = '';
|
|
27
75
|
if (opts.withinSelector) {
|
|
28
76
|
const sub = deepQueryFirst(opts.withinSelector);
|
|
29
77
|
if (!sub) {
|
|
30
|
-
return {
|
|
78
|
+
return withDocId({
|
|
31
79
|
title: document.title,
|
|
32
80
|
url: location.href,
|
|
33
81
|
interactive: [],
|
|
34
82
|
notice: 'within_selector did not match any element',
|
|
35
|
-
};
|
|
83
|
+
});
|
|
36
84
|
}
|
|
37
85
|
root = sub;
|
|
38
86
|
}
|
|
@@ -75,6 +123,8 @@ export function buildSnapshotJs(opts = {}) {
|
|
|
75
123
|
const role = el.getAttribute('role') || tag;
|
|
76
124
|
const selInfo = genSelectorInfo(el);
|
|
77
125
|
const entry = { tag, role, selector: selInfo.selector };
|
|
126
|
+
const ref = refOf(el);
|
|
127
|
+
if (ref) entry.ref = ref;
|
|
78
128
|
if (selInfo.ambiguous) entry.ambiguous = true;
|
|
79
129
|
const txt = shortText(el);
|
|
80
130
|
if (txt) entry.text = txt;
|
|
@@ -94,26 +144,105 @@ export function buildSnapshotJs(opts = {}) {
|
|
|
94
144
|
if (frame) entry.frame = frame;
|
|
95
145
|
interactive.push(entry);
|
|
96
146
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
url: location.href,
|
|
100
|
-
interactive: interactive.slice(0, opts.maxInteractive),
|
|
101
|
-
};
|
|
147
|
+
let headings = null;
|
|
148
|
+
let visibleText = '';
|
|
102
149
|
if (!opts.onlyInteractive) {
|
|
103
|
-
const
|
|
150
|
+
const found = [];
|
|
104
151
|
deepQueryAll('h1, h2, h3', root).forEach((h) => {
|
|
105
152
|
if (!isVisible(h)) return;
|
|
106
153
|
if (inExcludedLandmark(h)) return;
|
|
107
154
|
const t = shortText(h);
|
|
108
|
-
if (t)
|
|
155
|
+
if (t) found.push({ level: h.tagName, text: t });
|
|
109
156
|
});
|
|
110
|
-
|
|
157
|
+
headings = found.slice(0, 30);
|
|
111
158
|
const textRoot = (root === document) ? (document.body || document) : root;
|
|
112
|
-
|
|
159
|
+
visibleText = (textRoot && textRoot.innerText) ? textRoot.innerText.slice(0, 4000) : '';
|
|
160
|
+
}
|
|
161
|
+
let snapshotId = null;
|
|
162
|
+
if (withDocId({}).doc_id && typeof pageObserver.nextSnapshotId === 'function') {
|
|
163
|
+
const issued = pageObserver.nextSnapshotId();
|
|
164
|
+
if (typeof issued === 'string') snapshotId = issued;
|
|
165
|
+
}
|
|
166
|
+
function fullResult() {
|
|
167
|
+
const result = {
|
|
168
|
+
title: document.title,
|
|
169
|
+
url: location.href,
|
|
170
|
+
interactive: interactive.slice(0, opts.maxInteractive),
|
|
171
|
+
};
|
|
172
|
+
if (headings) result.headings = headings;
|
|
113
173
|
if (visibleText) result.text = visibleText;
|
|
174
|
+
if (notice) result.notice = notice;
|
|
175
|
+
withDocId(result);
|
|
176
|
+
if (snapshotId) result.snapshot_id = snapshotId;
|
|
177
|
+
return result;
|
|
114
178
|
}
|
|
115
|
-
|
|
116
|
-
|
|
179
|
+
// Record every snapshot (with or without since) so a later call can diff
|
|
180
|
+
// against it. Entries are keyed by ref; the hash is the entry serialised
|
|
181
|
+
// without its ref, in emission order. Capped at the first 500 entries.
|
|
182
|
+
const store = snapshotId ? snapshotStore() : null;
|
|
183
|
+
// A snapshot_id is \`<doc_id>:s<N>\`: a since whose prefix (before the LAST
|
|
184
|
+
// ':') is not this document's doc_id, or that has no ':', came from another
|
|
185
|
+
// document and is never looked up.
|
|
186
|
+
const sinceCut = opts.since === null ? -1 : opts.since.lastIndexOf(':');
|
|
187
|
+
const sinceSameDoc =
|
|
188
|
+
sinceCut >= 0 && withDocId({}).doc_id === opts.since.slice(0, sinceCut);
|
|
189
|
+
const previous = store && sinceSameDoc ? store.getSnapshot(opts.since) : null;
|
|
190
|
+
const current = new Map();
|
|
191
|
+
const headingsHash = headings ? JSON.stringify(headings) : null;
|
|
192
|
+
const textHash = opts.onlyInteractive ? null : visibleText;
|
|
193
|
+
if (store) {
|
|
194
|
+
for (const entry of interactive) {
|
|
195
|
+
if (current.size >= 500) break;
|
|
196
|
+
if (typeof entry.ref !== 'string') continue;
|
|
197
|
+
const copy = {};
|
|
198
|
+
for (const key of Object.keys(entry)) if (key !== 'ref') copy[key] = entry[key];
|
|
199
|
+
current.set(entry.ref, { hash: JSON.stringify(copy), entry });
|
|
200
|
+
}
|
|
201
|
+
store.recordSnapshot(snapshotId, { entries: current, headings_hash: headingsHash, text_hash: textHash });
|
|
202
|
+
}
|
|
203
|
+
if (opts.since === null) return fullResult();
|
|
204
|
+
if (!store) {
|
|
205
|
+
const result = fullResult();
|
|
206
|
+
result.since = opts.since;
|
|
207
|
+
result.since_status = 'observer-unavailable';
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
if (!previous || !previous.entries || typeof previous.entries.get !== 'function') {
|
|
211
|
+
const result = fullResult();
|
|
212
|
+
result.since = opts.since;
|
|
213
|
+
result.since_status = sinceSameDoc ? 'evicted' : 'doc-changed';
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
const added = [];
|
|
217
|
+
const changed = [];
|
|
218
|
+
const removed = [];
|
|
219
|
+
current.forEach((item, ref) => {
|
|
220
|
+
const old = previous.entries.get(ref);
|
|
221
|
+
if (!old) added.push(item.entry);
|
|
222
|
+
else if (old.hash !== item.hash) changed.push(item.entry);
|
|
223
|
+
});
|
|
224
|
+
previous.entries.forEach((item, ref) => {
|
|
225
|
+
if (current.has(ref) || removed.length >= 500) return;
|
|
226
|
+
const gone = { ref, tag: item.entry.tag };
|
|
227
|
+
if (item.entry.text) gone.text = item.entry.text;
|
|
228
|
+
removed.push(gone);
|
|
229
|
+
});
|
|
230
|
+
// max_interactive applies after diffing (added first, then changed), so
|
|
231
|
+
// a window shift never masquerades as change.
|
|
232
|
+
const listed = added.concat(changed);
|
|
233
|
+
const kept = listed.slice(0, opts.maxInteractive);
|
|
234
|
+
const diff = withDocId({ title: document.title, url: location.href });
|
|
235
|
+
diff.snapshot_id = snapshotId;
|
|
236
|
+
diff.since = opts.since;
|
|
237
|
+
diff.added = kept.slice(0, Math.min(added.length, kept.length));
|
|
238
|
+
diff.removed = removed;
|
|
239
|
+
diff.changed = kept.slice(diff.added.length);
|
|
240
|
+
if (listed.length > kept.length) diff.overflow = listed.length - kept.length;
|
|
241
|
+
if (!opts.onlyInteractive) {
|
|
242
|
+
if (headingsHash !== previous.headings_hash) diff.headings = headings;
|
|
243
|
+
if (textHash !== previous.text_hash) diff.text = visibleText;
|
|
244
|
+
}
|
|
245
|
+
return diff;
|
|
117
246
|
})()
|
|
118
247
|
`;
|
|
119
248
|
}
|
|
@@ -129,6 +258,7 @@ export function buildFindJs(opts) {
|
|
|
129
258
|
(() => {
|
|
130
259
|
${DEEP_QUERY_JS}
|
|
131
260
|
${DOM_HELPERS_JS}
|
|
261
|
+
${PAGE_OBSERVER_JS}
|
|
132
262
|
const opts = ${optsJson};
|
|
133
263
|
const needle = opts.text.toLowerCase();
|
|
134
264
|
const needleTokens = needle.split(/[^a-z0-9]+/).filter(Boolean);
|
|
@@ -189,6 +319,8 @@ export function buildFindJs(opts) {
|
|
|
189
319
|
}
|
|
190
320
|
function toNearMiss(el) {
|
|
191
321
|
const entry = { text: snippetOf(el), selector: genSelector(el) };
|
|
322
|
+
const ref = refOf(el);
|
|
323
|
+
if (ref) entry.ref = ref;
|
|
192
324
|
const roleAttr = el.getAttribute('role');
|
|
193
325
|
if (roleAttr) entry.role = roleAttr;
|
|
194
326
|
return entry;
|
|
@@ -221,44 +353,46 @@ export function buildFindJs(opts) {
|
|
|
221
353
|
' but none with role "' + opts.role + '"' +
|
|
222
354
|
(closest ? ' — closest: ' + describeForError(closest) + ' "' + snippetOf(closest) + '"' : '');
|
|
223
355
|
result.near_misses = ranked.map(toNearMiss);
|
|
224
|
-
return result;
|
|
356
|
+
return withDocId(result);
|
|
225
357
|
}
|
|
226
358
|
const ranked = rankCandidates(broadAll);
|
|
227
359
|
if (ranked.length > 0) result.near_misses = ranked.map(toNearMiss);
|
|
228
|
-
return result;
|
|
360
|
+
return withDocId(result);
|
|
229
361
|
}
|
|
230
362
|
const ranked = rankCandidates(all);
|
|
231
363
|
if (ranked.length > 0) result.near_misses = ranked.map(toNearMiss);
|
|
232
|
-
return result;
|
|
364
|
+
return withDocId(result);
|
|
233
365
|
}
|
|
234
366
|
if (matches.length > 1) {
|
|
235
|
-
return {
|
|
367
|
+
return withDocId({
|
|
236
368
|
matched: false,
|
|
237
369
|
reason: 'multiple-matches',
|
|
238
|
-
candidates: matches.slice(0, opts.candidateLimit).map((el) =>
|
|
239
|
-
selector: genSelector(el)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
370
|
+
candidates: matches.slice(0, opts.candidateLimit).map((el) => {
|
|
371
|
+
const candidate = { selector: genSelector(el) };
|
|
372
|
+
const ref = refOf(el);
|
|
373
|
+
if (ref) candidate.ref = ref;
|
|
374
|
+
candidate.text = shortText(el);
|
|
375
|
+
candidate.tag = el.tagName.toLowerCase();
|
|
376
|
+
return candidate;
|
|
377
|
+
}),
|
|
378
|
+
});
|
|
244
379
|
}
|
|
245
380
|
const el = matches[0];
|
|
246
381
|
const center = viewportCenterOf(el);
|
|
247
382
|
const selInfo = genSelectorInfo(el);
|
|
248
|
-
const result = {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
tag: el.tagName.toLowerCase(),
|
|
256
|
-
text: shortText(el),
|
|
383
|
+
const result = { matched: true, selector: selInfo.selector };
|
|
384
|
+
const ref = refOf(el);
|
|
385
|
+
if (ref) result.ref = ref;
|
|
386
|
+
result.coords = {
|
|
387
|
+
x: Math.round(center.x),
|
|
388
|
+
y: Math.round(center.y),
|
|
257
389
|
};
|
|
390
|
+
result.tag = el.tagName.toLowerCase();
|
|
391
|
+
result.text = shortText(el);
|
|
258
392
|
if (selInfo.ambiguous) result.ambiguous = true;
|
|
259
393
|
const frame = frameSelectorFor(el);
|
|
260
394
|
if (frame) result.frame = frame;
|
|
261
|
-
return result;
|
|
395
|
+
return withDocId(result);
|
|
262
396
|
})()
|
|
263
397
|
`;
|
|
264
398
|
}
|
|
@@ -323,6 +457,26 @@ export function buildFocusJs(opts) {
|
|
|
323
457
|
return true;
|
|
324
458
|
})()`;
|
|
325
459
|
}
|
|
460
|
+
export function buildWaitForSelectorJs(selector, condition) {
|
|
461
|
+
const optsJson = JSON.stringify({ selector, condition });
|
|
462
|
+
return `
|
|
463
|
+
(() => {
|
|
464
|
+
${DEEP_QUERY_JS}
|
|
465
|
+
const opts = ${optsJson};
|
|
466
|
+
document.querySelector(opts.selector);
|
|
467
|
+
const el = deepQueryFirst(opts.selector);
|
|
468
|
+
if (opts.condition === 'attached') return Boolean(el);
|
|
469
|
+
if (opts.condition === 'detached') return !el;
|
|
470
|
+
const wantHidden = opts.condition === 'hidden';
|
|
471
|
+
if (!el) return wantHidden;
|
|
472
|
+
const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
|
|
473
|
+
const s = view.getComputedStyle(el);
|
|
474
|
+
if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return wantHidden;
|
|
475
|
+
const r = el.getBoundingClientRect();
|
|
476
|
+
const shown = r.width > 0 && r.height > 0;
|
|
477
|
+
return wantHidden ? !shown : shown;
|
|
478
|
+
})()`;
|
|
479
|
+
}
|
|
326
480
|
export function buildElementRectJs(opts) {
|
|
327
481
|
const optsJson = JSON.stringify({ selector: opts.selector });
|
|
328
482
|
return `
|
|
@@ -603,12 +757,16 @@ export function buildExpectProbeJs(opts) {
|
|
|
603
757
|
* `focused` beyond `<body>`, no `dialogs` when none are open, and no
|
|
604
758
|
* `scroll` when the page is at its default (0,0) position — token-lean by
|
|
605
759
|
* construction, matching the omit-falsy convention the other builders use.
|
|
760
|
+
*
|
|
761
|
+
* With the page observer installed (see `PAGE_OBSERVER_JS`), `focused`
|
|
762
|
+
* carries `ref` right after `selector` and the result carries `doc_id`.
|
|
606
763
|
*/
|
|
607
764
|
export function buildStateJs() {
|
|
608
765
|
return `
|
|
609
766
|
(() => {
|
|
610
767
|
${DEEP_QUERY_JS}
|
|
611
768
|
${DOM_HELPERS_JS}
|
|
769
|
+
${PAGE_OBSERVER_JS}
|
|
612
770
|
function deepActiveElement() {
|
|
613
771
|
let el = document.activeElement;
|
|
614
772
|
let guard = 0;
|
|
@@ -669,7 +827,10 @@ export function buildStateJs() {
|
|
|
669
827
|
const active = deepActiveElement();
|
|
670
828
|
if (active && active !== document.body && active.tagName) {
|
|
671
829
|
const selInfo = genSelectorInfo(active);
|
|
672
|
-
const focused = { selector: selInfo.selector
|
|
830
|
+
const focused = { selector: selInfo.selector };
|
|
831
|
+
const ref = refOf(active);
|
|
832
|
+
if (ref) focused.ref = ref;
|
|
833
|
+
focused.tag = active.tagName.toLowerCase();
|
|
673
834
|
if (selInfo.ambiguous) focused.ambiguous = true;
|
|
674
835
|
result.focused = focused;
|
|
675
836
|
}
|
|
@@ -689,7 +850,7 @@ export function buildStateJs() {
|
|
|
689
850
|
const scrollY = Math.round(window.scrollY);
|
|
690
851
|
if (scrollX !== 0 || scrollY !== 0) result.scroll = { x: scrollX, y: scrollY };
|
|
691
852
|
|
|
692
|
-
return result;
|
|
853
|
+
return withDocId(result);
|
|
693
854
|
})()
|
|
694
855
|
`;
|
|
695
856
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builders.js","sourceRoot":"","sources":["../../src/inpage/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"builders.js","sourceRoot":"","sources":["../../src/inpage/builders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page observer: a persistent, coalescing MutationObserver that runs in
|
|
3
|
+
* the page's MAIN world (spec v4 §5.1–§5.3). The host installs it once per
|
|
4
|
+
* tab with `Runtime.addBinding` + `Page.addScriptToEvaluateOnNewDocument`
|
|
5
|
+
* (`runImmediately: true`), so Chrome re-runs it in every new document and
|
|
6
|
+
* in every same-process frame before page scripts. It pushes one summary
|
|
7
|
+
* per flush through the binding and keeps a bounded ring of summaries so an
|
|
8
|
+
* action window (mark → changes) can be read back exactly.
|
|
9
|
+
*
|
|
10
|
+
* Lab findings this source honours (`packages/plan/lab/2026-11-06-live-page-model/`):
|
|
11
|
+
* - flushes are scheduled with `setTimeout`, never `requestAnimationFrame`
|
|
12
|
+
* (rAF never fires in a hidden tab);
|
|
13
|
+
* - the instance is versioned: an older instance is torn down and replaced,
|
|
14
|
+
* an equal or newer one is left alone — never "if present, return";
|
|
15
|
+
* - one observer per root (an observer on `document` alone misses shadow
|
|
16
|
+
* roots), with `Element.prototype.attachShadow` patched to catch every
|
|
17
|
+
* root created later, open or closed.
|
|
18
|
+
*
|
|
19
|
+
* Verbatim sibling: `packages/server/src/cdp/inpage/observer.ts` (the server
|
|
20
|
+
* publishes standalone and cannot import this package). Everything below this
|
|
21
|
+
* header must stay byte-identical — `cdp/drift.test.ts` enforces it and
|
|
22
|
+
* `cdp/builders-parity.test.ts` checks the built output.
|
|
23
|
+
*/
|
|
24
|
+
/** Bump whenever the in-page protocol (global shape, messages, methods) changes. */
|
|
25
|
+
export declare const OBSERVER_VERSION = 2;
|
|
26
|
+
/** Name of the `Runtime.addBinding` function the observer pushes messages through. */
|
|
27
|
+
export declare const OBSERVER_BINDING = "__blPageObserverPush";
|
|
28
|
+
/** Well-known global that holds the installed observer instance. */
|
|
29
|
+
export declare const OBSERVER_GLOBAL = "__blPageObserver";
|
|
30
|
+
export interface ObserverInstallOpts {
|
|
31
|
+
/** Binding name to push through; defaults to `OBSERVER_BINDING`. */
|
|
32
|
+
binding?: string;
|
|
33
|
+
/** Instance version; defaults to `OBSERVER_VERSION`. */
|
|
34
|
+
version?: number;
|
|
35
|
+
/** Max target descriptors per summary (default 20, clamped to 1..100). */
|
|
36
|
+
descriptor_cap?: number;
|
|
37
|
+
/** Summaries kept for mark/changes read-back (default 64, clamped to 1..1024). */
|
|
38
|
+
ring_size?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the observer install source. Valid both as a
|
|
42
|
+
* `Page.addScriptToEvaluateOnNewDocument` source and as a one-off
|
|
43
|
+
* `Runtime.evaluate` expression, which evaluates to
|
|
44
|
+
* `{ ok: true, installed, version, doc_id, coverage }`.
|
|
45
|
+
*
|
|
46
|
+
* The instance also owns element identity (spec v4 §5.6): `refFor(el)` issues
|
|
47
|
+
* `e<N>` (monotonic per document instance, from `e1`) the first time an
|
|
48
|
+
* element is seen and reuses it while the node lives; `resolveRef(ref)`
|
|
49
|
+
* returns the element while it is connected, else `null`. Refs are only valid
|
|
50
|
+
* for the `doc_id` that issued them. Neither method throws. `nextSnapshotId()`
|
|
51
|
+
* issues `<doc_id>:s<N>` (N monotonic per document instance, from 1) for each
|
|
52
|
+
* `snapshot` result and returns `null` once the instance is torn down; the
|
|
53
|
+
* `doc_id` prefix lets a stale id from another document be rejected.
|
|
54
|
+
* `recordSnapshot(id, record)` keeps the last 4 snapshot records per document
|
|
55
|
+
* instance (oldest evicted first) for `snapshot({ since })`; `getSnapshot(id)`
|
|
56
|
+
* returns the record or `null` when unknown or evicted. The record is opaque
|
|
57
|
+
* here. Both are inert after teardown, which clears the store.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildObserverInstallJs(opts?: ObserverInstallOpts): string;
|
|
60
|
+
/**
|
|
61
|
+
* Build the action-mark expression. Forces a flush (including records still
|
|
62
|
+
* queued in the observers) so the mark is exact, then baselines URL and focus.
|
|
63
|
+
* Evaluates to `{ ok: true, doc_id, mark, url, has_focus_baseline: true }` or
|
|
64
|
+
* `{ ok: false, reason: 'observer-unavailable' }`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildObserverMarkJs(): string;
|
|
67
|
+
/**
|
|
68
|
+
* Build the "changes since mark" expression. Forces a flush, then aggregates
|
|
69
|
+
* the ring summaries with `seq > mark`. `docId`, when given, must match the
|
|
70
|
+
* current document. Evaluates to the aggregated changes (see
|
|
71
|
+
* `SettleChanges` in `@browser-link/shared`) plus `ok`, `doc_id`, `since`,
|
|
72
|
+
* and `url_changed` / `focus_moved` only when they differ from the mark's
|
|
73
|
+
* baseline; or `{ ok: false, reason: 'observer-unavailable' | 'doc-changed' }`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildObserverChangesJs(mark: number, docId?: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Build the ref-resolve expression, for tests and debugging. Evaluates to
|
|
78
|
+
* `{ ok: true, found: true, tag }` when `ref` names an element of the current
|
|
79
|
+
* document that is still connected, `{ ok: true, found: false }` when it is
|
|
80
|
+
* unknown or stale, or `{ ok: false, reason: 'observer-unavailable' }`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildObserverRefResolveJs(ref: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* Build the teardown expression: disconnects every observer, restores the
|
|
85
|
+
* `attachShadow` and `history` patches when they are still the observer's,
|
|
86
|
+
* and removes the global. Idempotent; evaluates to `{ ok: true, removed }`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildObserverTeardownJs(): string;
|