package browser import ( "encoding/json" "fmt" "strings" "sync" ) // frameCtxCache mapea frameID -> executionContextId del isolated world creado // para ese frame. Evita pagar Page.createIsolatedWorld en cada CdpEvalInFrame. // Es puro y testeable de forma aislada (su propio mutex, sin tocar CDP). type frameCtxCache struct { mu sync.Mutex m map[string]int } func newFrameCtxCache() *frameCtxCache { return &frameCtxCache{m: map[string]int{}} } func (f *frameCtxCache) get(frameID string) (int, bool) { f.mu.Lock() defer f.mu.Unlock() id, ok := f.m[frameID] return id, ok } func (f *frameCtxCache) set(frameID string, ctxID int) { f.mu.Lock() f.m[frameID] = ctxID f.mu.Unlock() } func (f *frameCtxCache) invalidate(frameID string) { f.mu.Lock() delete(f.m, frameID) f.mu.Unlock() } // isStaleContextError reconoce el error de CDP cuando un executionContextId // cacheado ya no existe (el frame recargó/navegó y su isolated world murió). Es // puro: decide a partir del texto del error. Permite reintentar recreando el // mundo en vez de fallar. func isStaleContextError(err error) bool { if err == nil { return false } s := err.Error() return strings.Contains(s, "Cannot find context") || strings.Contains(s, "context with specified id") || strings.Contains(s, "Execution context was destroyed") || strings.Contains(s, "uniqueContextId") } // frameCtxCacheLazy devuelve el cache de contextos del frame de esta conexion, // inicializandolo en el primer uso. El mutex de CDPConn solo protege este // lazy-init del puntero. func (c *CDPConn) frameCtxCacheLazy() *frameCtxCache { c.frameCtxMu.Lock() defer c.frameCtxMu.Unlock() if c.frameCtx == nil { c.frameCtx = newFrameCtxCache() } return c.frameCtx } // createIsolatedWorld crea un mundo aislado en el frame y devuelve su // executionContextId. func createIsolatedWorld(c *CDPConn, frameID string) (int, error) { ctxRes, err := c.sendCDP("Page.createIsolatedWorld", map[string]any{ "frameId": frameID, "worldName": "fn_registry_isolated", "grantUniversalAccess": false, }) if err != nil { return 0, fmt.Errorf("createIsolatedWorld: %w", err) } ctxIDRaw, ok := ctxRes["executionContextId"] if !ok { return 0, fmt.Errorf("createIsolatedWorld: executionContextId no encontrado en respuesta") } ctxID, ok := ctxIDRaw.(float64) if !ok { return 0, fmt.Errorf("createIsolatedWorld: executionContextId tipo inesperado: %T", ctxIDRaw) } return int(ctxID), nil } // evalInFrameContext ejecuta la expresion en el executionContextId dado y // serializa el resultado como string (mismo patron que CdpEvaluate). func evalInFrameContext(c *CDPConn, ctxID int, frameID, expression string) (string, error) { evRes, err := c.sendCDP("Runtime.evaluate", map[string]any{ "expression": expression, "contextId": ctxID, "returnByValue": true, "awaitPromise": true, }) if err != nil { return "", fmt.Errorf("Runtime.evaluate: %w", err) } if exc, ok := evRes["exceptionDetails"]; ok && exc != nil { excMap, _ := exc.(map[string]any) text, _ := excMap["text"].(string) return "", fmt.Errorf("excepcion JS en frame %q: %s", frameID, text) } resVal, ok := evRes["result"].(map[string]any) if !ok { return "", fmt.Errorf("resultado inesperado: %v", evRes) } value, ok := resVal["value"] if !ok { typ, _ := resVal["type"].(string) return typ, nil } if s, ok := value.(string); ok { return s, nil } b, err := json.Marshal(value) if err != nil { return fmt.Sprintf("%v", value), nil } return string(b), nil } // CdpEvalInFrame ejecuta una expresion JavaScript en el contexto aislado de un iframe // especifico usando Page.createIsolatedWorld + Runtime.evaluate con el contextId del frame. // Retorna el resultado serializado como string. // // Cachea el executionContextId por frameID en la conexion: la primera llamada // crea el mundo aislado, las siguientes lo reutilizan. Si el contexto cacheado // caducó (el frame navegó/recargó), recrea el mundo una vez y reintenta. func CdpEvalInFrame(c *CDPConn, frameID, expression string) (string, error) { if c == nil { return "", fmt.Errorf("cdp eval in frame: conexion nula") } if frameID == "" { return "", fmt.Errorf("cdp eval in frame: frameID vacio") } // Page.enable es idempotente; necesario antes de crear mundos aislados. if _, err := c.sendCDP("Page.enable", nil); err != nil { return "", fmt.Errorf("cdp eval in frame: Page.enable: %w", err) } cache := c.frameCtxCacheLazy() ctxID, cached := cache.get(frameID) if !cached { newID, err := createIsolatedWorld(c, frameID) if err != nil { return "", fmt.Errorf("cdp eval in frame: %w", err) } ctxID = newID cache.set(frameID, ctxID) } out, evErr := evalInFrameContext(c, ctxID, frameID, expression) if evErr != nil && cached && isStaleContextError(evErr) { // El contexto cacheado murió (frame recargó). Recrear una vez. cache.invalidate(frameID) newID, err := createIsolatedWorld(c, frameID) if err != nil { return "", fmt.Errorf("cdp eval in frame: %w", err) } cache.set(frameID, newID) out, evErr = evalInFrameContext(c, newID, frameID, expression) } if evErr != nil { return "", fmt.Errorf("cdp eval in frame: %w", evErr) } return out, nil }