Skip to content

Commit 3d3e97c

Browse files
committed
fix oopif
1 parent 7ce1533 commit 3d3e97c

6 files changed

Lines changed: 520 additions & 387 deletions

File tree

src/cdp/frame-context-manager.ts

Lines changed: 145 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,7 @@ export class FrameContextManager {
2828
CDPSession,
2929
Array<{ event: string; handler: (...args: unknown[]) => void }>
3030
>();
31-
private readonly autoAttachedSessions = new Map<
32-
string,
33-
{ session: CDPSession; frameId: string }
34-
>();
35-
private autoAttachEnabled = false;
36-
private autoAttachSetupPromise: Promise<void> | null = null;
37-
private autoAttachRootSession: CDPSession | null = null;
31+
private readonly oopifFrameIds = new Set<string>();
3832
private readonly pageTrackedSessions = new WeakSet<CDPSession>();
3933
private nextFrameIndex = 0;
4034
private initialized = false;
@@ -107,6 +101,10 @@ export class FrameContextManager {
107101
return this.graph.getFrame(frameId);
108102
}
109103

104+
getFrameIndex(frameId: string): number | undefined {
105+
return this.graph.getFrameIndex(frameId);
106+
}
107+
110108
getExecutionContextId(frameId: string): number | undefined {
111109
return this.frameExecutionContexts.get(frameId);
112110
}
@@ -146,6 +144,31 @@ export class FrameContextManager {
146144
});
147145
}
148146

147+
/**
148+
* Get all same-origin frames (use main session for these)
149+
*/
150+
getSameOriginFrames(): FrameRecord[] {
151+
return this.graph.getAllFrames().filter((frame: FrameRecord) =>
152+
!this.oopifFrameIds.has(frame.frameId)
153+
);
154+
}
155+
156+
/**
157+
* Get all OOPIF frames (each has its own session)
158+
*/
159+
getOOPIFs(): FrameRecord[] {
160+
return this.graph.getAllFrames().filter((frame: FrameRecord) =>
161+
this.oopifFrameIds.has(frame.frameId)
162+
);
163+
}
164+
165+
/**
166+
* Check if a frame is an OOPIF
167+
*/
168+
isOOPIF(frameId: string): boolean {
169+
return this.oopifFrameIds.has(frameId);
170+
}
171+
149172
toJSON(): { graph: ReturnType<FrameGraph["toJSON"]> } {
150173
return { graph: this.graph.toJSON() };
151174
}
@@ -171,9 +194,7 @@ export class FrameContextManager {
171194
}
172195
this.sessionListeners.clear();
173196

174-
this.autoAttachedSessions.clear();
175-
this.autoAttachEnabled = false;
176-
this.autoAttachRootSession = null;
197+
this.oopifFrameIds.clear();
177198
}
178199

179200
async ensureInitialized(): Promise<void> {
@@ -182,7 +203,6 @@ export class FrameContextManager {
182203

183204
this.initializingPromise = (async () => {
184205
const rootSession = this.client.rootSession;
185-
await this.enableAutoAttach(rootSession);
186206
await this.captureFrameTree(rootSession);
187207
this.initialized = true;
188208
})().finally(() => {
@@ -193,17 +213,9 @@ export class FrameContextManager {
193213
}
194214

195215
private async captureFrameTree(session: CDPSession): Promise<void> {
196-
const [{ frameTree }, { targetInfos }] = await Promise.all([
197-
session.send<Protocol.Page.GetFrameTreeResponse>("Page.getFrameTree"),
198-
session.send<Protocol.Target.GetTargetsResponse>("Target.getTargets"),
199-
]);
216+
const { frameTree } = await session.send<Protocol.Page.GetFrameTreeResponse>("Page.getFrameTree");
200217
if (!frameTree) return;
201218

202-
const targetMap = new Map<string, Protocol.Target.TargetInfo>();
203-
for (const target of targetInfos ?? []) {
204-
targetMap.set(target.targetId, target);
205-
}
206-
207219
let indexCounter = 0;
208220
const traverse = async (node: FrameTreeNode, parentFrameId: string | null): Promise<void> => {
209221
const frameId = node.frame.id;
@@ -225,154 +237,152 @@ export class FrameContextManager {
225237
await this.populateFrameOwner(session, frameId);
226238
}
227239

228-
const target = this.findTargetForFrame(targetMap, frameId);
229-
if (target && target.targetId && !this.autoAttachEnabled) {
230-
await this.attachToTarget(session, target.targetId, frameId);
231-
}
232-
233240
for (const child of node.childFrames ?? []) {
234241
await traverse(child, frameId);
235242
}
236243
};
237244

238245
await traverse(frameTree, frameTree.frame?.parentId ?? null);
239-
}
240246

241-
private findTargetForFrame(
242-
targetMap: Map<string, Protocol.Target.TargetInfo>,
243-
frameId: string
244-
): Protocol.Target.TargetInfo | undefined {
245-
for (const target of targetMap.values()) {
246-
const info = target as { frameId?: string };
247-
if (info.frameId === frameId) {
248-
return target;
249-
}
250-
}
251-
return undefined;
247+
// Discover and attach OOPIF frames
248+
await this.captureOOPIFs(indexCounter);
252249
}
253250

254-
private async attachToTarget(
255-
session: CDPSession,
256-
targetId: string,
257-
frameId: string
258-
): Promise<void> {
259-
try {
260-
const { sessionId } = await session.send<Protocol.Target.AttachToTargetResponse>(
261-
"Target.attachToTarget",
262-
{ targetId, flatten: true }
263-
);
264-
const childSession = await this.client.createSession({
265-
type: "raw",
266-
target: { sessionId },
267-
});
268-
this.setFrameSession(frameId, childSession);
269-
} catch (error) {
270-
console.warn(
271-
`[FrameContextManager] Failed to attach to target ${targetId} for frame ${frameId}:`,
272-
error
273-
);
274-
}
275-
}
276251

277252
private async populateFrameOwner(session: CDPSession, frameId: string): Promise<void> {
278253
try {
279254
const owner = await session.send<Protocol.DOM.GetFrameOwnerResponse>("DOM.getFrameOwner", { frameId });
280255
const record = this.graph.getFrame(frameId);
281256
if (!record) return;
282257
this.graph.upsertFrame({ ...record, backendNodeId: owner.backendNodeId ?? record.backendNodeId });
283-
} catch {}
258+
} catch {
259+
// Ignore errors when getting frame owner (e.g., for main frame or OOPIF)
260+
}
284261
}
285262

286-
async enableAutoAttach(session: CDPSession): Promise<void> {
287-
if (this.autoAttachEnabled) {
288-
return;
263+
private hasFrameWithUrl(url: string): boolean {
264+
if (!url || url === 'about:blank') return false;
265+
266+
for (const frame of this.graph.getAllFrames()) {
267+
if (frame.url === url) return true;
289268
}
290-
if (this.autoAttachSetupPromise) {
291-
return this.autoAttachSetupPromise;
292-
}
293-
294-
this.autoAttachRootSession = session;
295-
296-
this.autoAttachSetupPromise = (async () => {
297-
session.on("Target.attachedToTarget", this.handleTargetAttached);
298-
session.on("Target.detachedFromTarget", this.handleTargetDetached);
299-
await session.send("Target.setAutoAttach", {
300-
autoAttach: true,
301-
flatten: true,
302-
waitForDebuggerOnStart: false,
303-
});
304-
await this.trackPageEvents(session);
305-
this.autoAttachEnabled = true;
306-
this.log("[FrameContext] Target auto-attach enabled");
307-
})().finally(() => {
308-
this.autoAttachSetupPromise = null;
309-
});
269+
return false;
270+
}
310271

311-
return this.autoAttachSetupPromise;
272+
private getFrameIdByUrl(url: string): string | null {
273+
if (!url || url === 'about:blank') return null;
274+
275+
for (const frame of this.graph.getAllFrames()) {
276+
if (frame.url === url) return frame.frameId;
277+
}
278+
return null;
312279
}
313280

314-
private handleTargetAttached = async (
315-
event: Protocol.Target.AttachedToTargetEvent
316-
): Promise<void> => {
317-
const frameId = (event.targetInfo as { frameId?: string }).frameId;
318-
if (!frameId) {
281+
private async captureOOPIFs(startIndex: number): Promise<void> {
282+
const pageUnknown = this.client.getPage?.();
283+
if (!pageUnknown) {
284+
this.log("[FrameContext] No page available for OOPIF discovery");
319285
return;
320286
}
321287

322-
try {
323-
const session = await this.client.createSession({
324-
type: "raw",
325-
target: { sessionId: event.sessionId },
326-
});
288+
// Type cast to Playwright Page - this is safe because we're using PlaywrightCDPClient
289+
const page = pageUnknown as {
290+
context(): { newCDPSession(frame: unknown): Promise<CDPSession> };
291+
frames(): Array<{
292+
url(): string;
293+
parentFrame(): unknown | null;
294+
name(): string;
295+
}>;
296+
mainFrame(): unknown;
297+
};
327298

328-
this.autoAttachedSessions.set(event.sessionId, { session, frameId });
329-
this.setFrameSession(frameId, session);
330-
this.graph.upsertFrame({
331-
frameId,
332-
parentFrameId: event.targetInfo.openerFrameId ?? null,
333-
name: event.targetInfo.title,
334-
url: event.targetInfo.url,
335-
lastUpdated: Date.now(),
336-
});
299+
const context = page.context();
300+
const allFrames = page.frames();
337301

338-
this.log(
339-
`[FrameContext] Auto-attached session ${session.id ?? event.sessionId} for frame ${frameId} (${event.targetInfo.url ||
340-
"n/a"})`
341-
);
342-
} catch (error) {
343-
console.warn(
344-
`[FrameContext] Failed to auto-attach session for frame ${frameId}:`,
345-
error
346-
);
347-
}
348-
};
302+
// Filter frames to process (exclude main frame and already-captured same-origin frames)
303+
const framesToCheck = allFrames.filter((frame) => {
304+
if (frame === page.mainFrame()) return false;
305+
const frameUrl = frame.url();
306+
return !this.hasFrameWithUrl(frameUrl);
307+
});
349308

350-
private handleTargetDetached = async (
351-
event: Protocol.Target.DetachedFromTargetEvent
352-
): Promise<void> => {
353-
const record = this.autoAttachedSessions.get(event.sessionId);
354-
if (!record) {
309+
if (framesToCheck.length === 0) {
355310
return;
356311
}
357312

358-
this.autoAttachedSessions.delete(event.sessionId);
359-
const { session, frameId } = record;
360-
361-
if (this.sessions.get(frameId) === session) {
362-
this.sessions.delete(frameId);
363-
this.graph.removeFrame(frameId);
364-
}
313+
// Parallelize OOPIF discovery: try to create CDP session for all frames simultaneously
314+
const discoveryPromises = framesToCheck.map(async (frame, index) => {
315+
const frameUrl = frame.url();
316+
317+
// Try to create CDP session - if it succeeds, this is an OOPIF
318+
let oopifSession: CDPSession | null = null;
319+
try {
320+
oopifSession = await context.newCDPSession(frame);
321+
} catch {
322+
// Failed to create session = same-origin frame (already processed)
323+
this.log(`[FrameContext] Frame ${frameUrl} is same-origin, skipping`);
324+
return null;
325+
}
365326

366-
try {
367-
await session.detach();
368-
} catch {
369-
// ignore
370-
}
327+
// Success! This is an OOPIF - get its CDP frame ID
328+
try {
329+
await oopifSession.send("Page.enable");
330+
const { frameTree } = await oopifSession.send("Page.getFrameTree");
331+
const frameId = frameTree.frame.id;
332+
333+
this.log(`[FrameContext] Discovered OOPIF: frameId=${frameId}, url=${frameUrl}`);
334+
335+
// Find parent frame
336+
const parentFrameUnknown = frame.parentFrame();
337+
const parentFrame = parentFrameUnknown as { url(): string } | null;
338+
const parentFrameUrl = parentFrame?.url();
339+
340+
return {
341+
frameId,
342+
session: oopifSession,
343+
url: frameUrl,
344+
name: frame.name() || undefined,
345+
parentFrameUrl,
346+
discoveryOrder: index, // Preserve original order for deterministic frame indices
347+
};
348+
} catch (_error) {
349+
this.log(`[FrameContext] Failed to process OOPIF ${frameUrl}: ${_error}`);
350+
if (oopifSession) {
351+
await oopifSession.detach().catch(() => {
352+
// ignore detach errors
353+
});
354+
}
355+
return null;
356+
}
357+
});
371358

372-
this.log(
373-
`[FrameContext] Auto-detached session ${session.id ?? event.sessionId} for frame ${frameId}`
359+
// Wait for all OOPIF discovery to complete in parallel
360+
const discoveredOOPIFs = (await Promise.all(discoveryPromises)).filter(
361+
(result): result is NonNullable<typeof result> => result !== null
374362
);
375-
};
363+
364+
// Now assign frame indices and register all OOPIFs in deterministic order
365+
// Sort by discovery order to maintain deterministic frame indices
366+
discoveredOOPIFs.sort((a, b) => a.discoveryOrder - b.discoveryOrder);
367+
368+
for (let i = 0; i < discoveredOOPIFs.length; i++) {
369+
const oopif = discoveredOOPIFs[i];
370+
const frameIndex = startIndex + i; // Sequential indices for discovered OOPIFs
371+
const parentFrameId = oopif.parentFrameUrl
372+
? this.getFrameIdByUrl(oopif.parentFrameUrl)
373+
: null;
374+
375+
this.setFrameSession(oopif.frameId, oopif.session);
376+
this.assignFrameIndex(oopif.frameId, frameIndex);
377+
this.oopifFrameIds.add(oopif.frameId);
378+
this.upsertFrame({
379+
frameId: oopif.frameId,
380+
parentFrameId,
381+
url: oopif.url,
382+
name: oopif.name,
383+
});
384+
}
385+
}
376386

377387
private async trackPageEvents(session: CDPSession): Promise<void> {
378388
if (this.pageTrackedSessions.has(session)) {
@@ -432,7 +442,7 @@ export class FrameContextManager {
432442
const index = this.nextFrameIndex++;
433443
this.assignFrameIndex(frameId, index);
434444
}
435-
const rootSession = this.autoAttachRootSession ?? this.client.rootSession;
445+
const rootSession = this.client.rootSession;
436446
this.setFrameSession(frameId, rootSession);
437447
await this.populateFrameOwner(rootSession, frameId);
438448
this.log(

src/cdp/frame-graph.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export class FrameGraph {
3636
return this.children.get(parentFrameId) ?? [];
3737
}
3838

39+
getAllFrames(): FrameRecord[] {
40+
return Array.from(this.frames.values());
41+
}
42+
3943
upsertFrame(record: Omit<FrameRecord, "lastUpdated"> & { lastUpdated?: number }): FrameRecord {
4044
const existing = this.frames.get(record.frameId);
4145
const lastUpdated = record.lastUpdated ?? Date.now();

src/cdp/playwright-adapter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ class PlaywrightCDPClient implements CDPClient {
144144
}
145145
}
146146

147+
getPage(): Page {
148+
return this.page;
149+
}
150+
147151
async dispose(): Promise<void> {
148152
this.sessionPoolCleanup.forEach((cleanup) => cleanup());
149153
this.sessionPoolCleanup.clear();

0 commit comments

Comments
 (0)