Files
fn_registry/functions/browser/cdp_eval_in_frame.go
T
egutierrez 8742cb25be feat(browser): auto-commit con 60 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:31 +02:00

177 lines
5.2 KiB
Go

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
}