refactor(webkit): align WebViewID to port interface and simplify JS execution API
- commit
- 105c9619435bc3c68db0db5aa390144aa6f01f24
- author
- brice · · 2025-12-14 19:28 UTC
- committer
- brice · · 2025-12-14 22:12 UTC
- parent
- 3385450
- signature
- unsigned
4 files · +55 -83
M internal/infrastructure/webkit/context.go +1 -1
| @@ -74,7 +74,7 @@ func NewWebKitContext(ctx context.Context, dataDir, cacheDir string) (*WebKitCon | |||
| 74 | 74 | // initNetworkSession creates and configures the persistent network session. | |
| 75 | 75 | func (c *WebKitContext) initNetworkSession() error { | |
| 76 | 76 | // Create persistent network session | |
| 77 | - | session := webkit.NewNetworkSession(c.dataDir, c.cacheDir) | |
| 77 | + | session := webkit.NewNetworkSession(&c.dataDir, &c.cacheDir) | |
| 78 | 78 | if session == nil { | |
| 79 | 79 | return fmt.Errorf("failed to create network session") | |
| 80 | 80 | } | |
| @@ -74,7 +74,7 @@ func NewWebKitContext(ctx context.Context, dataDir, cacheDir string) (*WebKitCon | |||||
| 74 | // initNetworkSession creates and configures the persistent network session. | 74 | // initNetworkSession creates and configures the persistent network session. | ||
| 75 | func (c *WebKitContext) initNetworkSession() error { | 75 | func (c *WebKitContext) initNetworkSession() error { | ||
| 76 | // Create persistent network session | 76 | // Create persistent network session | ||
| 77 | - | session := webkit.NewNetworkSession(c.dataDir, c.cacheDir) | 77 | + | session := webkit.NewNetworkSession(&c.dataDir, &c.cacheDir) |
| 78 | if session == nil { | 78 | if session == nil { | ||
| 79 | return fmt.Errorf("failed to create network session") | 79 | return fmt.Errorf("failed to create network session") | ||
| 80 | } | 80 | } | ||
M internal/infrastructure/webkit/injector.go +15 -10
| @@ -174,7 +174,8 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||
| 174 | 174 | } | |
| 175 | 175 | } | |
| 176 | 176 | ||
| 177 | - | // InjectStyles adds the component CSS to the isolated world. | |
| 177 | + | // InjectStyles injects component CSS as a JS variable into the isolated world. | |
| 178 | + | // The shadow DOM code reads window.__dumber_component_styles and injects it into the shadow root. | |
| 178 | 179 | func (ci *ContentInjector) InjectStyles(ctx context.Context, ucm *webkit.UserContentManager) { | |
| 179 | 180 | log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger() | |
| 180 | 181 | ||
| @@ -188,19 +189,23 @@ func (ci *ContentInjector) InjectStyles(ctx context.Context, ucm *webkit.UserCon | |||
| 188 | 189 | return | |
| 189 | 190 | } | |
| 190 | 191 | ||
| 191 | - | style := webkit.NewUserStyleSheetForWorld( | |
| 192 | - | assets.ComponentStyles, | |
| 192 | + | // Inject CSS as a JS string variable in isolated world at document-start | |
| 193 | + | // The shadow DOM code (shadowHost.ts) reads this and injects into shadow root | |
| 194 | + | // Using %q properly escapes the CSS for JavaScript | |
| 195 | + | componentStylesScript := fmt.Sprintf("window.__dumber_component_styles=%q;", assets.ComponentStyles) | |
| 196 | + | script := webkit.NewUserScriptForWorld( | |
| 197 | + | componentStylesScript, | |
| 193 | 198 | webkit.UserContentInjectTopFrameValue, | |
| 194 | - | webkit.UserStyleLevelUserValue, | |
| 199 | + | webkit.UserScriptInjectAtDocumentStartValue, | |
| 195 | 200 | ScriptWorldName, | |
| 196 | - | ci.allowList, | |
| 197 | - | ci.blockList, | |
| 201 | + | nil, | |
| 202 | + | nil, | |
| 198 | 203 | ) | |
| 199 | - | if style == nil { | |
| 200 | - | log.Warn().Msg("failed to create user style sheet") | |
| 204 | + | if script == nil { | |
| 205 | + | log.Warn().Msg("failed to create component styles script") | |
| 201 | 206 | return | |
| 202 | 207 | } | |
| 203 | 208 | ||
| 204 | - | ucm.AddStyleSheet(style) | |
| 205 | - | log.Debug().Msg("injected component styles") | |
| 209 | + | ucm.AddScript(script) | |
| 210 | + | log.Debug().Int("css_bytes", len(assets.ComponentStyles)).Msg("injected component styles") | |
| 206 | 211 | } | |
| @@ -174,7 +174,8 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||||
| 174 | } | 174 | } | ||
| 175 | } | 175 | } | ||
| 176 | 176 | ||||
| 177 | - | // InjectStyles adds the component CSS to the isolated world. | 177 | + | // InjectStyles injects component CSS as a JS variable into the isolated world. |
| 178 | + | // The shadow DOM code reads window.__dumber_component_styles and injects it into the shadow root. | |||
| 178 | func (ci *ContentInjector) InjectStyles(ctx context.Context, ucm *webkit.UserContentManager) { | 179 | func (ci *ContentInjector) InjectStyles(ctx context.Context, ucm *webkit.UserContentManager) { | ||
| 179 | log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger() | 180 | log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger() | ||
| 180 | 181 | ||||
| @@ -188,19 +189,23 @@ func (ci *ContentInjector) InjectStyles(ctx context.Context, ucm *webkit.UserCon | |||||
| 188 | return | 189 | return | ||
| 189 | } | 190 | } | ||
| 190 | 191 | ||||
| 191 | - | style := webkit.NewUserStyleSheetForWorld( | 192 | + | // Inject CSS as a JS string variable in isolated world at document-start |
| 192 | - | assets.ComponentStyles, | 193 | + | // The shadow DOM code (shadowHost.ts) reads this and injects into shadow root |
| 194 | + | // Using %q properly escapes the CSS for JavaScript | |||
| 195 | + | componentStylesScript := fmt.Sprintf("window.__dumber_component_styles=%q;", assets.ComponentStyles) | |||
| 196 | + | script := webkit.NewUserScriptForWorld( | |||
| 197 | + | componentStylesScript, | |||
| 193 | webkit.UserContentInjectTopFrameValue, | 198 | webkit.UserContentInjectTopFrameValue, | ||
| 194 | - | webkit.UserStyleLevelUserValue, | 199 | + | webkit.UserScriptInjectAtDocumentStartValue, |
| 195 | ScriptWorldName, | 200 | ScriptWorldName, | ||
| 196 | - | ci.allowList, | 201 | + | nil, |
| 197 | - | ci.blockList, | 202 | + | nil, |
| 198 | ) | 203 | ) | ||
| 199 | - | if style == nil { | 204 | + | if script == nil { |
| 200 | - | log.Warn().Msg("failed to create user style sheet") | 205 | + | log.Warn().Msg("failed to create component styles script") |
| 201 | return | 206 | return | ||
| 202 | } | 207 | } | ||
| 203 | 208 | ||||
| 204 | - | ucm.AddStyleSheet(style) | 209 | + | ucm.AddScript(script) |
| 205 | - | log.Debug().Msg("injected component styles") | 210 | + | log.Debug().Int("css_bytes", len(assets.ComponentStyles)).Msg("injected component styles") |
| 206 | } | 211 | } | ||
M internal/infrastructure/webkit/scheme_handler.go +1 -1
| @@ -172,7 +172,7 @@ func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response | |||
| 172 | 172 | contentType = "text/html" | |
| 173 | 173 | } | |
| 174 | 174 | ||
| 175 | - | req.Finish(&stream.InputStream, int64(len(response.Data)), contentType) | |
| 175 | + | req.Finish(&stream.InputStream, int64(len(response.Data)), &contentType) | |
| 176 | 176 | } | |
| 177 | 177 | ||
| 178 | 178 | // RegisterWithContext registers the dumb:// scheme with a WebKitContext. | |
| @@ -172,7 +172,7 @@ func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response | |||||
| 172 | contentType = "text/html" | 172 | contentType = "text/html" | ||
| 173 | } | 173 | } | ||
| 174 | 174 | ||||
| 175 | - | req.Finish(&stream.InputStream, int64(len(response.Data)), contentType) | 175 | + | req.Finish(&stream.InputStream, int64(len(response.Data)), &contentType) |
| 176 | } | 176 | } | ||
| 177 | 177 | ||||
| 178 | // RegisterWithContext registers the dumb:// scheme with a WebKitContext. | 178 | // RegisterWithContext registers the dumb:// scheme with a WebKitContext. | ||
M internal/infrastructure/webkit/webview.go +38 -71
| @@ -2,11 +2,11 @@ package webkit | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "errors" | |
| 6 | 5 | "fmt" | |
| 7 | 6 | "sync" | |
| 8 | 7 | "sync/atomic" | |
| 9 | 8 | ||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | 10 | "github.com/bnema/dumber/internal/logging" | |
| 11 | 11 | "github.com/bnema/puregotk-webkit/webkit" | |
| 12 | 12 | "github.com/jwijenbergh/puregotk/v4/gio" | |
| @@ -14,8 +14,9 @@ import ( | |||
| 14 | 14 | "github.com/rs/zerolog" | |
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | - | // WebViewID uniquely identifies a WebView instance. | |
| 18 | - | type WebViewID uint64 | |
| 17 | + | // WebViewID is an alias to port.WebViewID for clean architecture compliance. | |
| 18 | + | // Infrastructure layer uses the type defined in the application port. | |
| 19 | + | type WebViewID = port.WebViewID | |
| 19 | 20 | ||
| 20 | 21 | // LoadEvent represents WebKit load events. | |
| 21 | 22 | type LoadEvent int | |
| @@ -295,7 +296,11 @@ func (wv *WebView) LoadHTML(ctx context.Context, content, baseURI string) error | |||
| 295 | 296 | if wv.destroyed.Load() { | |
| 296 | 297 | return fmt.Errorf("webview %d is destroyed", wv.id) | |
| 297 | 298 | } | |
| 298 | - | wv.inner.LoadHtml(content, baseURI) | |
| 299 | + | var baseURIPtr *string | |
| 300 | + | if baseURI != "" { | |
| 301 | + | baseURIPtr = &baseURI | |
| 302 | + | } | |
| 303 | + | wv.inner.LoadHtml(content, baseURIPtr) | |
| 299 | 304 | return nil | |
| 300 | 305 | } | |
| 301 | 306 | ||
| @@ -445,64 +450,9 @@ func (wv *WebView) Destroy() { | |||
| 445 | 450 | } | |
| 446 | 451 | ||
| 447 | 452 | // RunJavaScript executes script in the specified world (empty for main world). | |
| 448 | - | // It waits for completion and reports JS exceptions through the returned error. | |
| 449 | - | // WARNING: Do not call from GTK signal handlers - use RunJavaScriptAsync instead. | |
| 450 | - | func (wv *WebView) RunJavaScript(ctx context.Context, script, worldName string) error { | |
| 451 | - | if wv.destroyed.Load() { | |
| 452 | - | return fmt.Errorf("webview %d is destroyed", wv.id) | |
| 453 | - | } | |
| 454 | - | if ctx == nil { | |
| 455 | - | ctx = context.Background() | |
| 456 | - | } | |
| 457 | - | ||
| 458 | - | log := logging.FromContext(ctx) | |
| 459 | - | ||
| 460 | - | done := make(chan struct{}) | |
| 461 | - | var execErr error | |
| 462 | - | ||
| 463 | - | cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) { | |
| 464 | - | defer close(done) | |
| 465 | - | ||
| 466 | - | if resPtr == 0 { | |
| 467 | - | execErr = errors.New("nil async result") | |
| 468 | - | return | |
| 469 | - | } | |
| 470 | - | ||
| 471 | - | res := &gio.AsyncResultBase{Ptr: resPtr} | |
| 472 | - | value, err := wv.inner.EvaluateJavascriptFinish(res) | |
| 473 | - | if err != nil { | |
| 474 | - | execErr = err | |
| 475 | - | return | |
| 476 | - | } | |
| 477 | - | ||
| 478 | - | if value != nil { | |
| 479 | - | if jscCtx := value.GetContext(); jscCtx != nil { | |
| 480 | - | if exc := jscCtx.GetException(); exc != nil { | |
| 481 | - | execErr = fmt.Errorf("javascript exception: %s", exc.GetMessage()) | |
| 482 | - | } | |
| 483 | - | } | |
| 484 | - | } | |
| 485 | - | }) | |
| 486 | - | ||
| 487 | - | wv.inner.EvaluateJavascript(script, -1, worldName, "", nil, &cb, 0) | |
| 488 | - | ||
| 489 | - | select { | |
| 490 | - | case <-done: | |
| 491 | - | if execErr != nil { | |
| 492 | - | log.Warn(). | |
| 493 | - | Err(execErr). | |
| 494 | - | Uint64("webview_id", uint64(wv.id)). | |
| 495 | - | Msg("javascript execution failed") | |
| 496 | - | } | |
| 497 | - | return execErr | |
| 498 | - | case <-ctx.Done(): | |
| 499 | - | return ctx.Err() | |
| 500 | - | } | |
| 501 | - | } | |
| 502 | - | ||
| 503 | - | // RunJavaScriptAsync executes script without waiting for completion. | |
| 504 | - | // Safe to call from GTK signal handlers. Errors are logged but not returned. | |
| 505 | - | func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName string) { | |
| 453 | + | // This is fire-and-forget: it does not block and errors are logged asynchronously. | |
| 454 | + | // Safe to call from any context including GTK signal handlers. | |
| 455 | + | func (wv *WebView) RunJavaScript(ctx context.Context, script, worldName string) { | |
| 506 | 456 | if wv.destroyed.Load() { | |
| 507 | 457 | return | |
| 508 | 458 | } | |
| @@ -514,14 +464,14 @@ func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName str | |||
| 514 | 464 | ||
| 515 | 465 | cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) { | |
| 516 | 466 | if resPtr == 0 { | |
| 517 | - | log.Warn().Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScriptAsync: nil async result") | |
| 467 | + | log.Warn().Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScript: nil async result") | |
| 518 | 468 | return | |
| 519 | 469 | } | |
| 520 | 470 | ||
| 521 | 471 | res := &gio.AsyncResultBase{Ptr: resPtr} | |
| 522 | 472 | value, err := wv.inner.EvaluateJavascriptFinish(res) | |
| 523 | 473 | if err != nil { | |
| 524 | - | log.Warn().Err(err).Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScriptAsync: failed") | |
| 474 | + | log.Warn().Err(err).Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScript: failed") | |
| 525 | 475 | return | |
| 526 | 476 | } | |
| 527 | 477 | ||
| @@ -531,7 +481,7 @@ func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName str | |||
| 531 | 481 | log.Warn(). | |
| 532 | 482 | Str("exception", exc.GetMessage()). | |
| 533 | 483 | Uint64("webview_id", uint64(wv.id)). | |
| 534 | - | Msg("RunJavaScriptAsync: JS exception") | |
| 484 | + | Msg("RunJavaScript: JS exception") | |
| 535 | 485 | } | |
| 536 | 486 | } | |
| 537 | 487 | } | |
| @@ -542,7 +492,13 @@ func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName str | |||
| 542 | 492 | wv.asyncCallbacks = append(wv.asyncCallbacks, cb) | |
| 543 | 493 | wv.mu.Unlock() | |
| 544 | 494 | ||
| 545 | - | wv.inner.EvaluateJavascript(script, -1, worldName, "", nil, &cb, 0) | |
| 495 | + | // worldName: nil for main world, &worldName for specific world | |
| 496 | + | // sourceUri: nil (not used) | |
| 497 | + | var worldNamePtr *string | |
| 498 | + | if worldName != "" { | |
| 499 | + | worldNamePtr = &worldName | |
| 500 | + | } | |
| 501 | + | wv.inner.EvaluateJavascript(script, -1, worldNamePtr, nil, nil, &cb, 0) | |
| 546 | 502 | } | |
| 547 | 503 | ||
| 548 | 504 | // AttachFrontend injects scripts/styles and wires the message router once per WebView. | |
| @@ -556,14 +512,22 @@ func (wv *WebView) AttachFrontend(ctx context.Context, injector *ContentInjector | |||
| 556 | 512 | Uint64("webview_id", uint64(wv.id)). | |
| 557 | 513 | Logger() | |
| 558 | 514 | ||
| 515 | + | log.Debug(). | |
| 516 | + | Bool("injector_nil", injector == nil). | |
| 517 | + | Bool("router_nil", router == nil). | |
| 518 | + | Msg("AttachFrontend called") | |
| 519 | + | ||
| 559 | 520 | if wv.destroyed.Load() { | |
| 521 | + | log.Debug().Msg("AttachFrontend: webview is destroyed") | |
| 560 | 522 | return fmt.Errorf("webview %d is destroyed", wv.id) | |
| 561 | 523 | } | |
| 562 | 524 | if injector == nil && router == nil { | |
| 525 | + | log.Debug().Msg("AttachFrontend: both injector and router are nil, skipping") | |
| 563 | 526 | return nil | |
| 564 | 527 | } | |
| 565 | 528 | ||
| 566 | 529 | if !wv.frontendAttached.CompareAndSwap(false, true) { | |
| 530 | + | log.Debug().Msg("AttachFrontend: already attached, skipping") | |
| 567 | 531 | return nil // already attached | |
| 568 | 532 | } | |
| 569 | 533 | ||
| @@ -576,17 +540,20 @@ func (wv *WebView) AttachFrontend(ctx context.Context, injector *ContentInjector | |||
| 576 | 540 | } | |
| 577 | 541 | }() | |
| 578 | 542 | ||
| 579 | - | if injector != nil { | |
| 580 | - | injector.InjectScripts(ctx, wv.ucm, wv.id) | |
| 581 | - | injector.InjectStyles(ctx, wv.ucm) | |
| 582 | - | } | |
| 583 | - | ||
| 584 | 543 | if router != nil { | |
| 544 | + | log.Debug().Msg("AttachFrontend: setting up message router") | |
| 585 | 545 | if _, err := router.SetupMessageHandler(wv.ucm, ScriptWorldName); err != nil { | |
| 586 | 546 | attachErr = fmt.Errorf("setup message router: %w", err) | |
| 587 | 547 | log.Warn().Err(err).Msg("failed to attach message router") | |
| 588 | 548 | return attachErr | |
| 589 | 549 | } | |
| 550 | + | log.Debug().Msg("AttachFrontend: message router setup complete") | |
| 551 | + | } | |
| 552 | + | ||
| 553 | + | if injector != nil { | |
| 554 | + | log.Debug().Msg("AttachFrontend: injecting scripts and styles") | |
| 555 | + | injector.InjectScripts(ctx, wv.ucm, wv.id) | |
| 556 | + | injector.InjectStyles(ctx, wv.ucm) | |
| 590 | 557 | } | |
| 591 | 558 | ||
| 592 | 559 | log.Debug().Msg("frontend assets attached to webview") | |
| @@ -2,11 +2,11 @@ package webkit | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | - | "errors" | |||
| 6 | "fmt" | 5 | "fmt" | ||
| 7 | "sync" | 6 | "sync" | ||
| 8 | "sync/atomic" | 7 | "sync/atomic" | ||
| 9 | 8 | ||||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | "github.com/bnema/dumber/internal/logging" | 10 | "github.com/bnema/dumber/internal/logging" | ||
| 11 | "github.com/bnema/puregotk-webkit/webkit" | 11 | "github.com/bnema/puregotk-webkit/webkit" | ||
| 12 | "github.com/jwijenbergh/puregotk/v4/gio" | 12 | "github.com/jwijenbergh/puregotk/v4/gio" | ||
| @@ -14,8 +14,9 @@ import ( | |||||
| 14 | "github.com/rs/zerolog" | 14 | "github.com/rs/zerolog" | ||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | - | // WebViewID uniquely identifies a WebView instance. | 17 | + | // WebViewID is an alias to port.WebViewID for clean architecture compliance. |
| 18 | - | type WebViewID uint64 | 18 | + | // Infrastructure layer uses the type defined in the application port. |
| 19 | + | type WebViewID = port.WebViewID | |||
| 19 | 20 | ||||
| 20 | // LoadEvent represents WebKit load events. | 21 | // LoadEvent represents WebKit load events. | ||
| 21 | type LoadEvent int | 22 | type LoadEvent int | ||
| @@ -295,7 +296,11 @@ func (wv *WebView) LoadHTML(ctx context.Context, content, baseURI string) error | |||||
| 295 | if wv.destroyed.Load() { | 296 | if wv.destroyed.Load() { | ||
| 296 | return fmt.Errorf("webview %d is destroyed", wv.id) | 297 | return fmt.Errorf("webview %d is destroyed", wv.id) | ||
| 297 | } | 298 | } | ||
| 298 | - | wv.inner.LoadHtml(content, baseURI) | 299 | + | var baseURIPtr *string |
| 300 | + | if baseURI != "" { | |||
| 301 | + | baseURIPtr = &baseURI | |||
| 302 | + | } | |||
| 303 | + | wv.inner.LoadHtml(content, baseURIPtr) | |||
| 299 | return nil | 304 | return nil | ||
| 300 | } | 305 | } | ||
| 301 | 306 | ||||
| @@ -445,64 +450,9 @@ func (wv *WebView) Destroy() { | |||||
| 445 | } | 450 | } | ||
| 446 | 451 | ||||
| 447 | // RunJavaScript executes script in the specified world (empty for main world). | 452 | // RunJavaScript executes script in the specified world (empty for main world). | ||
| 448 | - | // It waits for completion and reports JS exceptions through the returned error. | 453 | + | // This is fire-and-forget: it does not block and errors are logged asynchronously. |
| 449 | - | // WARNING: Do not call from GTK signal handlers - use RunJavaScriptAsync instead. | 454 | + | // Safe to call from any context including GTK signal handlers. |
| 450 | - | func (wv *WebView) RunJavaScript(ctx context.Context, script, worldName string) error { | 455 | + | func (wv *WebView) RunJavaScript(ctx context.Context, script, worldName string) { |
| 451 | - | if wv.destroyed.Load() { | |||
| 452 | - | return fmt.Errorf("webview %d is destroyed", wv.id) | |||
| 453 | - | } | |||
| 454 | - | if ctx == nil { | |||
| 455 | - | ctx = context.Background() | |||
| 456 | - | } | |||
| 457 | - | ||||
| 458 | - | log := logging.FromContext(ctx) | |||
| 459 | - | ||||
| 460 | - | done := make(chan struct{}) | |||
| 461 | - | var execErr error | |||
| 462 | - | ||||
| 463 | - | cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) { | |||
| 464 | - | defer close(done) | |||
| 465 | - | ||||
| 466 | - | if resPtr == 0 { | |||
| 467 | - | execErr = errors.New("nil async result") | |||
| 468 | - | return | |||
| 469 | - | } | |||
| 470 | - | ||||
| 471 | - | res := &gio.AsyncResultBase{Ptr: resPtr} | |||
| 472 | - | value, err := wv.inner.EvaluateJavascriptFinish(res) | |||
| 473 | - | if err != nil { | |||
| 474 | - | execErr = err | |||
| 475 | - | return | |||
| 476 | - | } | |||
| 477 | - | ||||
| 478 | - | if value != nil { | |||
| 479 | - | if jscCtx := value.GetContext(); jscCtx != nil { | |||
| 480 | - | if exc := jscCtx.GetException(); exc != nil { | |||
| 481 | - | execErr = fmt.Errorf("javascript exception: %s", exc.GetMessage()) | |||
| 482 | - | } | |||
| 483 | - | } | |||
| 484 | - | } | |||
| 485 | - | }) | |||
| 486 | - | ||||
| 487 | - | wv.inner.EvaluateJavascript(script, -1, worldName, "", nil, &cb, 0) | |||
| 488 | - | ||||
| 489 | - | select { | |||
| 490 | - | case <-done: | |||
| 491 | - | if execErr != nil { | |||
| 492 | - | log.Warn(). | |||
| 493 | - | Err(execErr). | |||
| 494 | - | Uint64("webview_id", uint64(wv.id)). | |||
| 495 | - | Msg("javascript execution failed") | |||
| 496 | - | } | |||
| 497 | - | return execErr | |||
| 498 | - | case <-ctx.Done(): | |||
| 499 | - | return ctx.Err() | |||
| 500 | - | } | |||
| 501 | - | } | |||
| 502 | - | ||||
| 503 | - | // RunJavaScriptAsync executes script without waiting for completion. | |||
| 504 | - | // Safe to call from GTK signal handlers. Errors are logged but not returned. | |||
| 505 | - | func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName string) { | |||
| 506 | if wv.destroyed.Load() { | 456 | if wv.destroyed.Load() { | ||
| 507 | return | 457 | return | ||
| 508 | } | 458 | } | ||
| @@ -514,14 +464,14 @@ func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName str | |||||
| 514 | 464 | ||||
| 515 | cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) { | 465 | cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) { | ||
| 516 | if resPtr == 0 { | 466 | if resPtr == 0 { | ||
| 517 | - | log.Warn().Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScriptAsync: nil async result") | 467 | + | log.Warn().Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScript: nil async result") |
| 518 | return | 468 | return | ||
| 519 | } | 469 | } | ||
| 520 | 470 | ||||
| 521 | res := &gio.AsyncResultBase{Ptr: resPtr} | 471 | res := &gio.AsyncResultBase{Ptr: resPtr} | ||
| 522 | value, err := wv.inner.EvaluateJavascriptFinish(res) | 472 | value, err := wv.inner.EvaluateJavascriptFinish(res) | ||
| 523 | if err != nil { | 473 | if err != nil { | ||
| 524 | - | log.Warn().Err(err).Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScriptAsync: failed") | 474 | + | log.Warn().Err(err).Uint64("webview_id", uint64(wv.id)).Msg("RunJavaScript: failed") |
| 525 | return | 475 | return | ||
| 526 | } | 476 | } | ||
| 527 | 477 | ||||
| @@ -531,7 +481,7 @@ func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName str | |||||
| 531 | log.Warn(). | 481 | log.Warn(). | ||
| 532 | Str("exception", exc.GetMessage()). | 482 | Str("exception", exc.GetMessage()). | ||
| 533 | Uint64("webview_id", uint64(wv.id)). | 483 | Uint64("webview_id", uint64(wv.id)). | ||
| 534 | - | Msg("RunJavaScriptAsync: JS exception") | 484 | + | Msg("RunJavaScript: JS exception") |
| 535 | } | 485 | } | ||
| 536 | } | 486 | } | ||
| 537 | } | 487 | } | ||
| @@ -542,7 +492,13 @@ func (wv *WebView) RunJavaScriptAsync(ctx context.Context, script, worldName str | |||||
| 542 | wv.asyncCallbacks = append(wv.asyncCallbacks, cb) | 492 | wv.asyncCallbacks = append(wv.asyncCallbacks, cb) | ||
| 543 | wv.mu.Unlock() | 493 | wv.mu.Unlock() | ||
| 544 | 494 | ||||
| 545 | - | wv.inner.EvaluateJavascript(script, -1, worldName, "", nil, &cb, 0) | 495 | + | // worldName: nil for main world, &worldName for specific world |
| 496 | + | // sourceUri: nil (not used) | |||
| 497 | + | var worldNamePtr *string | |||
| 498 | + | if worldName != "" { | |||
| 499 | + | worldNamePtr = &worldName | |||
| 500 | + | } | |||
| 501 | + | wv.inner.EvaluateJavascript(script, -1, worldNamePtr, nil, nil, &cb, 0) | |||
| 546 | } | 502 | } | ||
| 547 | 503 | ||||
| 548 | // AttachFrontend injects scripts/styles and wires the message router once per WebView. | 504 | // AttachFrontend injects scripts/styles and wires the message router once per WebView. | ||
| @@ -556,14 +512,22 @@ func (wv *WebView) AttachFrontend(ctx context.Context, injector *ContentInjector | |||||
| 556 | Uint64("webview_id", uint64(wv.id)). | 512 | Uint64("webview_id", uint64(wv.id)). | ||
| 557 | Logger() | 513 | Logger() | ||
| 558 | 514 | ||||
| 515 | + | log.Debug(). | |||
| 516 | + | Bool("injector_nil", injector == nil). | |||
| 517 | + | Bool("router_nil", router == nil). | |||
| 518 | + | Msg("AttachFrontend called") | |||
| 519 | + | ||||
| 559 | if wv.destroyed.Load() { | 520 | if wv.destroyed.Load() { | ||
| 521 | + | log.Debug().Msg("AttachFrontend: webview is destroyed") | |||
| 560 | return fmt.Errorf("webview %d is destroyed", wv.id) | 522 | return fmt.Errorf("webview %d is destroyed", wv.id) | ||
| 561 | } | 523 | } | ||
| 562 | if injector == nil && router == nil { | 524 | if injector == nil && router == nil { | ||
| 525 | + | log.Debug().Msg("AttachFrontend: both injector and router are nil, skipping") | |||
| 563 | return nil | 526 | return nil | ||
| 564 | } | 527 | } | ||
| 565 | 528 | ||||
| 566 | if !wv.frontendAttached.CompareAndSwap(false, true) { | 529 | if !wv.frontendAttached.CompareAndSwap(false, true) { | ||
| 530 | + | log.Debug().Msg("AttachFrontend: already attached, skipping") | |||
| 567 | return nil // already attached | 531 | return nil // already attached | ||
| 568 | } | 532 | } | ||
| 569 | 533 | ||||
| @@ -576,17 +540,20 @@ func (wv *WebView) AttachFrontend(ctx context.Context, injector *ContentInjector | |||||
| 576 | } | 540 | } | ||
| 577 | }() | 541 | }() | ||
| 578 | 542 | ||||
| 579 | - | if injector != nil { | |||
| 580 | - | injector.InjectScripts(ctx, wv.ucm, wv.id) | |||
| 581 | - | injector.InjectStyles(ctx, wv.ucm) | |||
| 582 | - | } | |||
| 583 | - | ||||
| 584 | if router != nil { | 543 | if router != nil { | ||
| 544 | + | log.Debug().Msg("AttachFrontend: setting up message router") | |||
| 585 | if _, err := router.SetupMessageHandler(wv.ucm, ScriptWorldName); err != nil { | 545 | if _, err := router.SetupMessageHandler(wv.ucm, ScriptWorldName); err != nil { | ||
| 586 | attachErr = fmt.Errorf("setup message router: %w", err) | 546 | attachErr = fmt.Errorf("setup message router: %w", err) | ||
| 587 | log.Warn().Err(err).Msg("failed to attach message router") | 547 | log.Warn().Err(err).Msg("failed to attach message router") | ||
| 588 | return attachErr | 548 | return attachErr | ||
| 589 | } | 549 | } | ||
| 550 | + | log.Debug().Msg("AttachFrontend: message router setup complete") | |||
| 551 | + | } | |||
| 552 | + | ||||
| 553 | + | if injector != nil { | |||
| 554 | + | log.Debug().Msg("AttachFrontend: injecting scripts and styles") | |||
| 555 | + | injector.InjectScripts(ctx, wv.ucm, wv.id) | |||
| 556 | + | injector.InjectStyles(ctx, wv.ucm) | |||
| 590 | } | 557 | } | ||
| 591 | 558 | ||||
| 592 | log.Debug().Msg("frontend assets attached to webview") | 559 | log.Debug().Msg("frontend assets attached to webview") | ||