feat(cache): implement two-tier FaviconCache with async loading
Add new FaviconCache with in-memory cache keyed by normalized domain and WebKit FaviconDatabase as persistent backing store. Domain normalization strips www prefix for consistent lookups. GetOrFetch method provides async loading with URL variant matching (www/non-www, trailing slash) to handle WebKit's exact URL storage. Callback runs on GLib main loop for safe UI updates.
- commit
- 458bd84188abc1f888156b4919630fdefd5bf76b
- author
- brice · · 2025-12-15 18:34 UTC
- committer
- brice · · 2025-12-15 20:01 UTC
- parent
- 65aa3b0
- signature
- unsigned
1 file · +242 -0
A internal/ui/cache/favicon_cache.go +242 -0
| @@ -0,0 +1,242 @@ | |||
| 1 | + | // Package cache provides in-memory caching utilities for the UI layer. | |
| 2 | + | package cache | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "net/url" | |
| 7 | + | "strings" | |
| 8 | + | "sync" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/dumber/internal/logging" | |
| 11 | + | "github.com/bnema/puregotk-webkit/webkit" | |
| 12 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 13 | + | "github.com/jwijenbergh/puregotk/v4/gio" | |
| 14 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // FaviconCache provides a two-tier favicon caching system: | |
| 18 | + | // 1. In-memory cache keyed by domain for fast lookups | |
| 19 | + | // 2. WebKit FaviconDatabase as persistent backing store | |
| 20 | + | type FaviconCache struct { | |
| 21 | + | mu sync.RWMutex | |
| 22 | + | cache map[string]*gdk.Texture // key: domain (e.g., "github.com") | |
| 23 | + | faviconDB *webkit.FaviconDatabase | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase. | |
| 27 | + | // The faviconDB can be nil, in which case only the in-memory cache is used. | |
| 28 | + | func NewFaviconCache(faviconDB *webkit.FaviconDatabase) *FaviconCache { | |
| 29 | + | return &FaviconCache{ | |
| 30 | + | cache: make(map[string]*gdk.Texture), | |
| 31 | + | faviconDB: faviconDB, | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // extractDomain extracts the normalized domain (host) from a URL string. | |
| 36 | + | // Normalizes by stripping "www." prefix so youtube.com and www.youtube.com | |
| 37 | + | // resolve to the same cache key. | |
| 38 | + | func extractDomain(rawURL string) string { | |
| 39 | + | if rawURL == "" { | |
| 40 | + | return "" | |
| 41 | + | } | |
| 42 | + | parsed, err := url.Parse(rawURL) | |
| 43 | + | if err != nil { | |
| 44 | + | return "" | |
| 45 | + | } | |
| 46 | + | host := parsed.Host | |
| 47 | + | // Normalize: strip www. prefix | |
| 48 | + | host = strings.TrimPrefix(host, "www.") | |
| 49 | + | return host | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // Set stores a favicon texture for the given domain. | |
| 53 | + | func (fc *FaviconCache) Set(domain string, texture *gdk.Texture) { | |
| 54 | + | if domain == "" || texture == nil { | |
| 55 | + | return | |
| 56 | + | } | |
| 57 | + | fc.mu.Lock() | |
| 58 | + | fc.cache[domain] = texture | |
| 59 | + | fc.mu.Unlock() | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // SetByURL stores a favicon texture for the domain extracted from the URL. | |
| 63 | + | func (fc *FaviconCache) SetByURL(pageURL string, texture *gdk.Texture) { | |
| 64 | + | domain := extractDomain(pageURL) | |
| 65 | + | fc.Set(domain, texture) | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // Get retrieves a favicon texture by domain from the in-memory cache. | |
| 69 | + | // Returns nil if not found. | |
| 70 | + | func (fc *FaviconCache) Get(domain string) *gdk.Texture { | |
| 71 | + | if domain == "" { | |
| 72 | + | return nil | |
| 73 | + | } | |
| 74 | + | fc.mu.RLock() | |
| 75 | + | texture := fc.cache[domain] | |
| 76 | + | fc.mu.RUnlock() | |
| 77 | + | return texture | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // GetByURL retrieves a favicon texture by extracting the domain from the URL. | |
| 81 | + | // Returns nil if not found. | |
| 82 | + | func (fc *FaviconCache) GetByURL(pageURL string) *gdk.Texture { | |
| 83 | + | domain := extractDomain(pageURL) | |
| 84 | + | return fc.Get(domain) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // getURLVariants returns URL variants to try for favicon lookup. | |
| 88 | + | // WebKit stores favicons under exact URLs (e.g., https://www.youtube.com/) | |
| 89 | + | // so we need to try multiple variants: with/without www, with/without trailing slash. | |
| 90 | + | func getURLVariants(pageURL string) []string { | |
| 91 | + | parsed, err := url.Parse(pageURL) | |
| 92 | + | if err != nil { | |
| 93 | + | return nil | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | variants := make([]string, 0, 4) | |
| 97 | + | ||
| 98 | + | // Generate host variants (with and without www.) | |
| 99 | + | hosts := []string{parsed.Host} | |
| 100 | + | if strings.HasPrefix(parsed.Host, "www.") { | |
| 101 | + | hosts = append(hosts, strings.TrimPrefix(parsed.Host, "www.")) | |
| 102 | + | } else { | |
| 103 | + | hosts = append(hosts, "www."+parsed.Host) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // Generate path variants (with and without trailing slash) | |
| 107 | + | paths := []string{parsed.Path} | |
| 108 | + | if parsed.Path == "" || parsed.Path == "/" { | |
| 109 | + | paths = []string{"", "/"} | |
| 110 | + | } else if strings.HasSuffix(parsed.Path, "/") { | |
| 111 | + | paths = append(paths, strings.TrimSuffix(parsed.Path, "/")) | |
| 112 | + | } else { | |
| 113 | + | paths = append(paths, parsed.Path+"/") | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // Generate all combinations (skip the original URL) | |
| 117 | + | original := pageURL | |
| 118 | + | for _, host := range hosts { | |
| 119 | + | for _, path := range paths { | |
| 120 | + | p := *parsed | |
| 121 | + | p.Host = host | |
| 122 | + | p.Path = path | |
| 123 | + | variant := p.String() | |
| 124 | + | if variant != original { | |
| 125 | + | variants = append(variants, variant) | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | return variants | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // GetOrFetch checks the in-memory cache first, then queries the FaviconDatabase | |
| 134 | + | // asynchronously on cache miss. The callback is invoked with the texture (or nil). | |
| 135 | + | // This is safe to call from any goroutine; the callback runs on the GLib main loop. | |
| 136 | + | func (fc *FaviconCache) GetOrFetch(ctx context.Context, pageURL string, callback func(*gdk.Texture)) { | |
| 137 | + | if callback == nil { | |
| 138 | + | return | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | log := logging.FromContext(ctx) | |
| 142 | + | domain := extractDomain(pageURL) | |
| 143 | + | ||
| 144 | + | // Check in-memory cache first | |
| 145 | + | if texture := fc.Get(domain); texture != nil { | |
| 146 | + | callback(texture) | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // No FaviconDatabase, return nil | |
| 151 | + | if fc.faviconDB == nil { | |
| 152 | + | callback(nil) | |
| 153 | + | return | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // Helper to invoke callback on main thread | |
| 157 | + | invokeCallback := func(texture *gdk.Texture) { | |
| 158 | + | cb := glib.SourceFunc(func(uintptr) bool { | |
| 159 | + | callback(texture) | |
| 160 | + | return false | |
| 161 | + | }) | |
| 162 | + | glib.IdleAdd(&cb, 0) | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | // Get all URL variants to try (www/non-www, with/without trailing slash) | |
| 166 | + | variants := getURLVariants(pageURL) | |
| 167 | + | ||
| 168 | + | // Recursive function to try variants one by one | |
| 169 | + | var tryVariant func(index int) | |
| 170 | + | tryVariant = func(index int) { | |
| 171 | + | if index >= len(variants) { | |
| 172 | + | // All variants exhausted | |
| 173 | + | invokeCallback(nil) | |
| 174 | + | return | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | variantURL := variants[index] | |
| 178 | + | variantCb := gio.AsyncReadyCallback(func(_ uintptr, resultPtr uintptr, _ uintptr) { | |
| 179 | + | if resultPtr == 0 { | |
| 180 | + | tryVariant(index + 1) | |
| 181 | + | return | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | result := &gio.AsyncResultBase{Ptr: resultPtr} | |
| 185 | + | texture, err := fc.faviconDB.GetFaviconFinish(result) | |
| 186 | + | if err != nil { | |
| 187 | + | log.Debug().Err(err).Str("url", variantURL).Msg("favicon variant fetch failed") | |
| 188 | + | tryVariant(index + 1) | |
| 189 | + | return | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | if texture != nil && domain != "" { | |
| 193 | + | fc.Set(domain, texture) | |
| 194 | + | } | |
| 195 | + | invokeCallback(texture) | |
| 196 | + | }) | |
| 197 | + | ||
| 198 | + | fc.faviconDB.GetFavicon(variantURL, nil, &variantCb, 0) | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | // Query FaviconDatabase with original URL first | |
| 202 | + | asyncCb := gio.AsyncReadyCallback(func(_ uintptr, resultPtr uintptr, _ uintptr) { | |
| 203 | + | if resultPtr == 0 { | |
| 204 | + | log.Debug().Str("url", pageURL).Msg("favicon fetch: nil result, trying variants") | |
| 205 | + | tryVariant(0) | |
| 206 | + | return | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | result := &gio.AsyncResultBase{Ptr: resultPtr} | |
| 210 | + | texture, err := fc.faviconDB.GetFaviconFinish(result) | |
| 211 | + | if err != nil { | |
| 212 | + | log.Debug().Err(err).Str("url", pageURL).Msg("favicon fetch failed, trying variants") | |
| 213 | + | tryVariant(0) | |
| 214 | + | return | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // Cache the result | |
| 218 | + | if texture != nil && domain != "" { | |
| 219 | + | fc.Set(domain, texture) | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | invokeCallback(texture) | |
| 223 | + | }) | |
| 224 | + | ||
| 225 | + | // Start async fetch (nil cancellable, 0 userData) | |
| 226 | + | fc.faviconDB.GetFavicon(pageURL, nil, &asyncCb, 0) | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // Clear removes all entries from the in-memory cache. | |
| 230 | + | func (fc *FaviconCache) Clear() { | |
| 231 | + | fc.mu.Lock() | |
| 232 | + | fc.cache = make(map[string]*gdk.Texture) | |
| 233 | + | fc.mu.Unlock() | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // Size returns the number of entries in the in-memory cache. | |
| 237 | + | func (fc *FaviconCache) Size() int { | |
| 238 | + | fc.mu.RLock() | |
| 239 | + | size := len(fc.cache) | |
| 240 | + | fc.mu.RUnlock() | |
| 241 | + | return size | |
| 242 | + | } | |
| @@ -0,0 +1,242 @@ | |||||
| 1 | + | // Package cache provides in-memory caching utilities for the UI layer. | |||
| 2 | + | package cache | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "context" | |||
| 6 | + | "net/url" | |||
| 7 | + | "strings" | |||
| 8 | + | "sync" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/dumber/internal/logging" | |||
| 11 | + | "github.com/bnema/puregotk-webkit/webkit" | |||
| 12 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |||
| 13 | + | "github.com/jwijenbergh/puregotk/v4/gio" | |||
| 14 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // FaviconCache provides a two-tier favicon caching system: | |||
| 18 | + | // 1. In-memory cache keyed by domain for fast lookups | |||
| 19 | + | // 2. WebKit FaviconDatabase as persistent backing store | |||
| 20 | + | type FaviconCache struct { | |||
| 21 | + | mu sync.RWMutex | |||
| 22 | + | cache map[string]*gdk.Texture // key: domain (e.g., "github.com") | |||
| 23 | + | faviconDB *webkit.FaviconDatabase | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase. | |||
| 27 | + | // The faviconDB can be nil, in which case only the in-memory cache is used. | |||
| 28 | + | func NewFaviconCache(faviconDB *webkit.FaviconDatabase) *FaviconCache { | |||
| 29 | + | return &FaviconCache{ | |||
| 30 | + | cache: make(map[string]*gdk.Texture), | |||
| 31 | + | faviconDB: faviconDB, | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // extractDomain extracts the normalized domain (host) from a URL string. | |||
| 36 | + | // Normalizes by stripping "www." prefix so youtube.com and www.youtube.com | |||
| 37 | + | // resolve to the same cache key. | |||
| 38 | + | func extractDomain(rawURL string) string { | |||
| 39 | + | if rawURL == "" { | |||
| 40 | + | return "" | |||
| 41 | + | } | |||
| 42 | + | parsed, err := url.Parse(rawURL) | |||
| 43 | + | if err != nil { | |||
| 44 | + | return "" | |||
| 45 | + | } | |||
| 46 | + | host := parsed.Host | |||
| 47 | + | // Normalize: strip www. prefix | |||
| 48 | + | host = strings.TrimPrefix(host, "www.") | |||
| 49 | + | return host | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // Set stores a favicon texture for the given domain. | |||
| 53 | + | func (fc *FaviconCache) Set(domain string, texture *gdk.Texture) { | |||
| 54 | + | if domain == "" || texture == nil { | |||
| 55 | + | return | |||
| 56 | + | } | |||
| 57 | + | fc.mu.Lock() | |||
| 58 | + | fc.cache[domain] = texture | |||
| 59 | + | fc.mu.Unlock() | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // SetByURL stores a favicon texture for the domain extracted from the URL. | |||
| 63 | + | func (fc *FaviconCache) SetByURL(pageURL string, texture *gdk.Texture) { | |||
| 64 | + | domain := extractDomain(pageURL) | |||
| 65 | + | fc.Set(domain, texture) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // Get retrieves a favicon texture by domain from the in-memory cache. | |||
| 69 | + | // Returns nil if not found. | |||
| 70 | + | func (fc *FaviconCache) Get(domain string) *gdk.Texture { | |||
| 71 | + | if domain == "" { | |||
| 72 | + | return nil | |||
| 73 | + | } | |||
| 74 | + | fc.mu.RLock() | |||
| 75 | + | texture := fc.cache[domain] | |||
| 76 | + | fc.mu.RUnlock() | |||
| 77 | + | return texture | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // GetByURL retrieves a favicon texture by extracting the domain from the URL. | |||
| 81 | + | // Returns nil if not found. | |||
| 82 | + | func (fc *FaviconCache) GetByURL(pageURL string) *gdk.Texture { | |||
| 83 | + | domain := extractDomain(pageURL) | |||
| 84 | + | return fc.Get(domain) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // getURLVariants returns URL variants to try for favicon lookup. | |||
| 88 | + | // WebKit stores favicons under exact URLs (e.g., https://www.youtube.com/) | |||
| 89 | + | // so we need to try multiple variants: with/without www, with/without trailing slash. | |||
| 90 | + | func getURLVariants(pageURL string) []string { | |||
| 91 | + | parsed, err := url.Parse(pageURL) | |||
| 92 | + | if err != nil { | |||
| 93 | + | return nil | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | variants := make([]string, 0, 4) | |||
| 97 | + | ||||
| 98 | + | // Generate host variants (with and without www.) | |||
| 99 | + | hosts := []string{parsed.Host} | |||
| 100 | + | if strings.HasPrefix(parsed.Host, "www.") { | |||
| 101 | + | hosts = append(hosts, strings.TrimPrefix(parsed.Host, "www.")) | |||
| 102 | + | } else { | |||
| 103 | + | hosts = append(hosts, "www."+parsed.Host) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // Generate path variants (with and without trailing slash) | |||
| 107 | + | paths := []string{parsed.Path} | |||
| 108 | + | if parsed.Path == "" || parsed.Path == "/" { | |||
| 109 | + | paths = []string{"", "/"} | |||
| 110 | + | } else if strings.HasSuffix(parsed.Path, "/") { | |||
| 111 | + | paths = append(paths, strings.TrimSuffix(parsed.Path, "/")) | |||
| 112 | + | } else { | |||
| 113 | + | paths = append(paths, parsed.Path+"/") | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // Generate all combinations (skip the original URL) | |||
| 117 | + | original := pageURL | |||
| 118 | + | for _, host := range hosts { | |||
| 119 | + | for _, path := range paths { | |||
| 120 | + | p := *parsed | |||
| 121 | + | p.Host = host | |||
| 122 | + | p.Path = path | |||
| 123 | + | variant := p.String() | |||
| 124 | + | if variant != original { | |||
| 125 | + | variants = append(variants, variant) | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | return variants | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // GetOrFetch checks the in-memory cache first, then queries the FaviconDatabase | |||
| 134 | + | // asynchronously on cache miss. The callback is invoked with the texture (or nil). | |||
| 135 | + | // This is safe to call from any goroutine; the callback runs on the GLib main loop. | |||
| 136 | + | func (fc *FaviconCache) GetOrFetch(ctx context.Context, pageURL string, callback func(*gdk.Texture)) { | |||
| 137 | + | if callback == nil { | |||
| 138 | + | return | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | log := logging.FromContext(ctx) | |||
| 142 | + | domain := extractDomain(pageURL) | |||
| 143 | + | ||||
| 144 | + | // Check in-memory cache first | |||
| 145 | + | if texture := fc.Get(domain); texture != nil { | |||
| 146 | + | callback(texture) | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // No FaviconDatabase, return nil | |||
| 151 | + | if fc.faviconDB == nil { | |||
| 152 | + | callback(nil) | |||
| 153 | + | return | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | // Helper to invoke callback on main thread | |||
| 157 | + | invokeCallback := func(texture *gdk.Texture) { | |||
| 158 | + | cb := glib.SourceFunc(func(uintptr) bool { | |||
| 159 | + | callback(texture) | |||
| 160 | + | return false | |||
| 161 | + | }) | |||
| 162 | + | glib.IdleAdd(&cb, 0) | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | // Get all URL variants to try (www/non-www, with/without trailing slash) | |||
| 166 | + | variants := getURLVariants(pageURL) | |||
| 167 | + | ||||
| 168 | + | // Recursive function to try variants one by one | |||
| 169 | + | var tryVariant func(index int) | |||
| 170 | + | tryVariant = func(index int) { | |||
| 171 | + | if index >= len(variants) { | |||
| 172 | + | // All variants exhausted | |||
| 173 | + | invokeCallback(nil) | |||
| 174 | + | return | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | variantURL := variants[index] | |||
| 178 | + | variantCb := gio.AsyncReadyCallback(func(_ uintptr, resultPtr uintptr, _ uintptr) { | |||
| 179 | + | if resultPtr == 0 { | |||
| 180 | + | tryVariant(index + 1) | |||
| 181 | + | return | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | result := &gio.AsyncResultBase{Ptr: resultPtr} | |||
| 185 | + | texture, err := fc.faviconDB.GetFaviconFinish(result) | |||
| 186 | + | if err != nil { | |||
| 187 | + | log.Debug().Err(err).Str("url", variantURL).Msg("favicon variant fetch failed") | |||
| 188 | + | tryVariant(index + 1) | |||
| 189 | + | return | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | if texture != nil && domain != "" { | |||
| 193 | + | fc.Set(domain, texture) | |||
| 194 | + | } | |||
| 195 | + | invokeCallback(texture) | |||
| 196 | + | }) | |||
| 197 | + | ||||
| 198 | + | fc.faviconDB.GetFavicon(variantURL, nil, &variantCb, 0) | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | // Query FaviconDatabase with original URL first | |||
| 202 | + | asyncCb := gio.AsyncReadyCallback(func(_ uintptr, resultPtr uintptr, _ uintptr) { | |||
| 203 | + | if resultPtr == 0 { | |||
| 204 | + | log.Debug().Str("url", pageURL).Msg("favicon fetch: nil result, trying variants") | |||
| 205 | + | tryVariant(0) | |||
| 206 | + | return | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | result := &gio.AsyncResultBase{Ptr: resultPtr} | |||
| 210 | + | texture, err := fc.faviconDB.GetFaviconFinish(result) | |||
| 211 | + | if err != nil { | |||
| 212 | + | log.Debug().Err(err).Str("url", pageURL).Msg("favicon fetch failed, trying variants") | |||
| 213 | + | tryVariant(0) | |||
| 214 | + | return | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | // Cache the result | |||
| 218 | + | if texture != nil && domain != "" { | |||
| 219 | + | fc.Set(domain, texture) | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | invokeCallback(texture) | |||
| 223 | + | }) | |||
| 224 | + | ||||
| 225 | + | // Start async fetch (nil cancellable, 0 userData) | |||
| 226 | + | fc.faviconDB.GetFavicon(pageURL, nil, &asyncCb, 0) | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | // Clear removes all entries from the in-memory cache. | |||
| 230 | + | func (fc *FaviconCache) Clear() { | |||
| 231 | + | fc.mu.Lock() | |||
| 232 | + | fc.cache = make(map[string]*gdk.Texture) | |||
| 233 | + | fc.mu.Unlock() | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // Size returns the number of entries in the in-memory cache. | |||
| 237 | + | func (fc *FaviconCache) Size() int { | |||
| 238 | + | fc.mu.RLock() | |||
| 239 | + | size := len(fc.cache) | |||
| 240 | + | fc.mu.RUnlock() | |||
| 241 | + | return size | |||
| 242 | + | } | |||