Split service worker image cache into multiple

This commit is contained in:
schroda
2026-01-18 16:34:20 +01:00
parent 25b8c85ef2
commit 7b13c1dcc1
3 changed files with 99 additions and 22 deletions

View File

@@ -28,7 +28,7 @@ export const ImagesSettings = () => {
const clearCache = async () => { const clearCache = async () => {
try { try {
await Promise.all([triggerClearServerCache(), ImageCache.clear()]); await Promise.all([triggerClearServerCache(), ImageCache.clearAll()]);
makeToast(t`Cleared the cache`, 'success'); makeToast(t`Cleared the cache`, 'success');
} catch (e) { } catch (e) {
makeToast(t`Could not clear the cache`, 'error', getErrorMessage(e)); makeToast(t`Could not clear the cache`, 'error', getErrorMessage(e));

View File

@@ -6,12 +6,41 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
const CACHE_NAME = 'image-cache'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
// !!! IMPORTANT !!! - Update along with vite.config.ts workbox config
export enum ImageCacheKey {
OTHER = 'image-cache-other',
EXTENSION_ICONS = 'image-cache-extension-icons',
MANGA_THUMBNAILS = 'image-cache-manga-thumbnails',
CHAPTER_PAGES = 'image-cache-chapter-pages',
}
const IMAGE_CACHE_KEYS = Object.values(ImageCacheKey);
caches.delete('image-cache').catch(defaultPromiseErrorHandler('ImageCache: delete removed "image-cache"'));
export class ImageCache { export class ImageCache {
static async has(url: string): Promise<boolean> { // !!! IMPORTANT !!! - Update along with vite.config.ts workbox config
static getKeyFor(url: string): ImageCacheKey {
if (url.match(/\/chapter\/[0-9]+\/page\/[0-9]+/g)) {
return ImageCacheKey.CHAPTER_PAGES;
}
if (url.match(/\/manga\/[0-9]+\/thumbnail/g)) {
return ImageCacheKey.MANGA_THUMBNAILS;
}
if (url.includes('/extension/icon/')) {
return ImageCacheKey.EXTENSION_ICONS;
}
return ImageCacheKey.OTHER;
}
static async has(url: string, key: ImageCacheKey = this.getKeyFor(url)): Promise<boolean> {
try { try {
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(key);
const response = await cache.match(url, { ignoreVary: true }); const response = await cache.match(url, { ignoreVary: true });
return response !== undefined; return response !== undefined;
@@ -20,9 +49,9 @@ export class ImageCache {
} }
} }
static async get(url: string): Promise<Response | null> { static async get(url: string, key: ImageCacheKey = this.getKeyFor(url)): Promise<Response | null> {
try { try {
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(key);
const response = await cache.match(url); const response = await cache.match(url);
return response || null; return response || null;
@@ -31,10 +60,14 @@ export class ImageCache {
} }
} }
static async clear(): Promise<void> { static async clear(key: ImageCacheKey): Promise<void> {
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(key);
const keys = await cache.keys(); const imageKeys = await cache.keys();
await Promise.all(keys.map((key) => cache.delete(key))); await Promise.all(imageKeys.map((imageKey) => cache.delete(imageKey)));
}
static async clearAll(): Promise<void> {
IMAGE_CACHE_KEYS.forEach(ImageCache.clear);
} }
} }

View File

@@ -61,23 +61,67 @@ export default defineConfig(({ command }) => ({
globPatterns: [], globPatterns: [],
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: ({ request, url }) => { urlPattern: ({ url }) => {
if (request.destination === 'image') {
return true;
}
const { pathname } = url; const { pathname } = url;
return ( return pathname.match(/\/chapter\/[0-9]+\/page\/[0-9]+/g);
pathname.match(/\/chapter\/[0-9]+\/page\/[0-9]+/g) ||
pathname.match(/\/manga\/[0-9]+\/thumbnail/g) ||
pathname.includes('/extension/icon/')
);
}, },
handler: 'CacheFirst', handler: 'CacheFirst',
options: { options: {
cacheName: 'image-cache', // !!! IMPORTANT !!! - Update along with ImageCache.ts
cacheName: 'image-cache-chapter-pages',
expiration: { expiration: {
maxEntries: 10000, maxEntries: 2500,
purgeOnQuotaError: true,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: ({ url }) => {
const { pathname } = url;
return pathname.match(/\/manga\/[0-9]+\/thumbnail/g);
},
handler: 'CacheFirst',
options: {
// !!! IMPORTANT !!! - Update along with ImageCache.ts
cacheName: 'image-cache-manga-thumbnails',
expiration: {
maxEntries: 5000,
purgeOnQuotaError: true,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: ({ url }) => {
const { pathname } = url;
return pathname.includes('/extension/icon/');
},
handler: 'CacheFirst',
options: {
// !!! IMPORTANT !!! - Update along with ImageCache.ts
cacheName: 'image-cache-extension-icons',
expiration: {
maxEntries: 300,
purgeOnQuotaError: true,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: ({ request }) => request.destination === 'image',
handler: 'CacheFirst',
options: {
// !!! IMPORTANT !!! - Update along with ImageCache.ts
cacheName: 'image-cache-other',
expiration: {
maxEntries: 500,
purgeOnQuotaError: true, purgeOnQuotaError: true,
}, },
cacheableResponse: { cacheableResponse: {