fix: address copilot and coderabbit review follow-ups
- commit
- c1cf8b729a85f0a9a451842ffc50e3b8c3046b40
- author
- brice · · 2026-02-07 09:05 UTC
- parent
- d34c856
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +93 -43
M CHANGELOG.md +3 -0
| @@ -20,6 +20,9 @@ All notable changes to this project will be documented in this file. | |||
| 20 | 20 | ### Fixed | |
| 21 | 21 | - **Inline ghost suggestion stability**: Reworked ghostwriter flow to keep suggestions selected (not auto-committed), with stronger token guards and stale-callback protection. | |
| 22 | 22 | - **GTK thread-safety in omnibox**: Removed unsafe off-main-thread widget reads in async flows. | |
| 23 | + | - **Review follow-ups (Copilot/CodeRabbit)**: Fixed custom-scheme GET handling for empty WebKit methods, synchronized pool close/send paths, aligned WebKit signal ID storage types, and marshalled OAuth parent refresh reloads back to the GTK main loop. | |
| 24 | + | - **Persistence/update safety edges**: Enforced positive visit-count deltas in SQLite repos, removed redundant filter cache directory creation, canceled pending snapshot timer on `SetReady`, and made updater retry-loop exhaustion explicit. | |
| 25 | + | - **Logging/dedup consistency**: Unified abrupt-session marker stat warnings under structured session logging and reduced favicon warning dedup growth by using a global cache-dir warning key plus callback coverage tests. | |
| 23 | 26 | - **Omnibox input/ghost correctness**: Enforced minimum typed input before ghost suggestions, trimmed leading spaces before URL interpretation, deferred omnibox auto-open until layout is ready, and fixed width/layout edge-cases. | |
| 24 | 27 | - **History/autocomplete edge-cases**: Normalized FTS queries containing slashes and capped `about:blank` visit-count inflation. | |
| 25 | 28 | - **Navigation/history robustness**: Added pane history cleanup hooks, batched/capped visit increment fallback, and idempotent `NavigateUseCase.Close()`. | |
| @@ -20,6 +20,9 @@ All notable changes to this project will be documented in this file. | |||||
| 20 | ### Fixed | 20 | ### Fixed | ||
| 21 | - **Inline ghost suggestion stability**: Reworked ghostwriter flow to keep suggestions selected (not auto-committed), with stronger token guards and stale-callback protection. | 21 | - **Inline ghost suggestion stability**: Reworked ghostwriter flow to keep suggestions selected (not auto-committed), with stronger token guards and stale-callback protection. | ||
| 22 | - **GTK thread-safety in omnibox**: Removed unsafe off-main-thread widget reads in async flows. | 22 | - **GTK thread-safety in omnibox**: Removed unsafe off-main-thread widget reads in async flows. | ||
| 23 | + | - **Review follow-ups (Copilot/CodeRabbit)**: Fixed custom-scheme GET handling for empty WebKit methods, synchronized pool close/send paths, aligned WebKit signal ID storage types, and marshalled OAuth parent refresh reloads back to the GTK main loop. | |||
| 24 | + | - **Persistence/update safety edges**: Enforced positive visit-count deltas in SQLite repos, removed redundant filter cache directory creation, canceled pending snapshot timer on `SetReady`, and made updater retry-loop exhaustion explicit. | |||
| 25 | + | - **Logging/dedup consistency**: Unified abrupt-session marker stat warnings under structured session logging and reduced favicon warning dedup growth by using a global cache-dir warning key plus callback coverage tests. | |||
| 23 | - **Omnibox input/ghost correctness**: Enforced minimum typed input before ghost suggestions, trimmed leading spaces before URL interpretation, deferred omnibox auto-open until layout is ready, and fixed width/layout edge-cases. | 26 | - **Omnibox input/ghost correctness**: Enforced minimum typed input before ghost suggestions, trimmed leading spaces before URL interpretation, deferred omnibox auto-open until layout is ready, and fixed width/layout edge-cases. | ||
| 24 | - **History/autocomplete edge-cases**: Normalized FTS queries containing slashes and capped `about:blank` visit-count inflation. | 27 | - **History/autocomplete edge-cases**: Normalized FTS queries containing slashes and capped `about:blank` visit-count inflation. | ||
| 25 | - **Navigation/history robustness**: Added pane history cleanup hooks, batched/capped visit increment fallback, and idempotent `NavigateUseCase.Close()`. | 28 | - **Navigation/history robustness**: Added pane history cleanup hooks, batched/capped visit increment fallback, and idempotent `NavigateUseCase.Close()`. | ||
M internal/application/port/webkit_context.go +2 -0
| @@ -52,9 +52,11 @@ type WebKitContextOptions struct { | |||
| 52 | 52 | CacheDir string | |
| 53 | 53 | ||
| 54 | 54 | // CookiePolicy controls cookie acceptance behavior. | |
| 55 | + | // Empty value means runtime defaults are used. | |
| 55 | 56 | CookiePolicy WebKitCookiePolicy | |
| 56 | 57 | ||
| 57 | 58 | // ITPEnabled controls Intelligent Tracking Prevention for NetworkSession. | |
| 59 | + | // False means ITP remains disabled unless enabled explicitly. | |
| 58 | 60 | ITPEnabled bool | |
| 59 | 61 | ||
| 60 | 62 | // WebProcessMemory configures memory pressure for web processes. | |
| @@ -52,9 +52,11 @@ type WebKitContextOptions struct { | |||||
| 52 | CacheDir string | 52 | CacheDir string | ||
| 53 | 53 | ||||
| 54 | // CookiePolicy controls cookie acceptance behavior. | 54 | // CookiePolicy controls cookie acceptance behavior. | ||
| 55 | + | // Empty value means runtime defaults are used. | |||
| 55 | CookiePolicy WebKitCookiePolicy | 56 | CookiePolicy WebKitCookiePolicy | ||
| 56 | 57 | ||||
| 57 | // ITPEnabled controls Intelligent Tracking Prevention for NetworkSession. | 58 | // ITPEnabled controls Intelligent Tracking Prevention for NetworkSession. | ||
| 59 | + | // False means ITP remains disabled unless enabled explicitly. | |||
| 58 | ITPEnabled bool | 60 | ITPEnabled bool | ||
| 59 | 61 | ||||
| 60 | // WebProcessMemory configures memory pressure for web processes. | 62 | // WebProcessMemory configures memory pressure for web processes. | ||
M internal/bootstrap/session.go +16 -5
| @@ -4,7 +4,6 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | - | stdlog "log" | |
| 8 | 7 | "os" | |
| 9 | 8 | "path/filepath" | |
| 10 | 9 | "strings" | |
| @@ -99,7 +98,7 @@ func StartBrowserSession( | |||
| 99 | 98 | persistFn := func(persistCtx context.Context) error { | |
| 100 | 99 | persistMu.Do(func() { | |
| 101 | 100 | if lockDir != "" { | |
| 102 | - | abruptSessions, abruptErr := markAbruptExits(lockDir, time.Now().UTC()) | |
| 101 | + | abruptSessions, abruptErr := markAbruptExits(lockDir, time.Now().UTC(), &logger) | |
| 103 | 102 | if abruptErr != nil { | |
| 104 | 103 | logger.Warn().Err(abruptErr).Msg("failed to check abrupt exit markers") | |
| 105 | 104 | } else { | |
| @@ -333,7 +332,7 @@ func writeShutdownMarker(lockDir, sessionID string, endedAt time.Time) error { | |||
| 333 | 332 | return nil | |
| 334 | 333 | } | |
| 335 | 334 | ||
| 336 | - | func markAbruptExits(lockDir string, detectedAt time.Time) ([]string, error) { | |
| 335 | + | func markAbruptExits(lockDir string, detectedAt time.Time, logger *zerolog.Logger) ([]string, error) { | |
| 337 | 336 | if lockDir == "" { | |
| 338 | 337 | return nil, nil | |
| 339 | 338 | } | |
| @@ -357,14 +356,26 @@ func markAbruptExits(lockDir string, detectedAt time.Time) ([]string, error) { | |||
| 357 | 356 | if _, err := os.Stat(shutdownPath); err == nil { | |
| 358 | 357 | continue | |
| 359 | 358 | } else if !os.IsNotExist(err) { | |
| 360 | - | stdlog.Printf("markAbruptExits: stat failed session_id=%s path=%s err=%v", sessionID, shutdownPath, err) | |
| 359 | + | if logger != nil { | |
| 360 | + | logger.Warn(). | |
| 361 | + | Err(err). | |
| 362 | + | Str("session_id", sessionID). | |
| 363 | + | Str("path", shutdownPath). | |
| 364 | + | Msg("markAbruptExits: stat failed") | |
| 365 | + | } | |
| 361 | 366 | continue | |
| 362 | 367 | } | |
| 363 | 368 | abruptPath := abruptMarkerPath(lockDir, sessionID) | |
| 364 | 369 | if _, err := os.Stat(abruptPath); err == nil { | |
| 365 | 370 | continue | |
| 366 | 371 | } else if !os.IsNotExist(err) { | |
| 367 | - | stdlog.Printf("markAbruptExits: stat failed session_id=%s path=%s err=%v", sessionID, abruptPath, err) | |
| 372 | + | if logger != nil { | |
| 373 | + | logger.Warn(). | |
| 374 | + | Err(err). | |
| 375 | + | Str("session_id", sessionID). | |
| 376 | + | Str("path", abruptPath). | |
| 377 | + | Msg("markAbruptExits: stat failed") | |
| 378 | + | } | |
| 368 | 379 | continue | |
| 369 | 380 | } | |
| 370 | 381 | ||
| @@ -4,7 +4,6 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | - | stdlog "log" | |||
| 8 | "os" | 7 | "os" | ||
| 9 | "path/filepath" | 8 | "path/filepath" | ||
| 10 | "strings" | 9 | "strings" | ||
| @@ -99,7 +98,7 @@ func StartBrowserSession( | |||||
| 99 | persistFn := func(persistCtx context.Context) error { | 98 | persistFn := func(persistCtx context.Context) error { | ||
| 100 | persistMu.Do(func() { | 99 | persistMu.Do(func() { | ||
| 101 | if lockDir != "" { | 100 | if lockDir != "" { | ||
| 102 | - | abruptSessions, abruptErr := markAbruptExits(lockDir, time.Now().UTC()) | 101 | + | abruptSessions, abruptErr := markAbruptExits(lockDir, time.Now().UTC(), &logger) |
| 103 | if abruptErr != nil { | 102 | if abruptErr != nil { | ||
| 104 | logger.Warn().Err(abruptErr).Msg("failed to check abrupt exit markers") | 103 | logger.Warn().Err(abruptErr).Msg("failed to check abrupt exit markers") | ||
| 105 | } else { | 104 | } else { | ||
| @@ -333,7 +332,7 @@ func writeShutdownMarker(lockDir, sessionID string, endedAt time.Time) error { | |||||
| 333 | return nil | 332 | return nil | ||
| 334 | } | 333 | } | ||
| 335 | 334 | ||||
| 336 | - | func markAbruptExits(lockDir string, detectedAt time.Time) ([]string, error) { | 335 | + | func markAbruptExits(lockDir string, detectedAt time.Time, logger *zerolog.Logger) ([]string, error) { |
| 337 | if lockDir == "" { | 336 | if lockDir == "" { | ||
| 338 | return nil, nil | 337 | return nil, nil | ||
| 339 | } | 338 | } | ||
| @@ -357,14 +356,26 @@ func markAbruptExits(lockDir string, detectedAt time.Time) ([]string, error) { | |||||
| 357 | if _, err := os.Stat(shutdownPath); err == nil { | 356 | if _, err := os.Stat(shutdownPath); err == nil { | ||
| 358 | continue | 357 | continue | ||
| 359 | } else if !os.IsNotExist(err) { | 358 | } else if !os.IsNotExist(err) { | ||
| 360 | - | stdlog.Printf("markAbruptExits: stat failed session_id=%s path=%s err=%v", sessionID, shutdownPath, err) | 359 | + | if logger != nil { |
| 360 | + | logger.Warn(). | |||
| 361 | + | Err(err). | |||
| 362 | + | Str("session_id", sessionID). | |||
| 363 | + | Str("path", shutdownPath). | |||
| 364 | + | Msg("markAbruptExits: stat failed") | |||
| 365 | + | } | |||
| 361 | continue | 366 | continue | ||
| 362 | } | 367 | } | ||
| 363 | abruptPath := abruptMarkerPath(lockDir, sessionID) | 368 | abruptPath := abruptMarkerPath(lockDir, sessionID) | ||
| 364 | if _, err := os.Stat(abruptPath); err == nil { | 369 | if _, err := os.Stat(abruptPath); err == nil { | ||
| 365 | continue | 370 | continue | ||
| 366 | } else if !os.IsNotExist(err) { | 371 | } else if !os.IsNotExist(err) { | ||
| 367 | - | stdlog.Printf("markAbruptExits: stat failed session_id=%s path=%s err=%v", sessionID, abruptPath, err) | 372 | + | if logger != nil { |
| 373 | + | logger.Warn(). | |||
| 374 | + | Err(err). | |||
| 375 | + | Str("session_id", sessionID). | |||
| 376 | + | Str("path", abruptPath). | |||
| 377 | + | Msg("markAbruptExits: stat failed") | |||
| 378 | + | } | |||
| 368 | continue | 379 | continue | ||
| 369 | } | 380 | } | ||
| 370 | 381 | ||||
M internal/bootstrap/session_markers_test.go +3 -3
| @@ -39,7 +39,7 @@ func TestSessionMarkers_MarkAbruptExits(t *testing.T) { | |||
| 39 | 39 | require.NoError(t, writeStartupMarker(lockDir, "clean-b", detectedAt.Add(-4*time.Minute))) | |
| 40 | 40 | require.NoError(t, writeShutdownMarker(lockDir, "clean-b", detectedAt.Add(-1*time.Minute))) | |
| 41 | 41 | ||
| 42 | - | abruptSessions, err := markAbruptExits(lockDir, detectedAt) | |
| 42 | + | abruptSessions, err := markAbruptExits(lockDir, detectedAt, nil) | |
| 43 | 43 | require.NoError(t, err) | |
| 44 | 44 | assert.Equal(t, []string{"abrupt-a"}, abruptSessions) | |
| 45 | 45 | ||
| @@ -55,11 +55,11 @@ func TestSessionMarkers_MarkAbruptExitsIdempotent(t *testing.T) { | |||
| 55 | 55 | ||
| 56 | 56 | require.NoError(t, writeStartupMarker(lockDir, "abrupt-c", detectedAt.Add(-10*time.Minute))) | |
| 57 | 57 | ||
| 58 | - | first, err := markAbruptExits(lockDir, detectedAt) | |
| 58 | + | first, err := markAbruptExits(lockDir, detectedAt, nil) | |
| 59 | 59 | require.NoError(t, err) | |
| 60 | 60 | assert.Equal(t, []string{"abrupt-c"}, first) | |
| 61 | 61 | ||
| 62 | - | second, err := markAbruptExits(lockDir, detectedAt.Add(1*time.Minute)) | |
| 62 | + | second, err := markAbruptExits(lockDir, detectedAt.Add(1*time.Minute), nil) | |
| 63 | 63 | require.NoError(t, err) | |
| 64 | 64 | assert.Empty(t, second) | |
| 65 | 65 | } | |
| @@ -39,7 +39,7 @@ func TestSessionMarkers_MarkAbruptExits(t *testing.T) { | |||||
| 39 | require.NoError(t, writeStartupMarker(lockDir, "clean-b", detectedAt.Add(-4*time.Minute))) | 39 | require.NoError(t, writeStartupMarker(lockDir, "clean-b", detectedAt.Add(-4*time.Minute))) | ||
| 40 | require.NoError(t, writeShutdownMarker(lockDir, "clean-b", detectedAt.Add(-1*time.Minute))) | 40 | require.NoError(t, writeShutdownMarker(lockDir, "clean-b", detectedAt.Add(-1*time.Minute))) | ||
| 41 | 41 | ||||
| 42 | - | abruptSessions, err := markAbruptExits(lockDir, detectedAt) | 42 | + | abruptSessions, err := markAbruptExits(lockDir, detectedAt, nil) |
| 43 | require.NoError(t, err) | 43 | require.NoError(t, err) | ||
| 44 | assert.Equal(t, []string{"abrupt-a"}, abruptSessions) | 44 | assert.Equal(t, []string{"abrupt-a"}, abruptSessions) | ||
| 45 | 45 | ||||
| @@ -55,11 +55,11 @@ func TestSessionMarkers_MarkAbruptExitsIdempotent(t *testing.T) { | |||||
| 55 | 55 | ||||
| 56 | require.NoError(t, writeStartupMarker(lockDir, "abrupt-c", detectedAt.Add(-10*time.Minute))) | 56 | require.NoError(t, writeStartupMarker(lockDir, "abrupt-c", detectedAt.Add(-10*time.Minute))) | ||
| 57 | 57 | ||||
| 58 | - | first, err := markAbruptExits(lockDir, detectedAt) | 58 | + | first, err := markAbruptExits(lockDir, detectedAt, nil) |
| 59 | require.NoError(t, err) | 59 | require.NoError(t, err) | ||
| 60 | assert.Equal(t, []string{"abrupt-c"}, first) | 60 | assert.Equal(t, []string{"abrupt-c"}, first) | ||
| 61 | 61 | ||||
| 62 | - | second, err := markAbruptExits(lockDir, detectedAt.Add(1*time.Minute)) | 62 | + | second, err := markAbruptExits(lockDir, detectedAt.Add(1*time.Minute), nil) |
| 63 | require.NoError(t, err) | 63 | require.NoError(t, err) | ||
| 64 | assert.Empty(t, second) | 64 | assert.Empty(t, second) | ||
| 65 | } | 65 | } | ||
M internal/infrastructure/filtering/downloader.go +0 -4
| @@ -164,10 +164,6 @@ func (d *Downloader) downloadFile(ctx context.Context, filename string) (string, | |||
| 164 | 164 | url := fmt.Sprintf("%s/%s", d.baseURL, filename) | |
| 165 | 165 | localPath := filepath.Join(d.cacheDir, filename) | |
| 166 | 166 | ||
| 167 | - | if err := os.MkdirAll(d.cacheDir, cacheDirPerm); err != nil { | |
| 168 | - | return "", 0, fmt.Errorf("failed to create cache dir: %w", err) | |
| 169 | - | } | |
| 170 | - | ||
| 171 | 167 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) | |
| 172 | 168 | if err != nil { | |
| 173 | 169 | return "", 0, fmt.Errorf("failed to create request: %w", err) | |
| @@ -164,10 +164,6 @@ func (d *Downloader) downloadFile(ctx context.Context, filename string) (string, | |||||
| 164 | url := fmt.Sprintf("%s/%s", d.baseURL, filename) | 164 | url := fmt.Sprintf("%s/%s", d.baseURL, filename) | ||
| 165 | localPath := filepath.Join(d.cacheDir, filename) | 165 | localPath := filepath.Join(d.cacheDir, filename) | ||
| 166 | 166 | ||||
| 167 | - | if err := os.MkdirAll(d.cacheDir, cacheDirPerm); err != nil { | |||
| 168 | - | return "", 0, fmt.Errorf("failed to create cache dir: %w", err) | |||
| 169 | - | } | |||
| 170 | - | ||||
| 171 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) | 167 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) | ||
| 172 | if err != nil { | 168 | if err != nil { | ||
| 173 | return "", 0, fmt.Errorf("failed to create request: %w", err) | 169 | return "", 0, fmt.Errorf("failed to create request: %w", err) | ||
M internal/infrastructure/persistence/sqlite/history_repo.go +1 -1
| @@ -317,7 +317,7 @@ func (r *historyRepo) IncrementVisitCountBy(ctx context.Context, url string, del | |||
| 317 | 317 | return nil | |
| 318 | 318 | } | |
| 319 | 319 | if delta <= 0 { | |
| 320 | - | delta = 1 | |
| 320 | + | return fmt.Errorf("visit delta must be > 0, got %d", delta) | |
| 321 | 321 | } | |
| 322 | 322 | return r.queries.IncrementVisitCountByDelta(ctx, sqlc.IncrementVisitCountByDeltaParams{ | |
| 323 | 323 | VisitCount: sql.NullInt64{Int64: int64(delta), Valid: true}, | |
| @@ -317,7 +317,7 @@ func (r *historyRepo) IncrementVisitCountBy(ctx context.Context, url string, del | |||||
| 317 | return nil | 317 | return nil | ||
| 318 | } | 318 | } | ||
| 319 | if delta <= 0 { | 319 | if delta <= 0 { | ||
| 320 | - | delta = 1 | 320 | + | return fmt.Errorf("visit delta must be > 0, got %d", delta) |
| 321 | } | 321 | } | ||
| 322 | return r.queries.IncrementVisitCountByDelta(ctx, sqlc.IncrementVisitCountByDeltaParams{ | 322 | return r.queries.IncrementVisitCountByDelta(ctx, sqlc.IncrementVisitCountByDeltaParams{ | ||
| 323 | VisitCount: sql.NullInt64{Int64: int64(delta), Valid: true}, | 323 | VisitCount: sql.NullInt64{Int64: int64(delta), Valid: true}, | ||
M internal/infrastructure/persistence/sqlite/lazy_repos.go +4 -0
| @@ -18,6 +18,7 @@ package sqlite | |||
| 18 | 18 | import ( | |
| 19 | 19 | "context" | |
| 20 | 20 | "database/sql" | |
| 21 | + | "fmt" | |
| 21 | 22 | "sync" | |
| 22 | 23 | "time" | |
| 23 | 24 | ||
| @@ -115,6 +116,9 @@ func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url str | |||
| 115 | 116 | } | |
| 116 | 117 | ||
| 117 | 118 | func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url string, delta int) error { | |
| 119 | + | if delta <= 0 { | |
| 120 | + | return fmt.Errorf("visit delta must be > 0, got %d", delta) | |
| 121 | + | } | |
| 118 | 122 | if err := r.init(ctx); err != nil { | |
| 119 | 123 | return err | |
| 120 | 124 | } | |
| @@ -18,6 +18,7 @@ package sqlite | |||||
| 18 | import ( | 18 | import ( | ||
| 19 | "context" | 19 | "context" | ||
| 20 | "database/sql" | 20 | "database/sql" | ||
| 21 | + | "fmt" | |||
| 21 | "sync" | 22 | "sync" | ||
| 22 | "time" | 23 | "time" | ||
| 23 | 24 | ||||
| @@ -115,6 +116,9 @@ func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url str | |||||
| 115 | } | 116 | } | ||
| 116 | 117 | ||||
| 117 | func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url string, delta int) error { | 118 | func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url string, delta int) error { | ||
| 119 | + | if delta <= 0 { | |||
| 120 | + | return fmt.Errorf("visit delta must be > 0, got %d", delta) | |||
| 121 | + | } | |||
| 118 | if err := r.init(ctx); err != nil { | 122 | if err := r.init(ctx); err != nil { | ||
| 119 | return err | 123 | return err | ||
| 120 | } | 124 | } | ||
M internal/infrastructure/snapshot/service.go +4 -0
| @@ -67,6 +67,10 @@ func (s *Service) Start(ctx context.Context) { | |||
| 67 | 67 | // to avoid FK constraint violations. | |
| 68 | 68 | func (s *Service) SetReady() { | |
| 69 | 69 | s.mu.Lock() | |
| 70 | + | if s.timer != nil { | |
| 71 | + | s.timer.Stop() | |
| 72 | + | s.timer = nil | |
| 73 | + | } | |
| 70 | 74 | s.ready = true | |
| 71 | 75 | dirty := s.dirty | |
| 72 | 76 | ctx := s.ctx | |
| @@ -67,6 +67,10 @@ func (s *Service) Start(ctx context.Context) { | |||||
| 67 | // to avoid FK constraint violations. | 67 | // to avoid FK constraint violations. | ||
| 68 | func (s *Service) SetReady() { | 68 | func (s *Service) SetReady() { | ||
| 69 | s.mu.Lock() | 69 | s.mu.Lock() | ||
| 70 | + | if s.timer != nil { | |||
| 71 | + | s.timer.Stop() | |||
| 72 | + | s.timer = nil | |||
| 73 | + | } | |||
| 70 | s.ready = true | 74 | s.ready = true | ||
| 71 | dirty := s.dirty | 75 | dirty := s.dirty | ||
| 72 | ctx := s.ctx | 76 | ctx := s.ctx | ||
M internal/infrastructure/updater/github.go +4 -3
| @@ -322,7 +322,10 @@ func doRequestWithRetryHelper( | |||
| 322 | 322 | sleep func(ctx context.Context, d time.Duration) error, | |
| 323 | 323 | randInt63 func(n int64) int64, | |
| 324 | 324 | ) (*http.Response, error) { | |
| 325 | - | for attempt := 1; attempt <= maxRetryAttempts; attempt++ { | |
| 325 | + | for attempt := 1; ; attempt++ { | |
| 326 | + | if attempt > maxRetryAttempts { | |
| 327 | + | return nil, fmt.Errorf("request failed after retries") | |
| 328 | + | } | |
| 326 | 329 | resp, err := client.Do(req) | |
| 327 | 330 | if err != nil { | |
| 328 | 331 | if !isRetryableRequestError(err) || attempt == maxRetryAttempts { | |
| @@ -343,8 +346,6 @@ func doRequestWithRetryHelper( | |||
| 343 | 346 | return nil, waitErr | |
| 344 | 347 | } | |
| 345 | 348 | } | |
| 346 | - | ||
| 347 | - | return nil, fmt.Errorf("request failed after retries") | |
| 348 | 349 | } | |
| 349 | 350 | ||
| 350 | 351 | func (g *GitHubChecker) doRequestWithRetry(ctx context.Context, req *http.Request) (*http.Response, error) { | |
| @@ -322,7 +322,10 @@ func doRequestWithRetryHelper( | |||||
| 322 | sleep func(ctx context.Context, d time.Duration) error, | 322 | sleep func(ctx context.Context, d time.Duration) error, | ||
| 323 | randInt63 func(n int64) int64, | 323 | randInt63 func(n int64) int64, | ||
| 324 | ) (*http.Response, error) { | 324 | ) (*http.Response, error) { | ||
| 325 | - | for attempt := 1; attempt <= maxRetryAttempts; attempt++ { | 325 | + | for attempt := 1; ; attempt++ { |
| 326 | + | if attempt > maxRetryAttempts { | |||
| 327 | + | return nil, fmt.Errorf("request failed after retries") | |||
| 328 | + | } | |||
| 326 | resp, err := client.Do(req) | 329 | resp, err := client.Do(req) | ||
| 327 | if err != nil { | 330 | if err != nil { | ||
| 328 | if !isRetryableRequestError(err) || attempt == maxRetryAttempts { | 331 | if !isRetryableRequestError(err) || attempt == maxRetryAttempts { | ||
| @@ -343,8 +346,6 @@ func doRequestWithRetryHelper( | |||||
| 343 | return nil, waitErr | 346 | return nil, waitErr | ||
| 344 | } | 347 | } | ||
| 345 | } | 348 | } | ||
| 346 | - | ||||
| 347 | - | return nil, fmt.Errorf("request failed after retries") | |||
| 348 | } | 349 | } | ||
| 349 | 350 | ||||
| 350 | func (g *GitHubChecker) doRequestWithRetry(ctx context.Context, req *http.Request) (*http.Response, error) { | 351 | func (g *GitHubChecker) doRequestWithRetry(ctx context.Context, req *http.Request) (*http.Response, error) { | ||
M internal/infrastructure/webkit/pool.go +21 -19
| @@ -44,12 +44,10 @@ type PoolConfig struct { | |||
| 44 | 44 | // DefaultPoolConfig returns sensible defaults for the pool. | |
| 45 | 45 | func DefaultPoolConfig() PoolConfig { | |
| 46 | 46 | return PoolConfig{ | |
| 47 | - | MinSize: 2, | |
| 48 | - | MaxSize: 8, | |
| 49 | - | PrewarmCount: 4, // Pre-create 4 WebViews for faster initial tab creation | |
| 50 | - | IdleTimeout: 5 * time.Minute, | |
| 51 | - | ReusePolicy: reusePolicyOff, | |
| 52 | - | TerminatePolicy: terminatePolicyAuto, | |
| 47 | + | MinSize: 2, | |
| 48 | + | MaxSize: 8, | |
| 49 | + | PrewarmCount: 4, // Pre-create 4 WebViews for faster initial tab creation | |
| 50 | + | IdleTimeout: 5 * time.Minute, | |
| 53 | 51 | } | |
| 54 | 52 | } | |
| 55 | 53 | ||
| @@ -66,6 +64,7 @@ type WebViewPool struct { | |||
| 66 | 64 | config PoolConfig | |
| 67 | 65 | ||
| 68 | 66 | pool chan *WebView | |
| 67 | + | poolMu sync.Mutex | |
| 69 | 68 | injector *ContentInjector | |
| 70 | 69 | router *MessageRouter | |
| 71 | 70 | filterApplier FilterApplier // Optional content filter applier | |
| @@ -299,12 +298,11 @@ func (p *WebViewPool) PrewarmFirst(ctx context.Context) error { | |||
| 299 | 298 | return err | |
| 300 | 299 | } | |
| 301 | 300 | ||
| 302 | - | select { | |
| 303 | - | case p.pool <- wv: | |
| 301 | + | if p.tryPut(ctx, wv) { | |
| 304 | 302 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed first webview for cold start") | |
| 305 | - | default: | |
| 306 | - | wv.Destroy() // Pool somehow full (shouldn't happen) | |
| 303 | + | return nil | |
| 307 | 304 | } | |
| 305 | + | wv.Destroy() // Pool somehow full (shouldn't happen) | |
| 308 | 306 | return nil | |
| 309 | 307 | } | |
| 310 | 308 | ||
| @@ -375,10 +373,9 @@ func (p *WebViewPool) PrewarmAsync(ctx context.Context, count int) { | |||
| 375 | 373 | if err != nil { | |
| 376 | 374 | log.Warn().Err(err).Msg("failed to prewarm webview") | |
| 377 | 375 | } else { | |
| 378 | - | select { | |
| 379 | - | case p.pool <- wv: | |
| 376 | + | if p.tryPut(ctx, wv) { | |
| 380 | 377 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool") | |
| 381 | - | default: | |
| 378 | + | } else { | |
| 382 | 379 | wv.Destroy() | |
| 383 | 380 | } | |
| 384 | 381 | } | |
| @@ -417,10 +414,9 @@ func (p *WebViewPool) prewarmSync(ctx context.Context, count int) { | |||
| 417 | 414 | } | |
| 418 | 415 | ||
| 419 | 416 | // Try to add to pool | |
| 420 | - | select { | |
| 421 | - | case p.pool <- wv: | |
| 417 | + | if p.tryPut(ctx, wv) { | |
| 422 | 418 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool") | |
| 423 | - | default: | |
| 419 | + | } else { | |
| 424 | 420 | // Pool full, destroy | |
| 425 | 421 | wv.Destroy() | |
| 426 | 422 | } | |
| @@ -468,10 +464,9 @@ refreshLoop: | |||
| 468 | 464 | } | |
| 469 | 465 | ||
| 470 | 466 | // Put back in pool | |
| 471 | - | select { | |
| 472 | - | case p.pool <- wv: | |
| 467 | + | if p.tryPut(ctx, wv) { | |
| 473 | 468 | refreshed++ | |
| 474 | - | default: | |
| 469 | + | } else { | |
| 475 | 470 | wv.Destroy() | |
| 476 | 471 | } | |
| 477 | 472 | default: | |
| @@ -498,7 +493,9 @@ func (p *WebViewPool) Close(ctx context.Context) { | |||
| 498 | 493 | p.wg.Wait() | |
| 499 | 494 | ||
| 500 | 495 | // Drain and destroy all pooled views | |
| 496 | + | p.poolMu.Lock() | |
| 501 | 497 | close(p.pool) | |
| 498 | + | p.poolMu.Unlock() | |
| 502 | 499 | for wv := range p.pool { | |
| 503 | 500 | if wv != nil && !wv.IsDestroyed() { | |
| 504 | 501 | wv.Destroy() | |
| @@ -529,6 +526,11 @@ func (p *WebViewPool) shouldReuseOnRelease(wv *WebView) bool { | |||
| 529 | 526 | ||
| 530 | 527 | func (p *WebViewPool) tryPut(ctx context.Context, wv *WebView) bool { | |
| 531 | 528 | log := logging.FromContext(ctx) | |
| 529 | + | p.poolMu.Lock() | |
| 530 | + | defer p.poolMu.Unlock() | |
| 531 | + | if p.closed.Load() { | |
| 532 | + | return false | |
| 533 | + | } | |
| 532 | 534 | select { | |
| 533 | 535 | case p.pool <- wv: | |
| 534 | 536 | log.Debug().Uint64("id", uint64(wv.ID())).Int("pool_size", len(p.pool)).Msg("returned webview to pool") | |
| @@ -44,12 +44,10 @@ type PoolConfig struct { | |||||
| 44 | // DefaultPoolConfig returns sensible defaults for the pool. | 44 | // DefaultPoolConfig returns sensible defaults for the pool. | ||
| 45 | func DefaultPoolConfig() PoolConfig { | 45 | func DefaultPoolConfig() PoolConfig { | ||
| 46 | return PoolConfig{ | 46 | return PoolConfig{ | ||
| 47 | - | MinSize: 2, | 47 | + | MinSize: 2, |
| 48 | - | MaxSize: 8, | 48 | + | MaxSize: 8, |
| 49 | - | PrewarmCount: 4, // Pre-create 4 WebViews for faster initial tab creation | 49 | + | PrewarmCount: 4, // Pre-create 4 WebViews for faster initial tab creation |
| 50 | - | IdleTimeout: 5 * time.Minute, | 50 | + | IdleTimeout: 5 * time.Minute, |
| 51 | - | ReusePolicy: reusePolicyOff, | |||
| 52 | - | TerminatePolicy: terminatePolicyAuto, | |||
| 53 | } | 51 | } | ||
| 54 | } | 52 | } | ||
| 55 | 53 | ||||
| @@ -66,6 +64,7 @@ type WebViewPool struct { | |||||
| 66 | config PoolConfig | 64 | config PoolConfig | ||
| 67 | 65 | ||||
| 68 | pool chan *WebView | 66 | pool chan *WebView | ||
| 67 | + | poolMu sync.Mutex | |||
| 69 | injector *ContentInjector | 68 | injector *ContentInjector | ||
| 70 | router *MessageRouter | 69 | router *MessageRouter | ||
| 71 | filterApplier FilterApplier // Optional content filter applier | 70 | filterApplier FilterApplier // Optional content filter applier | ||
| @@ -299,12 +298,11 @@ func (p *WebViewPool) PrewarmFirst(ctx context.Context) error { | |||||
| 299 | return err | 298 | return err | ||
| 300 | } | 299 | } | ||
| 301 | 300 | ||||
| 302 | - | select { | 301 | + | if p.tryPut(ctx, wv) { |
| 303 | - | case p.pool <- wv: | |||
| 304 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed first webview for cold start") | 302 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed first webview for cold start") | ||
| 305 | - | default: | 303 | + | return nil |
| 306 | - | wv.Destroy() // Pool somehow full (shouldn't happen) | |||
| 307 | } | 304 | } | ||
| 305 | + | wv.Destroy() // Pool somehow full (shouldn't happen) | |||
| 308 | return nil | 306 | return nil | ||
| 309 | } | 307 | } | ||
| 310 | 308 | ||||
| @@ -375,10 +373,9 @@ func (p *WebViewPool) PrewarmAsync(ctx context.Context, count int) { | |||||
| 375 | if err != nil { | 373 | if err != nil { | ||
| 376 | log.Warn().Err(err).Msg("failed to prewarm webview") | 374 | log.Warn().Err(err).Msg("failed to prewarm webview") | ||
| 377 | } else { | 375 | } else { | ||
| 378 | - | select { | 376 | + | if p.tryPut(ctx, wv) { |
| 379 | - | case p.pool <- wv: | |||
| 380 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool") | 377 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool") | ||
| 381 | - | default: | 378 | + | } else { |
| 382 | wv.Destroy() | 379 | wv.Destroy() | ||
| 383 | } | 380 | } | ||
| 384 | } | 381 | } | ||
| @@ -417,10 +414,9 @@ func (p *WebViewPool) prewarmSync(ctx context.Context, count int) { | |||||
| 417 | } | 414 | } | ||
| 418 | 415 | ||||
| 419 | // Try to add to pool | 416 | // Try to add to pool | ||
| 420 | - | select { | 417 | + | if p.tryPut(ctx, wv) { |
| 421 | - | case p.pool <- wv: | |||
| 422 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool") | 418 | log.Debug().Uint64("id", uint64(wv.ID())).Msg("prewarmed webview added to pool") | ||
| 423 | - | default: | 419 | + | } else { |
| 424 | // Pool full, destroy | 420 | // Pool full, destroy | ||
| 425 | wv.Destroy() | 421 | wv.Destroy() | ||
| 426 | } | 422 | } | ||
| @@ -468,10 +464,9 @@ refreshLoop: | |||||
| 468 | } | 464 | } | ||
| 469 | 465 | ||||
| 470 | // Put back in pool | 466 | // Put back in pool | ||
| 471 | - | select { | 467 | + | if p.tryPut(ctx, wv) { |
| 472 | - | case p.pool <- wv: | |||
| 473 | refreshed++ | 468 | refreshed++ | ||
| 474 | - | default: | 469 | + | } else { |
| 475 | wv.Destroy() | 470 | wv.Destroy() | ||
| 476 | } | 471 | } | ||
| 477 | default: | 472 | default: | ||
| @@ -498,7 +493,9 @@ func (p *WebViewPool) Close(ctx context.Context) { | |||||
| 498 | p.wg.Wait() | 493 | p.wg.Wait() | ||
| 499 | 494 | ||||
| 500 | // Drain and destroy all pooled views | 495 | // Drain and destroy all pooled views | ||
| 496 | + | p.poolMu.Lock() | |||
| 501 | close(p.pool) | 497 | close(p.pool) | ||
| 498 | + | p.poolMu.Unlock() | |||
| 502 | for wv := range p.pool { | 499 | for wv := range p.pool { | ||
| 503 | if wv != nil && !wv.IsDestroyed() { | 500 | if wv != nil && !wv.IsDestroyed() { | ||
| 504 | wv.Destroy() | 501 | wv.Destroy() | ||
| @@ -529,6 +526,11 @@ func (p *WebViewPool) shouldReuseOnRelease(wv *WebView) bool { | |||||
| 529 | 526 | ||||
| 530 | func (p *WebViewPool) tryPut(ctx context.Context, wv *WebView) bool { | 527 | func (p *WebViewPool) tryPut(ctx context.Context, wv *WebView) bool { | ||
| 531 | log := logging.FromContext(ctx) | 528 | log := logging.FromContext(ctx) | ||
| 529 | + | p.poolMu.Lock() | |||
| 530 | + | defer p.poolMu.Unlock() | |||
| 531 | + | if p.closed.Load() { | |||
| 532 | + | return false | |||
| 533 | + | } | |||
| 532 | select { | 534 | select { | ||
| 533 | case p.pool <- wv: | 535 | case p.pool <- wv: | ||
| 534 | log.Debug().Uint64("id", uint64(wv.ID())).Int("pool_size", len(p.pool)).Msg("returned webview to pool") | 536 | log.Debug().Uint64("id", uint64(wv.ID())).Int("pool_size", len(p.pool)).Msg("returned webview to pool") | ||
M internal/infrastructure/webkit/scheme_handler.go +2 -2
| @@ -179,7 +179,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 179 | 179 | ||
| 180 | 180 | // API: Get current config (used by dumb://config) | |
| 181 | 181 | h.RegisterPage("/api/config", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | |
| 182 | - | if req.Method != httpGET { | |
| 182 | + | if req.Method != "" && req.Method != httpGET { | |
| 183 | 183 | return nil | |
| 184 | 184 | } | |
| 185 | 185 | ||
| @@ -188,7 +188,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||
| 188 | 188 | ||
| 189 | 189 | // API: Get default config (used by Reset Defaults in dumb://config) | |
| 190 | 190 | h.RegisterPage("/api/config/default", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | |
| 191 | - | if req.Method != httpGET { | |
| 191 | + | if req.Method != "" && req.Method != httpGET { | |
| 192 | 192 | return nil | |
| 193 | 193 | } | |
| 194 | 194 | ||
| @@ -179,7 +179,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||||
| 179 | 179 | ||||
| 180 | // API: Get current config (used by dumb://config) | 180 | // API: Get current config (used by dumb://config) | ||
| 181 | h.RegisterPage("/api/config", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | 181 | h.RegisterPage("/api/config", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | ||
| 182 | - | if req.Method != httpGET { | 182 | + | if req.Method != "" && req.Method != httpGET { |
| 183 | return nil | 183 | return nil | ||
| 184 | } | 184 | } | ||
| 185 | 185 | ||||
| @@ -188,7 +188,7 @@ func (h *DumbSchemeHandler) registerDefaults() { | |||||
| 188 | 188 | ||||
| 189 | // API: Get default config (used by Reset Defaults in dumb://config) | 189 | // API: Get default config (used by Reset Defaults in dumb://config) | ||
| 190 | h.RegisterPage("/api/config/default", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | 190 | h.RegisterPage("/api/config/default", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse { | ||
| 191 | - | if req.Method != httpGET { | 191 | + | if req.Method != "" && req.Method != httpGET { |
| 192 | return nil | 192 | return nil | ||
| 193 | } | 193 | } | ||
| 194 | 194 | ||||
M internal/infrastructure/webkit/webview.go +3 -3
| @@ -171,7 +171,7 @@ type WebView struct { | |||
| 171 | 171 | findControllerOnce sync.Once | |
| 172 | 172 | ||
| 173 | 173 | backForwardList *webkit.BackForwardList | |
| 174 | - | backForwardListSignalID uint | |
| 174 | + | backForwardListSignalID uintptr | |
| 175 | 175 | } | |
| 176 | 176 | ||
| 177 | 177 | type runJSErrorStat struct { | |
| @@ -882,7 +882,7 @@ func (wv *WebView) connectBackForwardListChangedSignal() { | |||
| 882 | 882 | } | |
| 883 | 883 | sigID := backForwardList.ConnectChanged(&changedCb) | |
| 884 | 884 | wv.backForwardList = backForwardList | |
| 885 | - | wv.backForwardListSignalID = sigID | |
| 885 | + | wv.backForwardListSignalID = uintptr(sigID) | |
| 886 | 886 | } | |
| 887 | 887 | ||
| 888 | 888 | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | |
| @@ -1333,7 +1333,7 @@ func (wv *WebView) DisconnectSignals() { | |||
| 1333 | 1333 | ||
| 1334 | 1334 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | |
| 1335 | 1335 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | |
| 1336 | - | gobject.SignalHandlerDisconnect(bfObj, wv.backForwardListSignalID) | |
| 1336 | + | gobject.SignalHandlerDisconnect(bfObj, uint(wv.backForwardListSignalID)) | |
| 1337 | 1337 | wv.backForwardListSignalID = 0 | |
| 1338 | 1338 | wv.backForwardList = nil | |
| 1339 | 1339 | } | |
| @@ -171,7 +171,7 @@ type WebView struct { | |||||
| 171 | findControllerOnce sync.Once | 171 | findControllerOnce sync.Once | ||
| 172 | 172 | ||||
| 173 | backForwardList *webkit.BackForwardList | 173 | backForwardList *webkit.BackForwardList | ||
| 174 | - | backForwardListSignalID uint | 174 | + | backForwardListSignalID uintptr |
| 175 | } | 175 | } | ||
| 176 | 176 | ||||
| 177 | type runJSErrorStat struct { | 177 | type runJSErrorStat struct { | ||
| @@ -882,7 +882,7 @@ func (wv *WebView) connectBackForwardListChangedSignal() { | |||||
| 882 | } | 882 | } | ||
| 883 | sigID := backForwardList.ConnectChanged(&changedCb) | 883 | sigID := backForwardList.ConnectChanged(&changedCb) | ||
| 884 | wv.backForwardList = backForwardList | 884 | wv.backForwardList = backForwardList | ||
| 885 | - | wv.backForwardListSignalID = sigID | 885 | + | wv.backForwardListSignalID = uintptr(sigID) |
| 886 | } | 886 | } | ||
| 887 | 887 | ||||
| 888 | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | 888 | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | ||
| @@ -1333,7 +1333,7 @@ func (wv *WebView) DisconnectSignals() { | |||||
| 1333 | 1333 | ||||
| 1334 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | 1334 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | ||
| 1335 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | 1335 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | ||
| 1336 | - | gobject.SignalHandlerDisconnect(bfObj, wv.backForwardListSignalID) | 1336 | + | gobject.SignalHandlerDisconnect(bfObj, uint(wv.backForwardListSignalID)) |
| 1337 | wv.backForwardListSignalID = 0 | 1337 | wv.backForwardListSignalID = 0 | ||
| 1338 | wv.backForwardList = nil | 1338 | wv.backForwardList = nil | ||
| 1339 | } | 1339 | } | ||
M internal/ui/adapter/favicon.go +1 -1
| @@ -201,7 +201,7 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin( | |||
| 201 | 201 | // Uses glib.IdleAdd to defer disk I/O until GTK main loop is idle, avoiding UI blocking. | |
| 202 | 202 | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | |
| 203 | 203 | log := logging.FromContext(ctx) | |
| 204 | - | cacheDirWarnKey := "cache-dir:" + domain | |
| 204 | + | cacheDirWarnKey := "cache-dir" | |
| 205 | 205 | savePNGWarnKey := "save-png:" + domain | |
| 206 | 206 | sizedPNGWarnKey := "sized-png:" + domain | |
| 207 | 207 | ||
| @@ -201,7 +201,7 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin( | |||||
| 201 | // Uses glib.IdleAdd to defer disk I/O until GTK main loop is idle, avoiding UI blocking. | 201 | // Uses glib.IdleAdd to defer disk I/O until GTK main loop is idle, avoiding UI blocking. | ||
| 202 | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | 202 | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | ||
| 203 | log := logging.FromContext(ctx) | 203 | log := logging.FromContext(ctx) | ||
| 204 | - | cacheDirWarnKey := "cache-dir:" + domain | 204 | + | cacheDirWarnKey := "cache-dir" |
| 205 | savePNGWarnKey := "save-png:" + domain | 205 | savePNGWarnKey := "save-png:" + domain | ||
| 206 | sizedPNGWarnKey := "sized-png:" + domain | 206 | sizedPNGWarnKey := "sized-png:" + domain | ||
| 207 | 207 | ||||
M internal/ui/adapter/favicon_test.go +23 -1
| @@ -1,6 +1,11 @@ | |||
| 1 | 1 | package adapter | |
| 2 | 2 | ||
| 3 | - | import "testing" | |
| 3 | + | import ( | |
| 4 | + | "sync/atomic" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/rs/zerolog" | |
| 8 | + | ) | |
| 4 | 9 | ||
| 5 | 10 | func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) { | |
| 6 | 11 | adapter := NewFaviconAdapter(nil, nil) | |
| @@ -49,3 +54,20 @@ func TestFaviconWarningDedup_ClearResetsState(t *testing.T) { | |||
| 49 | 54 | t.Fatalf("expected suppressed count reset after clear, got %d", suppressed) | |
| 50 | 55 | } | |
| 51 | 56 | } | |
| 57 | + | ||
| 58 | + | func TestFaviconWarningDedup_LogWarningDedupInvokesCallbackOnce(t *testing.T) { | |
| 59 | + | adapter := NewFaviconAdapter(nil, nil) | |
| 60 | + | key := "save-png:example.com" | |
| 61 | + | var calls atomic.Int32 | |
| 62 | + | ||
| 63 | + | adapter.logWarningDedup(t.Context(), key, nil, func(_ *zerolog.Logger, _ error) { | |
| 64 | + | calls.Add(1) | |
| 65 | + | }) | |
| 66 | + | adapter.logWarningDedup(t.Context(), key, nil, func(_ *zerolog.Logger, _ error) { | |
| 67 | + | calls.Add(1) | |
| 68 | + | }) | |
| 69 | + | ||
| 70 | + | if got := calls.Load(); got != 1 { | |
| 71 | + | t.Fatalf("expected warning callback to run once, got %d", got) | |
| 72 | + | } | |
| 73 | + | } | |
| @@ -1,6 +1,11 @@ | |||||
| 1 | package adapter | 1 | package adapter | ||
| 2 | 2 | ||||
| 3 | - | import "testing" | 3 | + | import ( |
| 4 | + | "sync/atomic" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/rs/zerolog" | |||
| 8 | + | ) | |||
| 4 | 9 | ||||
| 5 | func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) { | 10 | func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) { | ||
| 6 | adapter := NewFaviconAdapter(nil, nil) | 11 | adapter := NewFaviconAdapter(nil, nil) | ||
| @@ -49,3 +54,20 @@ func TestFaviconWarningDedup_ClearResetsState(t *testing.T) { | |||||
| 49 | t.Fatalf("expected suppressed count reset after clear, got %d", suppressed) | 54 | t.Fatalf("expected suppressed count reset after clear, got %d", suppressed) | ||
| 50 | } | 55 | } | ||
| 51 | } | 56 | } | ||
| 57 | + | ||||
| 58 | + | func TestFaviconWarningDedup_LogWarningDedupInvokesCallbackOnce(t *testing.T) { | |||
| 59 | + | adapter := NewFaviconAdapter(nil, nil) | |||
| 60 | + | key := "save-png:example.com" | |||
| 61 | + | var calls atomic.Int32 | |||
| 62 | + | ||||
| 63 | + | adapter.logWarningDedup(t.Context(), key, nil, func(_ *zerolog.Logger, _ error) { | |||
| 64 | + | calls.Add(1) | |||
| 65 | + | }) | |||
| 66 | + | adapter.logWarningDedup(t.Context(), key, nil, func(_ *zerolog.Logger, _ error) { | |||
| 67 | + | calls.Add(1) | |||
| 68 | + | }) | |||
| 69 | + | ||||
| 70 | + | if got := calls.Load(); got != 1 { | |||
| 71 | + | t.Fatalf("expected warning callback to run once, got %d", got) | |||
| 72 | + | } | |||
| 73 | + | } | |||
M internal/ui/coordinator/content.go +6 -1
| @@ -23,6 +23,7 @@ import ( | |||
| 23 | 23 | "github.com/bnema/dumber/internal/ui/layout" | |
| 24 | 24 | webkitlib "github.com/bnema/puregotk-webkit/webkit" | |
| 25 | 25 | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 26 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 26 | 27 | ) | |
| 27 | 28 | ||
| 28 | 29 | const ( | |
| @@ -1751,7 +1752,11 @@ func (c *ContentCoordinator) scheduleParentPaneRefresh( | |||
| 1751 | 1752 | c.popupMu.Lock() | |
| 1752 | 1753 | delete(c.popupRefresh, parentPaneID) | |
| 1753 | 1754 | c.popupMu.Unlock() | |
| 1754 | - | c.refreshPaneAfterOAuth(ctx, parentPaneID, popupID) | |
| 1755 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 1756 | + | c.refreshPaneAfterOAuth(ctx, parentPaneID, popupID) | |
| 1757 | + | return false | |
| 1758 | + | }) | |
| 1759 | + | glib.IdleAdd(&cb, 0) | |
| 1755 | 1760 | }) | |
| 1756 | 1761 | c.popupMu.Unlock() | |
| 1757 | 1762 | } | |
| @@ -23,6 +23,7 @@ import ( | |||||
| 23 | "github.com/bnema/dumber/internal/ui/layout" | 23 | "github.com/bnema/dumber/internal/ui/layout" | ||
| 24 | webkitlib "github.com/bnema/puregotk-webkit/webkit" | 24 | webkitlib "github.com/bnema/puregotk-webkit/webkit" | ||
| 25 | "github.com/jwijenbergh/puregotk/v4/gdk" | 25 | "github.com/jwijenbergh/puregotk/v4/gdk" | ||
| 26 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |||
| 26 | ) | 27 | ) | ||
| 27 | 28 | ||||
| 28 | const ( | 29 | const ( | ||
| @@ -1751,7 +1752,11 @@ func (c *ContentCoordinator) scheduleParentPaneRefresh( | |||||
| 1751 | c.popupMu.Lock() | 1752 | c.popupMu.Lock() | ||
| 1752 | delete(c.popupRefresh, parentPaneID) | 1753 | delete(c.popupRefresh, parentPaneID) | ||
| 1753 | c.popupMu.Unlock() | 1754 | c.popupMu.Unlock() | ||
| 1754 | - | c.refreshPaneAfterOAuth(ctx, parentPaneID, popupID) | 1755 | + | cb := glib.SourceFunc(func(_ uintptr) bool { |
| 1756 | + | c.refreshPaneAfterOAuth(ctx, parentPaneID, popupID) | |||
| 1757 | + | return false | |||
| 1758 | + | }) | |||
| 1759 | + | glib.IdleAdd(&cb, 0) | |||
| 1755 | }) | 1760 | }) | ||
| 1756 | c.popupMu.Unlock() | 1761 | c.popupMu.Unlock() | ||
| 1757 | } | 1762 | } | ||