diff --git a/apps/extension/src/browser-driver/__tests__/frame-graph.test.ts b/apps/extension/src/browser-driver/__tests__/frame-graph.test.ts index 3838d2b..b9d4b40 100644 --- a/apps/extension/src/browser-driver/__tests__/frame-graph.test.ts +++ b/apps/extension/src/browser-driver/__tests__/frame-graph.test.ts @@ -2,24 +2,6 @@ import { describe, expect, it } from "vitest"; import { buildFrameGraph, type CdpFrameTreeNode, type CdpFrameTreeSource } from "../frame-graph"; describe("buildFrameGraph", () => { - it("preserves the owning target's loader identity", () => { - const graph = buildFrameGraph([ - { - target: { tabId: 4 }, - tree: { - frame: { id: "root", loaderId: "root-loader" }, - childFrames: [{ frame: { id: "child", loaderId: "stale-placeholder" } }], - }, - }, - { - target: { tabId: 4, sessionId: "child-session" }, - tree: { frame: { id: "child", loaderId: "child-loader" } }, - }, - ]); - expect(graph?.frames.find((frame) => frame.frameId === "root")?.loaderId).toBe("root-loader"); - expect(graph?.frames.find((frame) => frame.frameId === "child")?.loaderId).toBe("child-loader"); - }); - it("keeps sibling frames distinct and routes nested OOPIFs to their child sessions", () => { const graph = buildFrameGraph([ { diff --git a/apps/extension/src/browser-driver/frame-graph.ts b/apps/extension/src/browser-driver/frame-graph.ts index 2367a5f..1dc2ead 100644 --- a/apps/extension/src/browser-driver/frame-graph.ts +++ b/apps/extension/src/browser-driver/frame-graph.ts @@ -8,7 +8,6 @@ export interface CdpFrame { parentFrameId?: string; ownerBackendNodeId?: number; url?: string; - loaderId?: string; target: CdpTarget; } @@ -22,7 +21,6 @@ export interface CdpFrameTreeNode { id: string; parentId?: string; url?: string; - loaderId?: string; }; childFrames?: CdpFrameTreeNode[]; } @@ -50,7 +48,6 @@ function mergeFrame( frameId: node.frame.id, ...(parentFrameId ? { parentFrameId } : {}), ...(url ? { url } : {}), - ...(node.frame.loaderId ? { loaderId: node.frame.loaderId } : {}), target, }); } diff --git a/apps/extension/src/tools/vom/__tests__/capture-coordinator.test.ts b/apps/extension/src/tools/vom/__tests__/capture-coordinator.test.ts index ad972b5..8b7b8a0 100644 --- a/apps/extension/src/tools/vom/__tests__/capture-coordinator.test.ts +++ b/apps/extension/src/tools/vom/__tests__/capture-coordinator.test.ts @@ -11,10 +11,7 @@ import { REQUESTED_STYLES, type SnapshotReply } from "../snapshot"; function fixture( options: { frames?: CdpFrame[]; - after?: Record< - string, - { loader?: string; element?: number; missing?: boolean; unreadable?: boolean } - >; + after?: Record; fail?: string; missingIdentity?: boolean; omitDocument?: string; @@ -23,26 +20,23 @@ function fixture( } = {}, ) { const frames = options.frames ?? [ - { frameId: "main", loaderId: "main-loader", target: { tabId: 4 } }, + { frameId: "main", target: { tabId: 4 } }, { frameId: "same", parentFrameId: "main", ownerBackendNodeId: 3, - loaderId: "same-loader", target: { tabId: 4 }, }, { frameId: "nested", parentFrameId: "same", ownerBackendNodeId: 13, - loaderId: "nested-loader", target: { tabId: 4 }, }, { frameId: "remote", parentFrameId: "main", ownerBackendNodeId: 4, - loaderId: "remote-loader", target: { tabId: 4, sessionId: "remote" }, }, ]; @@ -50,7 +44,6 @@ function fixture( frames.map((frame, i) => [frame.frameId, frame.target.sessionId ? 1 : i * 10 + 1]), ); const contexts = new Map(frames.map((frame, i) => [i + 1, frame.frameId])); - const reads = new Map(); const logs: Array<{ target: CdpTarget; method: string; params: Record }> = []; let snapshots = 0; const targetCount = new Set(frames.map((frame) => cdpTargetKey(frame.target))).size; @@ -65,47 +58,23 @@ function fixture( result = { executionContextId: frames.findIndex((frame) => frame.frameId === args.frameId) + 1, }; - if (method === "Runtime.evaluate") - result = { result: { objectId: contexts.get(Number(args.contextId)) } }; - if (method === "DOM.describeNode") { - const id = String(args.objectId); - const count = reads.get(id) ?? 0; - reads.set(id, count + 1); + if (method === "Runtime.evaluate" && args.contextId) { + const id = contexts.get(Number(args.contextId))!; + const change = options.after?.[id]; result = - options.missingIdentity || (count > 0 && options.after?.[id]?.unreadable) + options.missingIdentity || change?.unreadable ? {} : { - node: { - backendNodeId: - count > 0 - ? (options.after?.[id]?.element ?? elements.get(id)) - : elements.get(id), + result: { + deepSerializedValue: change?.missing + ? { type: "null" } + : { + type: "node", + value: { backendNodeId: change?.element ?? elements.get(id) }, + }, }, }; } - if (method === "Page.getFrameTree") { - const own = frames.filter( - (frame) => - cdpTargetKey(frame.target) === cdpTargetKey(target) && - !options.after?.[frame.frameId]?.missing, - ); - const trees = new Map( - own.map((frame) => [ - frame.frameId, - { - frame: { - id: frame.frameId, - loaderId: options.after?.[frame.frameId]?.loader ?? frame.loaderId, - }, - childFrames: [] as unknown[], - }, - ]), - ); - for (const frame of own) - if (frame.parentFrameId && trees.has(frame.parentFrameId)) - trees.get(frame.parentFrameId)!.childFrames.push(trees.get(frame.frameId)); - result = { frameTree: trees.get(own[0]?.frameId) }; - } if (method === "Page.getLayoutMetrics") result = { visualViewport: { clientWidth: 1000 }, @@ -203,7 +172,11 @@ function fixture( ); const cdp: CdpRunner = { getAttachmentId: () => - options.attachmentChanged && snapshots === targetCount ? "new-attachment" : "attachment", + options.missingIdentity + ? undefined + : options.attachmentChanged && snapshots === targetCount + ? "new-attachment" + : "attachment", getFrameGraph: async () => ({ rootFrameId: frames[0].frameId, frames }), send: (tabId, method, params) => (send as NonNullable)({ tabId }, method, params), @@ -249,8 +222,8 @@ describe("captureObservationFacts", () => { expect(logs.filter((call) => call.method === "Accessibility.enable")).toHaveLength(2); expect(logs.filter((call) => call.method === "Accessibility.getFullAXTree")).toHaveLength(4); expect(facts.documents.every((doc) => doc.identity)).toBe(true); - expect(logs.filter((call) => call.method === "DOM.describeNode")).toHaveLength(8); - expect(logs.filter((call) => call.method === "Runtime.releaseObject")).toHaveLength(8); + expect(logs.filter((call) => call.method === "DOM.describeNode")).toHaveLength(0); + expect(logs.filter((call) => call.method === "Page.createIsolatedWorld")).toHaveLength(4); const main = facts.documents.find((doc) => doc.frame.frameId === "main")!; const remote = facts.documents.find((doc) => doc.frame.frameId === "remote")!; expect(main.index.nodes.get(2)).not.toBe(remote.index.nodes.get(2)); @@ -262,7 +235,6 @@ describe("captureObservationFacts", () => { }); it.each([ - { loader: "new-loader" }, { element: 99 }, { missing: true }, ])("isolates changed child identity %j and dependent descendants", async (change) => { @@ -299,31 +271,20 @@ describe("captureObservationFacts", () => { } }); - it("keeps newly discovered snapshot documents unverified without reading a synthetic before identity", async () => { + it("validates newly discovered snapshot documents without a pre-snapshot identity read", async () => { const { cdp, logs } = fixture(); - const originalGraph = await cdp.getFrameGraph!(4); - cdp.getFrameGraph = async () => ({ - ...originalGraph, - frames: originalGraph.frames.slice(0, 1), - }); + const graph = await cdp.getFrameGraph!(4); + cdp.getFrameGraph = async () => ({ ...graph, frames: graph.frames.slice(0, 1) }); const facts = await captureObservationFacts(cdp, 4); expect(facts.documents.map((doc) => doc.frame.frameId)).toEqual(["main", "same", "nested"]); - expect(facts.documents[0].identity).toBeDefined(); - for (const doc of facts.documents.slice(1)) { - expect(doc.identity).toBeUndefined(); - expect(facts.issues).toContainEqual( - expect.objectContaining({ frameId: doc.frame.frameId, reason: "identity-unverified" }), - ); - } - expect( - logs.filter((call) => call.method === "DOM.describeNode").map((call) => call.params.objectId), - ).toEqual(["main", "main"]); + expect(facts.documents.every((doc) => doc.identity)).toBe(true); + expect(logs.filter((call) => call.method === "Page.createIsolatedWorld")).toHaveLength(3); + expect(logs.findIndex((call) => call.method === "Page.createIsolatedWorld")).toBeGreaterThan( + logs.findIndex((call) => call.method === "Accessibility.getFullAXTree"), + ); }); - it.each([ - "frame-id", - "snapshot-element", - ])("rejects root replacement evidenced by %s even if the later graph is stale", async (mode) => { + it("rejects a snapshot root that no longer matches the current frame root", async () => { const { cdp } = fixture(); const original = cdp.sendToTarget!; cdp.sendToTarget = async (target, method, params) => { @@ -332,8 +293,7 @@ describe("captureObservationFacts", () => { const snapshot = result as { documents: { frameId: string; nodes: { backendNodeId: number[] } }[]; }; - if (mode === "frame-id") snapshot.documents[0].frameId = "replacement"; - else snapshot.documents[0].nodes.backendNodeId[2] = 999; + snapshot.documents[0].nodes.backendNodeId[2] = 999; } return result as never; }; @@ -347,17 +307,20 @@ describe("captureObservationFacts", () => { const original = cdp.sendToTarget!; let identityCleanup = 0; cdp.sendToTarget = async (target, method, params) => { - if (method === "Page.getFrameTree") { + if (method === "Page.createIsolatedWorld") { expect(logs.filter((call) => call.method === "Accessibility.getFullAXTree")).toHaveLength( 4, ); } const result = await original(target, method, params); if ( - method === "Runtime.releaseObject" && - (params as { objectId?: string })?.objectId === "remote" + method === "Runtime.releaseObjectGroup" && + String((params as { objectGroup?: string })?.objectGroup).startsWith( + "bsk-document-identity-", + ) && + target.sessionId === "remote" ) { - if (++identityCleanup === 2) controller.abort(); + if (++identityCleanup === 1) controller.abort(); } return result as never; }; @@ -365,15 +328,14 @@ describe("captureObservationFacts", () => { await expect(captureObservationFacts(cdp, 4, controller.signal)).rejects.toMatchObject({ name: "AbortError", }); - expect(identityCleanup).toBe(2); + expect(identityCleanup).toBe(1); }); it.each([ 1, 10, 100, - ])("bounds identity reads for %i same-target documents across both phases", async (count) => { + ])("bounds identity reads for %i same-target documents after snapshot and AX collection", async (count) => { const frames: CdpFrame[] = Array.from({ length: count }, (_, i) => ({ frameId: `frame-${i}`, - loaderId: `loader-${i}`, target: { tabId: 4 }, ...(i ? { parentFrameId: "frame-0", ownerBackendNodeId: 90000 + i } : {}), })); @@ -389,16 +351,20 @@ describe("captureObservationFacts", () => { } if (method === "DOMSnapshot.captureSnapshot") { expect(active).toBe(0); - expect(released).toBe(count); + expect(released).toBe(0); } - if (method === "Page.getFrameTree") { - expect(active).toBe(0); + if (method === "Page.createIsolatedWorld") { expect(logs.filter((call) => call.method === "Accessibility.getFullAXTree")).toHaveLength( count, ); } const result = await original(target, method, params); - if (method === "Runtime.releaseObject") { + if ( + method === "Runtime.releaseObjectGroup" && + String((params as { objectGroup?: string })?.objectGroup).startsWith( + "bsk-document-identity-", + ) + ) { active--; released++; } @@ -409,25 +375,20 @@ describe("captureObservationFacts", () => { expect(facts.documents.every((doc) => doc.identity)).toBe(true); expect(peak).toBe(Math.min(count, 4)); expect(active).toBe(0); - expect(released).toBe(count * 2); + expect(released).toBe(count); for (const method of [ "Page.createIsolatedWorld", "Runtime.evaluate", - "DOM.describeNode", - "Runtime.releaseObject", + "Runtime.releaseObjectGroup", ]) - expect(logs.filter((call) => call.method === method)).toHaveLength(count * 2); + expect(logs.filter((call) => call.method === method)).toHaveLength(count); expect(logs.filter((call) => call.method === "DOMSnapshot.captureSnapshot")).toHaveLength(1); - expect(logs.filter((call) => call.method === "Page.getFrameTree")).toHaveLength(1); + expect(logs.filter((call) => call.method === "Page.getFrameTree")).toHaveLength(0); }); - it.each([ - "before", - "after", - ])("joins identity cleanup on %s cancellation without starting queued frames", async (phase) => { + it("joins identity cleanup on cancellation without starting queued frames", async () => { const frames: CdpFrame[] = Array.from({ length: 5 }, (_, i) => ({ frameId: `frame-${i}`, - loaderId: `loader-${i}`, target: { tabId: 4 }, ...(i ? { parentFrameId: "frame-0", ownerBackendNodeId: 90000 + i } : {}), })); @@ -443,11 +404,14 @@ describe("captureObservationFacts", () => { ready = resolve; }); let held = 0; - let after = false; cdp.sendToTarget = async (target, method, params) => { - if (method === "Page.getFrameTree") after = true; const result = await original(target, method, params); - if (method === "Runtime.releaseObject" && after === (phase === "after")) { + if ( + method === "Runtime.releaseObjectGroup" && + String((params as { objectGroup?: string })?.objectGroup).startsWith( + "bsk-document-identity-", + ) + ) { if (++held === 4) ready(); await gate; } @@ -466,9 +430,7 @@ describe("captureObservationFacts", () => { release(); await rejected; expect(held).toBe(4); - expect(logs.filter((call) => call.method === "Page.createIsolatedWorld")).toHaveLength( - phase === "after" ? 9 : 4, - ); + expect(logs.filter((call) => call.method === "Page.createIsolatedWorld")).toHaveLength(4); }); it.each([ @@ -1206,7 +1168,7 @@ describe("sibling frame measurement scheduling", () => { return original(tabId, method, params); }; const captured = await captureObservationFacts(fixture.cdp, 4); - expect(captured.issues).toEqual([]); + expect(captured.issues.filter((issue) => issue.stage === "geometry")).toEqual([]); expect(captured.documents.map((doc) => doc.frame.frameId)).toEqual( Array.from({ length: 8 }, (_, i) => `frame-${i}`), ); diff --git a/apps/extension/src/tools/vom/capture-coordinator.ts b/apps/extension/src/tools/vom/capture-coordinator.ts index c859c3f..c5ecf53 100644 --- a/apps/extension/src/tools/vom/capture-coordinator.ts +++ b/apps/extension/src/tools/vom/capture-coordinator.ts @@ -1,19 +1,17 @@ import { - buildFrameGraph, type CdpFrame, type CdpFrameGraph, - type CdpFrameTreeNode, type CdpTarget, cdpTargetKey, } from "@/browser-driver/frame-graph"; import { cssViewport, GeometryContext } from "../geometry/frame-context"; -import { type CdpRunner, cdpRunnerForTarget, sendToCdpTarget } from "../shared"; +import { type CdpRunner, cdpRunnerForTarget } from "../shared"; import { collectOverlayExcludedBackendIds } from "./capture"; import { isAbortError as isCaptureAbort, throwIfAborted as throwCaptureAborted, } from "./capture-abort"; -import { readDocumentIdentity, sameDocument } from "./document-identity"; +import { verifyDocumentIdentity } from "./document-identity"; import type { CapturedSceneInput, DocumentIdentity } from "./facts"; import { buildDocumentIndex, type CaptureIssue, type ObservationFacts } from "./facts"; import { enrichFormControlStates } from "./form-capture"; @@ -29,7 +27,7 @@ import { describeSnapshotFrames, REQUESTED_STYLES, type SnapshotReply } from "./ interface TargetBatch { target: CdpTarget; frames: CdpFrame[]; - before: Map; + snapshotAttachmentId?: string; documents: NormalizedFrameDocument[]; ax: FrameAxBatch[]; fallbackExcluded: Set; @@ -92,7 +90,6 @@ export async function captureObservationFacts( group = { target: frame.target, frames: [], - before: new Map(), documents: [], ax: [], fallbackExcluded: new Set(), @@ -109,7 +106,6 @@ export async function captureObservationFacts( groups.set(cdpTargetKey({ tabId }), { target: { tabId }, frames: [], - before: new Map(), documents: [], ax: [], fallbackExcluded: new Set(), @@ -117,16 +113,6 @@ export async function captureObservationFacts( const batches = [...groups.values()]; const unavailable = (batch: TargetBatch, stage: CaptureIssue["stage"], frameId?: string) => issues.push({ target: batch.target, frameId, stage, reason: "capture-unavailable" }); - // All before reads finish before any target starts collecting. The bound is - // shared across frames, including frames in the same target. - await collectTasks( - batches.flatMap((batch) => batch.frames.map((frame) => ({ batch, frame }))), - async ({ batch, frame }) => { - const identity = await readDocumentIdentity(cdp, frame, signal); - if (identity) batch.before.set(frame.frameId, identity); - }, - signal, - ); let firstFailure: unknown; const axReady = new Set>(); await collectTasks( @@ -137,6 +123,7 @@ export async function captureObservationFacts( throwCaptureAborted(signal); await scoped.send(tabId, "DOMSnapshot.enable", {}); throwCaptureAborted(signal); + batch.snapshotAttachmentId = cdp.getAttachmentId?.(tabId); const snapshot = await scoped.send(tabId, "DOMSnapshot.captureSnapshot", { computedStyles: REQUESTED_STYLES, includePaintOrder: true, @@ -264,44 +251,7 @@ export async function captureObservationFacts( rootBatch?.rootFrameId ?? graph?.rootFrameId ?? rootBatch?.frames[0]?.frameId ?? "root"; const ownershipInvalid = new Set(invalid); const identities = new Map(); - const rootBefore = graph && rootBatch?.before.get(graph.rootFrameId); - if (rootBefore && rootFrameId !== rootBefore.frameId) { - invalid.add(rootFrameId); - issues.push({ - target: rootBefore.target, - frameId: rootFrameId, - stage: "identity", - reason: "document-changed", - }); - } const selected = new Map(currentFrames.map((frame) => [frame.frameId, frame])); - const freshFrames = new Map>(); - await collectTasks( - batches.filter((batch) => batch.before.size > 0), - async (batch) => { - try { - throwCaptureAborted(signal); - const reply = await sendToCdpTarget<{ frameTree?: CdpFrameTreeNode }>( - cdp, - batch.target, - "Page.getFrameTree", - {}, - ); - const tree = reply.frameTree - ? buildFrameGraph([{ target: batch.target, tree: reply.frameTree }]) - : null; - if (tree) - freshFrames.set( - cdpTargetKey(batch.target), - new Map(tree.frames.map((frame) => [frame.frameId, frame])), - ); - } catch (error) { - throwCaptureAborted(signal); - if (isCaptureAbort(error)) throw error; - } - }, - signal, - ); const checks = batches.flatMap((batch) => { const docs = new Map(batch.documents.map((doc) => [doc.frame.frameId, doc])); return batch.frames @@ -315,43 +265,33 @@ export async function captureObservationFacts( await collectTasks( checks, async ({ batch, frame, doc }) => { - const fresh = freshFrames.get(cdpTargetKey(batch.target)); throwCaptureAborted(signal); - const before = batch.before.get(frame.frameId); - const current = fresh?.get(frame.frameId); - const after = - before && current ? await readDocumentIdentity(cdp, current, signal) : undefined; - const snapshotMismatch = - before && - doc?.documentElementBackendNodeId !== undefined && - before.documentElementBackendNodeId !== doc.documentElementBackendNodeId; - const changed = - before && - (snapshotMismatch || - (fresh && !current) || - (after && !sameDocument(before, after)) || - cdp.getAttachmentId?.(tabId) !== before.attachmentId); - if (changed || (before && !after)) { - invalid.add(frame.frameId); - issues.push({ - target: frame.target, - frameId: frame.frameId, - stage: "identity", - reason: changed ? "document-changed" : "identity-unavailable", - }); - } else if ( - before && - after && - (!doc || doc.documentElementBackendNodeId === before.documentElementBackendNodeId) - ) { - identities.set(frame.frameId, after); - } else { + if (!batch.snapshotAttachmentId || doc?.documentElementBackendNodeId === undefined) { issues.push({ target: frame.target, frameId: frame.frameId, stage: "identity", reason: "identity-unverified", }); + return; + } + const identity: DocumentIdentity = { + attachmentId: batch.snapshotAttachmentId, + target: frame.target, + frameId: frame.frameId, + documentElementBackendNodeId: doc.documentElementBackendNodeId, + }; + const verified = await verifyDocumentIdentity(cdp, identity, signal); + const status = cdp.getAttachmentId?.(tabId) === identity.attachmentId ? verified : "changed"; + if (status === "current") identities.set(frame.frameId, identity); + else { + invalid.add(frame.frameId); + issues.push({ + target: frame.target, + frameId: frame.frameId, + stage: "identity", + reason: status === "changed" ? "document-changed" : "identity-unavailable", + }); } }, signal, diff --git a/apps/extension/src/tools/vom/document-identity.ts b/apps/extension/src/tools/vom/document-identity.ts index 90b6391..e10fc77 100644 --- a/apps/extension/src/tools/vom/document-identity.ts +++ b/apps/extension/src/tools/vom/document-identity.ts @@ -1,71 +1,53 @@ -import type { CdpFrame } from "@/browser-driver/frame-graph"; import type { CdpRunner } from "../shared"; import { sendToCdpTarget } from "../shared"; -import { - isAbortError as isCaptureAbort, - throwIfAborted as throwCaptureAborted, -} from "./capture-abort"; +import { isAbortError, throwIfAborted } from "./capture-abort"; import type { DocumentIdentity } from "./facts"; -/** Fresh, frame-scoped identity read. An isolated world supplies the owning - * document without querying each node or installing a page observer. */ -export async function readDocumentIdentity( +/** Compare the snapshot root with the current root in that exact frame. + * Deep serialization supplies the backend ID without a separate describeNode. */ +export async function verifyDocumentIdentity( cdp: CdpRunner, - frame: CdpFrame, + identity: DocumentIdentity, signal?: AbortSignal, -): Promise { - const attachmentId = cdp.getAttachmentId?.(frame.target.tabId); - if (!attachmentId || !frame.loaderId) return undefined; - let objectId: string | undefined; +): Promise<"current" | "changed" | "unavailable"> { + const attached = () => cdp.getAttachmentId?.(identity.target.tabId) === identity.attachmentId; + throwIfAborted(signal); + if (!attached()) return "changed"; + const objectGroup = `bsk-document-identity-${crypto.randomUUID()}`; const send = (method: string, params: object) => { - throwCaptureAborted(signal); - return sendToCdpTarget(cdp, frame.target, method, params); + throwIfAborted(signal); + return sendToCdpTarget(cdp, identity.target, method, params); }; try { const world = await send<{ executionContextId: number }>("Page.createIsolatedWorld", { - frameId: frame.frameId, + frameId: identity.frameId, worldName: "bsk-document-identity", }); - const element = await send<{ result?: { objectId?: string } }>("Runtime.evaluate", { + const reply = await send<{ + result?: { deepSerializedValue?: { type: string; value?: { backendNodeId?: number } } }; + }>("Runtime.evaluate", { expression: "document.documentElement", contextId: world.executionContextId, + objectGroup, + serializationOptions: { + serialization: "deep", + additionalParameters: { maxNodeDepth: 0, includeShadowTree: "none" }, + }, }); - objectId = element.result?.objectId; - if (!objectId) return undefined; - const reply = await send<{ node?: { backendNodeId?: number } }>("DOM.describeNode", { - objectId, - depth: 0, - }); - const backendNodeId = reply.node?.backendNodeId; - if (!backendNodeId || cdp.getAttachmentId?.(frame.target.tabId) !== attachmentId) - return undefined; - return { - attachmentId, - target: frame.target, - frameId: frame.frameId, - loaderId: frame.loaderId, - documentElementBackendNodeId: backendNodeId, - }; + if (!attached()) return "changed"; + const root = reply.result?.deepSerializedValue; + if (root?.type === "null") return "changed"; + const id = root?.type === "node" ? root.value?.backendNodeId : undefined; + if (id === undefined) return "unavailable"; + return id === identity.documentElementBackendNodeId ? "current" : "changed"; } catch (error) { - throwCaptureAborted(signal); - if (isCaptureAbort(error)) throw error; - return undefined; + throwIfAborted(signal); + if (isAbortError(error)) throw error; + return attached() ? "unavailable" : "changed"; } finally { - if (objectId) - await sendToCdpTarget(cdp, frame.target, "Runtime.releaseObject", { objectId }).catch( - () => {}, - ); - throwCaptureAborted(signal); + await sendToCdpTarget(cdp, identity.target, "Runtime.releaseObjectGroup", { + objectGroup, + }).catch(() => {}); + throwIfAborted(signal); } } - -export function sameDocument(a: DocumentIdentity, b: DocumentIdentity): boolean { - return ( - a.attachmentId === b.attachmentId && - a.target.tabId === b.target.tabId && - a.target.sessionId === b.target.sessionId && - a.frameId === b.frameId && - a.loaderId === b.loaderId && - a.documentElementBackendNodeId === b.documentElementBackendNodeId - ); -} diff --git a/apps/extension/src/tools/vom/facts.ts b/apps/extension/src/tools/vom/facts.ts index 36e4a13..c2e4e75 100644 --- a/apps/extension/src/tools/vom/facts.ts +++ b/apps/extension/src/tools/vom/facts.ts @@ -101,7 +101,6 @@ export interface DocumentIdentity { attachmentId: string; target: CdpTarget; frameId: string; - loaderId: string; documentElementBackendNodeId: number; }