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