package browser import ( "fmt" "sync" "time" ) // CdpWaitIdleOpts configura el comportamiento de CdpWaitIdle. type CdpWaitIdleOpts struct { QuietMs int // ms que inflight debe permanecer <= MaxInflight (default 500) Timeout time.Duration // maximo total a esperar (default 8s) MaxInflight int // requests en vuelo tolerados para considerar idle (default 0) PollMs int // intervalo de chequeo en ms (default 100) } // CdpWaitIdle espera a que la actividad de red de la pagina llegue a idle. // Suscribe eventos Network.requestWillBeSent / Network.loadingFinished / // Network.loadingFailed via el mecanismo OnEvent del CDPConn para mantener // un contador de requests en vuelo (inflight). Cuando inflight <= MaxInflight // de forma continuada durante QuietMs milisegundos, la funcion retorna nil. // Si se alcanza Timeout sin lograr esa ventana quieta, retorna error con el // inflight actual en el mensaje. // // Inmune a extensiones que mutan el DOM (Dark Reader, uBlock) y a animaciones // JS, ya que la seƱal es red, no DOM. func CdpWaitIdle(c *CDPConn, opts CdpWaitIdleOpts) error { if c == nil { return fmt.Errorf("cdp wait idle: conexion nula") } // Aplicar defaults. if opts.QuietMs <= 0 { opts.QuietMs = 500 } if opts.Timeout <= 0 { opts.Timeout = 8 * time.Second } // MaxInflight 0 es el default semantico: queremos red completamente idle. if opts.PollMs <= 0 { opts.PollMs = 100 } var ( mu sync.Mutex inflight int ) // Suscribir eventos Network usando el mismo mecanismo que cdp_har_record: // c.OnEvent retorna una funcion cancel que des-registra el handler. // Multiples consumidores del mismo metodo son soportados (slice de handlers). cancel1 := c.OnEvent("Network.requestWillBeSent", func(_ string, p map[string]any) { mu.Lock() inflight++ mu.Unlock() }) defer cancel1() cancel2 := c.OnEvent("Network.loadingFinished", func(_ string, p map[string]any) { mu.Lock() if inflight > 0 { inflight-- } mu.Unlock() }) defer cancel2() cancel3 := c.OnEvent("Network.loadingFailed", func(_ string, p map[string]any) { mu.Lock() if inflight > 0 { inflight-- } mu.Unlock() }) defer cancel3() // Habilitar dominio Network (igual que cdp_har_record). if _, err := c.sendCDP("Network.enable", nil); err != nil { return fmt.Errorf("cdp wait idle: Network.enable: %w", err) } defer c.sendCDP("Network.disable", nil) //nolint:errcheck deadline := time.Now().Add(opts.Timeout) pollInterval := time.Duration(opts.PollMs) * time.Millisecond quietThreshold := time.Duration(opts.QuietMs) * time.Millisecond var quietSince time.Time for time.Now().Before(deadline) { time.Sleep(pollInterval) mu.Lock() current := inflight mu.Unlock() if current <= opts.MaxInflight { // Red idle: iniciar o mantener la ventana de quietud. if quietSince.IsZero() { quietSince = time.Now() } if time.Since(quietSince) >= quietThreshold { return nil } } else { // Actividad detectada: reiniciar ventana. quietSince = time.Time{} } } mu.Lock() current := inflight mu.Unlock() return fmt.Errorf("cdp wait idle: red no alcanzo idle despues de %s (inflight=%d)", opts.Timeout, current) }