From 58224ee038eb2ab17b9375832c72756ae874af5c Mon Sep 17 00:00:00 2001 From: pouriya Date: Fri, 24 Jul 2026 12:55:57 +0330 Subject: [PATCH] fix(cache): recover offline wallpapers, cache favicons, purge orphaned caches - Wallpapers (image/gif/video) no longer stay black after an update or while offline. The image is probed out-of-band and re-applied once the service worker has pinned/cached it, videos retry on error, and everything re-applies when the tab comes back online instead of waiting for a manual re-select. - Bookmark favicons (Google's favicon service) are now cached, so they load offline and hit the network far less. Bounded via ExpirationPlugin and versioned like the other caches. - Three legacy cache names (images-cache-v1, cross-origin-cache-v1, widgetify-api-cache-v1) from pre-overhaul Workbox versions were missing from the purge list and lingered forever on updated installs; they are now removed by purgeStaleCaches(). --- background/cache-names.ts | 4 + background/cache.ts | 19 ++++ background/utils.ts | 1 + .../wallpapers/hooks/use-wallpaper-apply.tsx | 88 ++++++++++++++++--- 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/background/cache-names.ts b/background/cache-names.ts index f05d3ecc..3553d697 100644 --- a/background/cache-names.ts +++ b/background/cache-names.ts @@ -18,6 +18,7 @@ export const CacheNames = { fonts: ns('fonts'), cdn: ns('cdn'), cdnCss: ns('cdn-css'), + favicons: ns('favicons'), } as const export const EXPECTED_CACHES = new Set(Object.values(CacheNames)) @@ -32,6 +33,9 @@ export const LEGACY_CACHES: string[] = [ 'remote-fonts-css-cache', 'widgetify-public-api', 'critical-resources-v1', + 'images-cache-v1', + 'cross-origin-cache-v1', + 'widgetify-api-cache-v1', ] export function resolveCacheName(logical: CacheName): string { diff --git a/background/cache.ts b/background/cache.ts index 28df20ac..2048be5c 100644 --- a/background/cache.ts +++ b/background/cache.ts @@ -119,6 +119,25 @@ export function setupCaching() { }) ) + registerRoute( + ({ url }) => + (url.hostname === 'www.google.com' && + url.pathname.startsWith('/s2/favicons')) || + (url.hostname.endsWith('.gstatic.com') && + url.pathname.startsWith('/faviconV2')), + new CacheFirst({ + cacheName: CacheNames.favicons, + plugins: [ + new CacheableResponsePlugin({ statuses: [0, 200] }), + new ExpirationPlugin({ + maxEntries: 200, + maxAgeSeconds: 30 * DAY, + purgeOnQuotaError: true, + }), + ], + }) + ) + registerRoute( ({ url }) => url.origin === CDN_ORIGIN && diff --git a/background/utils.ts b/background/utils.ts index 793a557e..4b4fc54e 100644 --- a/background/utils.ts +++ b/background/utils.ts @@ -38,6 +38,7 @@ export async function enforceCacheBudget(): Promise { CacheNames.cdn, CacheNames.api, CacheNames.fonts, + CacheNames.favicons, ] for (const name of trimOrder) { const cache = await caches.open(name) diff --git a/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx b/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx index 58f4fdd5..972afc8c 100644 --- a/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx +++ b/src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx @@ -6,17 +6,18 @@ import { getRandomWallpaper } from '@/services/hooks/wallpapers/getWallpaperCate import { safeAwait } from '@/services/api' import { SwEventType } from '@/common/types/sw-events' -function pinWallpaperForOffline(wallpaper: StoredWallpaper) { - if (wallpaper.type !== 'IMAGE' && wallpaper.type !== 'VIDEO') return - if (!/^https?:\/\//.test(wallpaper.src)) return +function pinWallpaperForOffline(wallpaper: StoredWallpaper): Promise { + if (wallpaper.type !== 'IMAGE' && wallpaper.type !== 'VIDEO') return Promise.resolve() + if (!/^https?:\/\//.test(wallpaper.src)) return Promise.resolve() - browser.runtime + return browser.runtime .sendMessage({ type: SwEventType.SetActiveWallpaper, src: wallpaper.src, wallpaperType: wallpaper.type, }) - .catch(() => {}) + .then(() => undefined) + .catch(() => undefined) } const GRADIENT_DIRECTION_MAP: Record = { @@ -30,18 +31,62 @@ const GRADIENT_DIRECTION_MAP: Record = { 'to-bl': 'to bottom left', } +let applyToken = 0 + +const RETRY_LIMIT = 4 +const RETRY_BASE_MS = 500 + +function setImageStyles() { + document.body.style.backgroundPosition = 'center' + document.body.style.backgroundRepeat = 'no-repeat' + document.body.style.backgroundSize = 'cover' + document.body.style.backgroundColor = '' +} + +// A background-image whose request failed is never retried by the browser, and +// re-assigning the same url() is a no-op. Clear it for one frame to force a +// fresh evaluation once the bytes are actually available. +function forceReapplyImage(src: string, token: number) { + if (token !== applyToken) return + document.body.style.backgroundImage = 'none' + requestAnimationFrame(() => { + if (token !== applyToken) return + setImageStyles() + document.body.style.backgroundImage = `url("${src}")` + }) +} + +// background-image emits no load/error events, so probe the URL out-of-band. If +// the first paint missed (cold worker, or a cache emptied by an update), wait +// for the worker to pin the wallpaper, then retry and re-apply once loadable. +function verifyImage(src: string, pinned: Promise, token: number, attempt: number) { + const probe = new Image() + probe.onload = () => { + if (attempt > 0) forceReapplyImage(src, token) + } + probe.onerror = () => { + if (token !== applyToken || attempt >= RETRY_LIMIT) return + const retry = () => verifyImage(src, pinned, token, attempt + 1) + if (attempt === 0) { + pinned.then(() => window.setTimeout(retry, RETRY_BASE_MS)) + } else { + window.setTimeout(retry, RETRY_BASE_MS * (attempt + 1)) + } + } + probe.src = src +} + function applyWallpaper(wallpaper: StoredWallpaper) { - pinWallpaperForOffline(wallpaper) + const token = ++applyToken + const pinned = pinWallpaperForOffline(wallpaper) const existingVideo = document.getElementById('background-video') if (existingVideo) existingVideo.remove() if (wallpaper.type === 'IMAGE') { - document.body.style.backgroundImage = `url(${wallpaper.src})` - document.body.style.backgroundPosition = 'center' - document.body.style.backgroundRepeat = 'no-repeat' - document.body.style.backgroundSize = 'cover' - document.body.style.backgroundColor = '' + setImageStyles() + document.body.style.backgroundImage = `url("${wallpaper.src}")` + verifyImage(wallpaper.src, pinned, token, 0) } else if (wallpaper.type === 'GRADIENT' && wallpaper.gradient) { const { from, to, direction } = wallpaper.gradient const cssDirection = GRADIENT_DIRECTION_MAP[direction] ?? direction @@ -75,8 +120,19 @@ function applyWallpaper(wallpaper: StoredWallpaper) { objectFit: 'cover', }) + let videoRetries = 0 video.onerror = () => { document.body.style.backgroundColor = '#000' + // Retry driven by the error event (not a one-shot check) so it also + // recovers when the error arrives after pinning has resolved. + if (token !== applyToken || videoRetries >= RETRY_LIMIT) return + videoRetries++ + const delay = RETRY_BASE_MS * videoRetries + pinned.then(() => + window.setTimeout(() => { + if (token === applyToken && video.isConnected) video.load() + }, delay) + ) } video.onloadeddata = () => { video.play().catch((e) => console.warn('Play failed:', e)) @@ -144,8 +200,18 @@ export function useWallpaperApply() { } ) + // Back online: re-pin and re-apply so a wallpaper that couldn't be fetched + // while offline (e.g. right after an update cleared its cache) recovers on + // its own instead of staying black until the user re-selects one. + const handleOnline = async () => { + const wallpaper: StoredWallpaper | null = await getFromStorage('wallpaper') + if (wallpaper) applyWallpaper(wallpaper) + } + window.addEventListener('online', handleOnline) + return () => { unsubscribe() + window.removeEventListener('online', handleOnline) } }, []) }