feat(browser): auto-commit con 60 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 11:42:31 +02:00
parent 37aacfcfa9
commit 8742cb25be
71 changed files with 5660 additions and 192 deletions
+88 -33
View File
@@ -10,17 +10,84 @@ import (
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)
MaxInflight int // requests en vuelo tolerados para considerar idle (default 2)
PollMs int // intervalo de chequeo en ms (default 100)
}
// isPersistentResourceType indica si un Network resourceType corresponde a una
// conexion de larga duracion que NO emite loadingFinished/loadingFailed y por
// tanto colgaria el contador inflight para siempre. La pagina abre estas
// conexiones (analytics en vivo, push, hot-reload) y nunca "terminan".
func isPersistentResourceType(resourceType string) bool {
switch resourceType {
case "WebSocket", "EventSource":
return true
default:
return false
}
}
// InflightTracker cuenta requests de red en vuelo de forma pura y testeable: no
// toca red ni CDP, solo recibe eventos ya parseados (requestId + resourceType) y
// mantiene el conjunto de requests activos. Trackea por requestId para que el
// loadingFinished/loadingFailed de un request que nunca contamos (una conexion
// persistente) sea un no-op en vez de un decremento espurio.
//
// Las conexiones persistentes (WebSocket, EventSource) se excluyen del conteo
// porque no emiten un evento de finalizacion: contarlas haria que la red nunca
// pareciera idle.
type InflightTracker struct {
mu sync.Mutex
tracked map[string]bool
}
// NewInflightTracker crea un tracker vacio listo para recibir eventos.
func NewInflightTracker() *InflightTracker {
return &InflightTracker{tracked: map[string]bool{}}
}
// OnRequest registra el inicio de un request (Network.requestWillBeSent). Ignora
// las conexiones persistentes para no contaminar el conteo.
func (t *InflightTracker) OnRequest(requestID, resourceType string) {
if isPersistentResourceType(resourceType) {
return
}
t.mu.Lock()
t.tracked[requestID] = true
t.mu.Unlock()
}
// OnFinish marca un request como completado (Network.loadingFinished).
func (t *InflightTracker) OnFinish(requestID string) { t.complete(requestID) }
// OnFail marca un request como fallido (Network.loadingFailed). A efectos de
// inflight, fallar y terminar son lo mismo: el request ya no esta en vuelo.
func (t *InflightTracker) OnFail(requestID string) { t.complete(requestID) }
func (t *InflightTracker) complete(requestID string) {
t.mu.Lock()
delete(t.tracked, requestID)
t.mu.Unlock()
}
// Inflight retorna el numero de requests actualmente en vuelo.
func (t *InflightTracker) Inflight() int {
t.mu.Lock()
defer t.mu.Unlock()
return len(t.tracked)
}
// IsIdle indica si el numero de requests en vuelo esta dentro del umbral dado.
func (t *InflightTracker) IsIdle(maxInflight int) bool {
return t.Inflight() <= maxInflight
}
// 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.
// Network.loadingFailed via el mecanismo OnEvent del CDPConn y delega el conteo
// en un InflightTracker. 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.
//
// Inmune a extensiones que mutan el DOM (Dark Reader, uBlock) y a animaciones
// JS, ya que la señal es red, no DOM.
@@ -36,41 +103,36 @@ func CdpWaitIdle(c *CDPConn, opts CdpWaitIdleOpts) error {
if opts.Timeout <= 0 {
opts.Timeout = 8 * time.Second
}
// MaxInflight 0 es el default semantico: queremos red completamente idle.
// MaxInflight default 2: la web moderna mantiene 1-2 beacons/analytics de
// fondo que casi nunca dejan inflight en 0; exigir 0 cuelga hasta el timeout.
if opts.MaxInflight <= 0 {
opts.MaxInflight = 2
}
if opts.PollMs <= 0 {
opts.PollMs = 100
}
var (
mu sync.Mutex
inflight int
)
tracker := NewInflightTracker()
// 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()
id, _ := p["requestId"].(string)
typ, _ := p["type"].(string)
tracker.OnRequest(id, typ)
})
defer cancel1()
cancel2 := c.OnEvent("Network.loadingFinished", func(_ string, p map[string]any) {
mu.Lock()
if inflight > 0 {
inflight--
}
mu.Unlock()
id, _ := p["requestId"].(string)
tracker.OnFinish(id)
})
defer cancel2()
cancel3 := c.OnEvent("Network.loadingFailed", func(_ string, p map[string]any) {
mu.Lock()
if inflight > 0 {
inflight--
}
mu.Unlock()
id, _ := p["requestId"].(string)
tracker.OnFail(id)
})
defer cancel3()
@@ -89,11 +151,7 @@ func CdpWaitIdle(c *CDPConn, opts CdpWaitIdleOpts) error {
for time.Now().Before(deadline) {
time.Sleep(pollInterval)
mu.Lock()
current := inflight
mu.Unlock()
if current <= opts.MaxInflight {
if tracker.IsIdle(opts.MaxInflight) {
// Red idle: iniciar o mantener la ventana de quietud.
if quietSince.IsZero() {
quietSince = time.Now()
@@ -107,8 +165,5 @@ func CdpWaitIdle(c *CDPConn, opts CdpWaitIdleOpts) error {
}
}
mu.Lock()
current := inflight
mu.Unlock()
return fmt.Errorf("cdp wait idle: red no alcanzo idle despues de %s (inflight=%d)", opts.Timeout, current)
return fmt.Errorf("cdp wait idle: red no alcanzo idle despues de %s (inflight=%d)", opts.Timeout, tracker.Inflight())
}