import type { Page } from "playwright"; import { createLogger, type Platform } from "@src/shared"; import type { EventBus } from "@src/events"; const log = createLogger("dom"); export interface DomDelta { added: number; removed: number; text_changes: number; attr_changes: number; regions: Record; // landmark/role region → nodes added new_videos: number; new_articles: number; new_links: number; modal_opened: boolean; url: string; at: number; } /** * Injected before any page script (§13). Buffers MutationObserver deltas and flushes * them to Node every 500 ms through an exposed binding. Never serializes the whole DOM. */ const INJECTED = String.raw` (() => { if (window.__srcDomObserverInstalled) return; window.__srcDomObserverInstalled = true; const state = { added: 0, removed: 0, text_changes: 0, attr_changes: 0, regions: {}, new_videos: 0, new_articles: 0, new_links: 0, modal_opened: false }; const regionOf = (node) => { let el = node.nodeType === 1 ? node : node.parentElement; let hops = 0; while (el && hops++ < 25) { const role = el.getAttribute && (el.getAttribute('role') || ''); const tag = el.tagName ? el.tagName.toLowerCase() : ''; if (role === 'main' || tag === 'main') return 'main'; if (role === 'feed' ) return 'feed'; if (role === 'dialog' || el.getAttribute && el.getAttribute('aria-modal') === 'true') return 'dialog'; if (role === 'navigation' || tag === 'nav') return 'navigation'; if (role === 'complementary' || tag === 'aside') return 'sidebar'; if (tag === 'header' || role === 'banner') return 'header'; if (tag === 'ytd-comments' || (el.id && /comment/i.test(el.id))) return 'comments'; el = el.parentElement; } return 'unknown'; }; const count = (node) => { if (node.nodeType !== 1) return; const el = node; const tag = el.tagName.toLowerCase(); if (tag === 'video' || el.querySelector && el.querySelector('video')) state.new_videos++; if (tag === 'article' || el.getAttribute('role') === 'article' || (el.querySelectorAll && el.querySelectorAll('article,[role=article]').length)) state.new_articles++; if (tag === 'a' ) state.new_links++; else if (el.querySelectorAll) state.new_links += Math.min(50, el.querySelectorAll('a[href]').length); if (el.getAttribute('role') === 'dialog' || el.getAttribute('aria-modal') === 'true') state.modal_opened = true; }; const obs = new MutationObserver((muts) => { for (const m of muts) { if (m.type === 'childList') { if (m.addedNodes.length) { state.added += m.addedNodes.length; const r = regionOf(m.target); state.regions[r] = (state.regions[r] || 0) + m.addedNodes.length; m.addedNodes.forEach(count); } state.removed += m.removedNodes.length; } else if (m.type === 'characterData') state.text_changes++; else if (m.type === 'attributes') state.attr_changes++; } }); const start = () => obs.observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['src', 'href', 'aria-expanded', 'aria-hidden', 'hidden', 'class'] }); if (document.documentElement) start(); else document.addEventListener('DOMContentLoaded', start); setInterval(() => { if (!state.added && !state.removed && !state.text_changes && !state.attr_changes) return; const snapshot = Object.assign({}, state, { url: location.href, at: Date.now() }); state.added = 0; state.removed = 0; state.text_changes = 0; state.attr_changes = 0; state.regions = {}; state.new_videos = 0; state.new_articles = 0; state.new_links = 0; state.modal_opened = false; if (window.__srcEmitDomDelta) window.__srcEmitDomDelta(snapshot); }, 500); })(); `; export class DomObserver { private deltas: DomDelta[] = []; private stepDeltas: DomDelta[] = []; private step = 0; private installed = false; constructor(private readonly opts: { page: Page; bus: EventBus; sessionId: string; platform: Platform }) {} async attach(): Promise { if (this.installed) return; const { page } = this.opts; await page.exposeBinding("__srcEmitDomDelta", (_src, delta: DomDelta) => this.onDelta(delta)); await page.addInitScript(INJECTED); // For the page already loaded, install immediately too. await page.evaluate(INJECTED).catch(() => {}); this.installed = true; log.debug("dom observer attached"); } beginStep(step: number): void { this.step = step; this.stepDeltas = []; } collectStep(): DomDelta[] { return [...this.stepDeltas]; } private onDelta(delta: DomDelta): void { this.deltas.push(delta); this.stepDeltas.push(delta); if (this.deltas.length > 5000) this.deltas.splice(0, 1000); if (delta.added + delta.removed < 3 && delta.text_changes < 3 && !delta.modal_opened) return; // noise this.opts.bus.emit({ event_type: "DOM_CHANGED", platform: this.opts.platform, session_id: this.opts.sessionId, step: this.step, payload: { added: delta.added, removed: delta.removed, text_changes: delta.text_changes, attr_changes: delta.attr_changes, regions: delta.regions, new_videos: delta.new_videos, new_articles: delta.new_articles, new_links: delta.new_links, modal_opened: delta.modal_opened, url: delta.url, }, provenance: [{ surface: "dom", confidence: 0.9 }], }); } }