@@ -113,6 +113,8 @@ const MAX_CONSOLE_STACK_FRAMES = 20;
113113const MAX_NETWORK_BUFFER = 200 ;
114114const MAX_NETWORK_FIELD_LENGTH = 4096 ;
115115const MAX_NETWORK_REQUEST_META = 1024 ;
116+ const FRAME_DISCOVERY_TIMEOUT_MS = 1000 ;
117+ const FRAME_DISCOVERY_QUIET_MS = 20 ;
116118
117119interface ParsedDialogOpening {
118120 type : JavaScriptDialogType ;
@@ -134,6 +136,27 @@ interface NetworkRequestMeta {
134136 truncated : boolean ;
135137}
136138
139+ interface FrameDiscoveryState {
140+ sessions : Set < string > ;
141+ pending : Set < Promise < void > > ;
142+ generation : number ;
143+ }
144+
145+ async function settleBeforeDeadline ( promises : Promise < void > [ ] , deadline : number ) : Promise < boolean > {
146+ if ( promises . length === 0 ) return true ;
147+ const remaining = deadline - Date . now ( ) ;
148+ if ( remaining <= 0 ) return false ;
149+ let timer : ReturnType < typeof setTimeout > | undefined ;
150+ const timedOut = await Promise . race ( [
151+ Promise . allSettled ( promises ) . then ( ( ) => false ) ,
152+ new Promise < boolean > ( ( resolve ) => {
153+ timer = setTimeout ( ( ) => resolve ( true ) , remaining ) ;
154+ } ) ,
155+ ] ) ;
156+ if ( timer ) clearTimeout ( timer ) ;
157+ return ! timedOut ;
158+ }
159+
137160/**
138161 * Wrapper around `chrome.debugger` that owns the "attach once per
139162 * tabId" cache and exposes typed `send<T>()`.
@@ -152,8 +175,7 @@ export class ChromiumCdp {
152175 private readonly networkSequences = new Map < number , number > ( ) ;
153176 private readonly networkDomainsEnabledTabs = new Set < number > ( ) ;
154177 private readonly networkRequestMeta = new Map < number , Map < string , NetworkRequestMeta > > ( ) ;
155- private readonly frameSessions = new Map < number , Set < string > > ( ) ;
156- private readonly frameAttachTasks = new Map < number , Set < Promise < void > > > ( ) ;
178+ private readonly frameDiscovery = new Map < number , FrameDiscoveryState > ( ) ;
157179 private detachSubscription : { dispose ( ) : void } | null = null ;
158180 private dialogSubscription : { dispose ( ) : void } | null = null ;
159181 private consoleSubscription : { dispose ( ) : void } | null = null ;
@@ -256,7 +278,7 @@ export class ChromiumCdp {
256278 ) ;
257279 if ( root . frameTree ) sources . push ( { target : { tabId } , tree : root . frameTree } ) ;
258280
259- const sessions = [ ...( this . frameSessions . get ( tabId ) ?? [ ] ) ] ;
281+ const sessions = [ ...( this . frameDiscovery . get ( tabId ) ?. sessions ?? [ ] ) ] ;
260282 const childTrees = await Promise . all (
261283 sessions . map ( async ( sessionId ) : Promise < CdpFrameTreeSource | null > => {
262284 const target = { tabId, sessionId } ;
@@ -456,8 +478,7 @@ export class ChromiumCdp {
456478 this . networkSequences . clear ( ) ;
457479 this . networkDomainsEnabledTabs . clear ( ) ;
458480 this . networkRequestMeta . clear ( ) ;
459- this . frameSessions . clear ( ) ;
460- this . frameAttachTasks . clear ( ) ;
481+ this . frameDiscovery . clear ( ) ;
461482 await Promise . all (
462483 tabs . map ( async ( tabId ) => {
463484 try {
@@ -527,24 +548,23 @@ export class ChromiumCdp {
527548 if ( ! sessionId ) return ;
528549
529550 if ( method === "Target.detachedFromTarget" ) {
530- this . frameSessions . get ( tabId ) ?. delete ( sessionId ) ;
551+ const state = this . frameDiscovery . get ( tabId ) ;
552+ if ( state ?. sessions . delete ( sessionId ) ) state . generation += 1 ;
531553 return ;
532554 }
533555 if ( method !== "Target.attachedToTarget" ) return ;
534556 const targetInfo = raw . targetInfo as { type ?: string } | undefined ;
535557 if ( targetInfo ?. type && targetInfo . type !== "iframe" ) return ;
536558
537- const sessions = this . frameSessions . get ( tabId ) ?? new Set < string > ( ) ;
538- sessions . add ( sessionId ) ;
539- this . frameSessions . set ( tabId , sessions ) ;
559+ const state = this . frameDiscoveryState ( tabId ) ;
560+ if ( state . sessions . has ( sessionId ) ) return ;
561+ state . sessions . add ( sessionId ) ;
562+ state . generation += 1 ;
540563
541564 const task = this . initializeFrameTarget ( { tabId, sessionId } ) ;
542- const tasks = this . frameAttachTasks . get ( tabId ) ?? new Set < Promise < void > > ( ) ;
543- tasks . add ( task ) ;
544- this . frameAttachTasks . set ( tabId , tasks ) ;
565+ state . pending . add ( task ) ;
545566 void task . finally ( ( ) => {
546- tasks . delete ( task ) ;
547- if ( tasks . size === 0 ) this . frameAttachTasks . delete ( tabId ) ;
567+ state . pending . delete ( task ) ;
548568 } ) ;
549569 } ;
550570 this . api . onEvent . addListener ( listener ) ;
@@ -557,23 +577,51 @@ export class ChromiumCdp {
557577 try {
558578 await this . enableFrameDiscovery ( target ) ;
559579 } catch ( err ) {
560- this . frameSessions . get ( target . tabId ) ?. delete ( target . sessionId as string ) ;
580+ const state = this . frameDiscovery . get ( target . tabId ) ;
581+ if ( state ?. sessions . delete ( target . sessionId as string ) ) state . generation += 1 ;
561582 console . debug ( "[bsk cdp] child frame target initialization failed" , { target, err } ) ;
562583 }
563584 }
564585
565586 private async drainFrameAttachTasks ( tabId : number ) : Promise < void > {
566- for ( let round = 0 ; round < 4 ; round += 1 ) {
567- await Promise . resolve ( ) ;
568- const tasks = [ ...( this . frameAttachTasks . get ( tabId ) ?? [ ] ) ] ;
569- if ( tasks . length === 0 ) return ;
570- await Promise . allSettled ( tasks ) ;
587+ const deadline = Date . now ( ) + FRAME_DISCOVERY_TIMEOUT_MS ;
588+ while ( Date . now ( ) < deadline ) {
589+ const state = this . frameDiscovery . get ( tabId ) ;
590+ const generation = state ?. generation ?? 0 ;
591+ if ( state ?. pending . size && ! ( await settleBeforeDeadline ( [ ...state . pending ] , deadline ) ) ) break ;
592+
593+ // Target.setAutoAttach may enqueue the next attachedToTarget event after
594+ // its command promise settles. Wait for a short quiet window, then finish
595+ // only if neither the state object nor its generation changed.
596+ const quietTime = Math . min ( FRAME_DISCOVERY_QUIET_MS , deadline - Date . now ( ) ) ;
597+ if ( quietTime <= 0 ) break ;
598+ await new Promise < void > ( ( resolve ) => setTimeout ( resolve , quietTime ) ) ;
599+ const current = this . frameDiscovery . get ( tabId ) ;
600+ if (
601+ current === state &&
602+ ( current ?. generation ?? 0 ) === generation &&
603+ ! current ?. pending . size
604+ ) {
605+ return ;
606+ }
571607 }
608+ console . debug ( "[bsk cdp] frame discovery did not reach quiescence before timeout" , { tabId } ) ;
609+ }
610+
611+ private frameDiscoveryState ( tabId : number ) : FrameDiscoveryState {
612+ const existing = this . frameDiscovery . get ( tabId ) ;
613+ if ( existing ) return existing ;
614+ const created : FrameDiscoveryState = {
615+ sessions : new Set ( ) ,
616+ pending : new Set ( ) ,
617+ generation : 0 ,
618+ } ;
619+ this . frameDiscovery . set ( tabId , created ) ;
620+ return created ;
572621 }
573622
574623 private clearFrameState ( tabId : number ) : void {
575- this . frameSessions . delete ( tabId ) ;
576- this . frameAttachTasks . delete ( tabId ) ;
624+ this . frameDiscovery . delete ( tabId ) ;
577625 }
578626
579627 private bindDialogHandler ( ) : void {
0 commit comments