@@ -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 (
0 commit comments