Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions background/cache-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Network-first: try the network, fall back to cache when offline/slow.
export const NETWORK_FIRST_API_PATHS = [
'/extension/wigi-pad-data',
'/date/events',
'/news/rss',
'/contents',
'/extension/notifications',
'/extension/searchbox',
]

// Stale-while-revalidate: return the cached response instantly, refresh in the background.
export const SWR_API_PATHS = ['/searchbox', '/currencies', '/weather']

export const NEVER_CACHE_API_PATHS = ['/searchbox/suggest-search']

export const CDN_NO_CACHE_PREFIXES = ['/wallpapers/', '/avatars/']
44 changes: 44 additions & 0 deletions background/cache-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CacheName } from '@/common/types/sw-events'

export const CACHE_PREFIX = 'wgf-'

export const APP_VERSION: string = (() => {
try {
return browser.runtime.getManifest().version
} catch {
return '0'
}
})()

const ns = (base: string) => `${CACHE_PREFIX}${base}-v${APP_VERSION}`

export const CacheNames = {
api: ns('api'),
wallpaper: ns('wallpaper'),
fonts: ns('fonts'),
cdn: ns('cdn'),
cdnCss: ns('cdn-css'),
} as const

export const EXPECTED_CACHES = new Set<string>(Object.values(CacheNames))

export const LEGACY_CACHES: string[] = [
'cdn-cache-v1',
'videos-cache-v1',
'static-assets-v1',
'fonts-cache-v1',
'html-cache-v1',
'navigation-cache-v1',
'remote-fonts-css-cache',
'widgetify-public-api',
'critical-resources-v1',
]

export function resolveCacheName(logical: CacheName): string {
switch (logical) {
case CacheName.API:
return CacheNames.api
default:
return CacheNames.api
}
}
262 changes: 113 additions & 149 deletions background/cache.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import { ExpirationPlugin } from 'workbox-expiration'
import { RangeRequestsPlugin } from 'workbox-range-requests'
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { NavigationRoute } from 'workbox-routing'
const allowedPaths = [
'/extension/wigi-pad-data',
'/date/events',
'/news/rss',
'/currencies',
'/contents',
'/extension/notifications',
'/extension/searchbox',
'/searchbox',
'/weather',
]
import { CacheNames } from './cache-names'
import {
CDN_NO_CACHE_PREFIXES,
NETWORK_FIRST_API_PATHS,
NEVER_CACHE_API_PATHS,
SWR_API_PATHS,
} from './cache-config'
import { activeWallpaperUrls } from './wallpaper-cache'

const API_ORIGIN = 'https://api.widgetify.ir'
const CDN_ORIGIN = 'https://cdn.widgetify.ir'

function matchesApiPaths(url: URL, request: Request, paths: string[]): boolean {
if (request.method !== 'GET') return false
if (url.origin !== API_ORIGIN) return false
if (NEVER_CACHE_API_PATHS.some((path) => url.pathname.includes(path))) return false
return paths.some((path) => url.pathname.startsWith(path))
}

// Browser-initiated requests for CDN assets (<img>, <link>, ...) default to
// no-cors, which yields opaque responses. Opaque responses can't be validated
// and get a large, misleading padding added to storage estimates. Since the CDN
// sends `Access-Control-Allow-Origin: *`, upgrade the fetch to CORS so we store
// a real, unpadded response instead.
const upgradeCdnToCorsPlugin = {
requestWillFetch: async ({ request }: { request: Request }) => {
if (request.mode === 'no-cors' && new URL(request.url).origin === CDN_ORIGIN) {
return new Request(request.url, { mode: 'cors', credentials: 'omit' })
}
return request
},
}

const DAY = 24 * 60 * 60

export function setupCaching() {
try {
Expand All @@ -23,154 +46,95 @@ export function setupCaching() {
})
}

const apiCachePlugins = () => [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 2 * DAY,
purgeOnQuotaError: true,
}),
]

registerRoute(
({ url, request }) => matchesApiPaths(url, request, SWR_API_PATHS),
new StaleWhileRevalidate({
cacheName: CacheNames.api,
plugins: apiCachePlugins(),
})
)

registerRoute(
({ url, request }) => {
if (request.method !== 'GET') return false
if (url.origin !== 'https://api.widgetify.ir') return false
({ url, request }) => matchesApiPaths(url, request, NETWORK_FIRST_API_PATHS),
new NetworkFirst({
cacheName: CacheNames.api,
networkTimeoutSeconds: 3,
plugins: apiCachePlugins(),
})
)

return allowedPaths.some((path) => url.pathname.startsWith(path))
},
registerRoute(
({ url }) => activeWallpaperUrls.has(url.href),
new CacheFirst({
cacheName: CacheNames.wallpaper,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
// Video wallpapers issue Range requests; serve proper 206 slices
// from the cached full 200 instead of relying on browser leniency.
new RangeRequestsPlugin(),
],
})
)

registerRoute(
({ url }) => url.origin === CDN_ORIGIN && url.pathname.endsWith('.css'),
new StaleWhileRevalidate({
cacheName: 'widgetify-public-api',
// Separate cache from the general CDN route: two ExpirationPlugins
// sharing one cache name fight over ownership and thrash IndexedDB.
cacheName: CacheNames.cdnCss,
plugins: [
new CacheableResponsePlugin({
statuses: [200],
}),
upgradeCdnToCorsPlugin,
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({
maxEntries: 200,
maxAgeSeconds: 10 * 24 * 60 * 60, // 10 days
purgeOnQuotaError: true, // Automatically cleanup if quota is exceeded
maxEntries: 20,
maxAgeSeconds: 30 * DAY,
purgeOnQuotaError: true,
}),
],
})
)

const isDev = import.meta.env.DEV

if (!isDev) {
registerRoute(
({ url }) =>
url.href === 'https://cdn.widgetify.ir/fonts/remote-fonts.css',
new StaleWhileRevalidate({
cacheName: 'remote-fonts-css-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 1,
maxAgeSeconds: 2 * 60,
purgeOnQuotaError: true,
}),
],
})
)

registerRoute(
({ request }) =>
request.destination === 'script' || request.destination === 'style',
new CacheFirst({
cacheName: 'static-assets-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 200,
maxAgeSeconds: 10 * 24 * 60 * 60,
purgeOnQuotaError: true,
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
)

registerRoute(
({ request }) => request.destination === 'font',
new CacheFirst({
cacheName: 'fonts-cache-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 2 * 60,
purgeOnQuotaError: true,
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
)

registerRoute(
({ request }) => request.destination === 'video',
new CacheFirst({
cacheName: 'videos-cache-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 300,
maxAgeSeconds: 5 * 24 * 60 * 60, // 5 days
purgeOnQuotaError: true,
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
)

registerRoute(
({ request }) => request.destination === 'document',
new NetworkFirst({
cacheName: 'html-cache-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 2 * 24 * 60 * 60, // 2 days
purgeOnQuotaError: true,
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
)

registerRoute(
({ url }) =>
url.origin === 'https://cdn.jsdelivr.net' ||
url.origin === 'https://unpkg.com' ||
url.origin === 'https://cdnjs.cloudflare.com' ||
url.hostname.includes('googleapis.com') ||
url.hostname.includes('gstatic.com') ||
url.hostname.includes('storage'),
new StaleWhileRevalidate({
cacheName: 'cdn-cache-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 1 * 60 * 60, // 1 hours
purgeOnQuotaError: true,
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
)
registerRoute(
({ url, request }) =>
request.destination === 'font' && url.protocol === 'https:',
new CacheFirst({
cacheName: CacheNames.fonts,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 30,
maxAgeSeconds: 60 * DAY,
purgeOnQuotaError: true,
}),
],
})
)

const navigationRoute = new NavigationRoute(
new NetworkFirst({
cacheName: 'navigation-cache-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 30,
maxAgeSeconds: 1 * 60 * 60, // 1 hours
}),
],
})
)
registerRoute(navigationRoute)
}
registerRoute(
({ url }) =>
url.origin === CDN_ORIGIN &&
!CDN_NO_CACHE_PREFIXES.some((prefix) => url.pathname.startsWith(prefix)),
new CacheFirst({
cacheName: CacheNames.cdn,
plugins: [
upgradeCdnToCorsPlugin,
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({
maxEntries: 150,
maxAgeSeconds: 30 * DAY,
purgeOnQuotaError: true,
}),
],
})
)
} catch {}
}
Loading
Loading