diff --git a/web/src/service-worker/broadcast-channel.ts b/web/src/service-worker/broadcast-channel.ts index e1a30b203b..12f835c52f 100644 --- a/web/src/service-worker/broadcast-channel.ts +++ b/web/src/service-worker/broadcast-channel.ts @@ -1,4 +1,4 @@ -import { cancelLoad, getCachedOrFetch } from './cache'; +import { cancelLoad, getCachedOrFetch } from './fetch-event'; export const installBroadcastChannelListener = () => { const broadcast = new BroadcastChannel('immich'); diff --git a/web/src/service-worker/cache.ts b/web/src/service-worker/cache.ts deleted file mode 100644 index 3a2e92c973..0000000000 --- a/web/src/service-worker/cache.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { build, files, version } from '$service-worker'; - -const useCache = true; -const CACHE = `cache-${version}`; - -export const APP_RESOURCES = [ - ...build, // the app itself - ...files, // everything in `static` -]; - -export const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined; -export const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined; - -export async function deleteOldCaches() { - for (const key of await caches.keys()) { - if (key !== CACHE) { - await caches.delete(key); - } - } -} - -export async function addFilesToCache() { - const cache = await caches.open(CACHE); - await cache.addAll(APP_RESOURCES); -} - -const pendingLoads = new Map(); - -export async function cancelLoad(urlString: string) { - const pending = pendingLoads.get(urlString); - if (pending) { - pending.abort(); - pendingLoads.delete(urlString); - } -} - -export async function getCachedOrFetch(request: URL | Request | string, cancelable: boolean = false) { - const cached = await checkCache(request); - if (cached.response) { - return cached.response; - } - - try { - if (!cancelable) { - const response = await fetch(request); - checkResponse(response); - return response; - } - - return await fetchWithCancellation(request, cached.cache); - } catch { - return new Response(undefined, { - status: 499, - statusText: 'Request canceled: Instructions unclear, accidentally interrupted myself', - }); - } -} - -async function fetchWithCancellation(request: URL | Request | string, cache: Cache) { - const cacheKey = getCacheKey(request); - const cancelToken = new AbortController(); - - try { - pendingLoads.set(cacheKey, cancelToken); - const response = await fetch(request, { - signal: cancelToken.signal, - }); - - checkResponse(response); - setCached(response, cache, cacheKey); - return response; - } finally { - pendingLoads.delete(cacheKey); - } -} - -async function checkCache(url: URL | Request | string) { - if (!useCache) { - return; - } - const cache = await caches.open(CACHE); - const response = await cache.match(url); - return { cache, response }; -} - -async function setCached(response: Response, cache: Cache, cacheKey: URL | Request | string) { - if (response.status === 200) { - cache.put(cacheKey, response.clone()); - } -} - -function checkResponse(response: Response) { - if (!(response instanceof Response)) { - throw new TypeError('invalid response from fetch'); - } -} - -function getCacheKey(request: URL | Request | string) { - if (isURL(request)) { - return request.toString(); - } else if (isRequest(request)) { - return request.url; - } else { - return request; - } -} diff --git a/web/src/service-worker/fetch-event.ts b/web/src/service-worker/fetch-event.ts index 9ac53c8c14..11c8e0fd00 100644 --- a/web/src/service-worker/fetch-event.ts +++ b/web/src/service-worker/fetch-event.ts @@ -1,26 +1,111 @@ -import { APP_RESOURCES, getCachedOrFetch } from './cache'; +import { version } from '$service-worker'; + +const useCache = true; +const CACHE = `cache-${version}`; + +export const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined; +export const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined; + +export async function deleteOldCaches() { + for (const key of await caches.keys()) { + if (key !== CACHE) { + await caches.delete(key); + } + } +} + +const pendingLoads = new Map(); + +export async function cancelLoad(urlString: string) { + const pending = pendingLoads.get(urlString); + if (pending) { + pending.abort(); + pendingLoads.delete(urlString); + } +} + +export async function getCachedOrFetch(request: URL | Request | string, cancelable: boolean = false) { + const cached = await checkCache(request); + if (cached.response) { + return cached.response; + } + + try { + if (!cancelable) { + const response = await fetch(request); + checkResponse(response); + return response; + } + + return await fetchWithCancellation(request, cached.cache); + } catch { + return new Response(undefined, { + status: 499, + statusText: 'Request canceled: Instructions unclear, accidentally interrupted myself', + }); + } +} + +async function fetchWithCancellation(request: URL | Request | string, cache: Cache) { + const cacheKey = getCacheKey(request); + const cancelToken = new AbortController(); + + try { + pendingLoads.set(cacheKey, cancelToken); + const response = await fetch(request, { + signal: cancelToken.signal, + }); + + checkResponse(response); + setCached(response, cache, cacheKey); + return response; + } finally { + pendingLoads.delete(cacheKey); + } +} + +async function checkCache(url: URL | Request | string) { + if (!useCache) { + return; + } + const cache = await caches.open(CACHE); + const response = await cache.match(url); + return { cache, response }; +} + +async function setCached(response: Response, cache: Cache, cacheKey: URL | Request | string) { + if (response.status === 200) { + cache.put(cacheKey, response.clone()); + } +} + +function checkResponse(response: Response) { + if (!(response instanceof Response)) { + throw new TypeError('invalid response from fetch'); + } +} + +function getCacheKey(request: URL | Request | string) { + if (isURL(request)) { + return request.toString(); + } else if (isRequest(request)) { + return request.url; + } else { + return request; + } +} function isAssetRequest(pathname: string): boolean { return /^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(pathname); } -function isIgnoredFileType(pathname: string): boolean { - return /\.(png|ico|txt|json|ts|ttf|css|js|svelte)$/.test(pathname); -} - -function isIgnoredPath(pathname: string): boolean { - return /^\/(src|api)(\/.*)?$/.test(pathname) || /^\/(node_modules|@vite|@id)(\/.*)?$/.test(pathname); -} - export function handleFetchEvent(event: FetchEvent): void { if (event.request.method !== 'GET') { return; } const url = new URL(event.request.url); - - if (APP_RESOURCES.includes(url.pathname)) { - event.respondWith(getCachedOrFetch(event.request)); + if (url.origin !== self.location.origin) { return; } @@ -28,11 +113,4 @@ export function handleFetchEvent(event: FetchEvent): void { event.respondWith(getCachedOrFetch(event.request, true)); return; } - - if (isIgnoredFileType(url.pathname) || isIgnoredPath(url.pathname)) { - return; - } - - const slash = new URL('/', url.origin); - event.respondWith(getCachedOrFetch(slash)); } diff --git a/web/src/service-worker/index.ts b/web/src/service-worker/index.ts index b3c1fda38e..fbb6f74d82 100644 --- a/web/src/service-worker/index.ts +++ b/web/src/service-worker/index.ts @@ -3,21 +3,17 @@ /// /// import { installBroadcastChannelListener } from './broadcast-channel'; -import { addFilesToCache, deleteOldCaches } from './cache'; -import { handleFetchEvent } from './fetch-event'; +import { deleteOldCaches, handleFetchEvent } from './fetch-event'; const sw = globalThis as unknown as ServiceWorkerGlobalScope; const handleActivate = (event: ExtendableEvent) => { event.waitUntil(sw.clients.claim()); - // Remove previous cached data from disk event.waitUntil(deleteOldCaches()); }; const handleInstall = (event: ExtendableEvent) => { event.waitUntil(sw.skipWaiting()); - // Create a new cache and add all files to it - event.waitUntil(addFilesToCache()); }; sw.addEventListener('install', handleInstall, { passive: true });