diff --git a/web/src/service-worker/index.ts b/web/src/service-worker/index.ts index 797f4754b6..7d87d0bdc3 100644 --- a/web/src/service-worker/index.ts +++ b/web/src/service-worker/index.ts @@ -2,7 +2,7 @@ /// <reference no-default-lib="true"/> /// <reference lib="esnext" /> /// <reference lib="webworker" /> -import { version } from '$service-worker'; +import { build, files, version } from '$service-worker'; const useCache = true; const sw = globalThis as unknown as ServiceWorkerGlobalScope; @@ -11,8 +11,15 @@ const pendingLoads = new Map<string, AbortController>(); // Create a unique cache name for this deployment const CACHE = `cache-${version}`; +const APP_RESOURCES = [ + ...build, // the app itself + ...files, // everything in `static` +]; + sw.addEventListener('install', (event) => { event.waitUntil(sw.skipWaiting()); + // Create a new cache and add all files to it + event.waitUntil(addFilesToCache()); }); sw.addEventListener('activate', (event) => { @@ -26,8 +33,16 @@ sw.addEventListener('fetch', (event) => { return; } const url = new URL(event.request.url); - if (/^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(url.pathname)) { + if (APP_RESOURCES.includes(url.pathname)) { + event.respondWith(appResources(url, event)); + } else if (/^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(url.pathname)) { event.respondWith(immichAsset(url)); + } else if ( + /^(\/(link|auth|admin|albums|archive|buy|explore|favorites|folders|maps|memory|partners|people|photos|places|search|share|shared-links|sharing|tags|trash|user-settings|utilities))(\/.*)?$/.test( + url.pathname, + ) + ) { + event.respondWith(ssr(new URL(event.request.url).origin)); } }); @@ -39,6 +54,28 @@ async function deleteOldCaches() { } } +async function addFilesToCache() { + const cache = await caches.open(CACHE); + await cache.addAll(APP_RESOURCES); +} + +async function ssr(origin: string) { + const cache = await caches.open(CACHE); + const url = new URL('/', origin); + let response = useCache ? await cache.match(url) : undefined; + if (response) { + return response; + } + response = await fetch(url); + if (!(response instanceof Response)) { + return Response.error(); + } + if (response.status === 200) { + cache.put(url, response.clone()); + } + return response; +} + async function immichAsset(url: URL) { const cache = await caches.open(CACHE); let response = useCache ? await cache.match(url) : undefined; @@ -66,6 +103,40 @@ async function immichAsset(url: URL) { } } +async function appResources(url: URL, event: FetchEvent) { + const cache = await caches.open(CACHE); + // `build`/`files` can always be served from the cache + if (APP_RESOURCES.includes(url.pathname)) { + const response = await cache.match(url.pathname); + if (response) { + return response; + } + } + // for everything else, try the network first, but + // fall back to the cache if we're offline + try { + const response = await fetch(event.request); + // if we're offline, fetch can return a value that is not a Response + // instead of throwing - and we can't pass this non-Response to respondWith + if (!(response instanceof Response)) { + throw new TypeError('invalid response from fetch'); + } + + if (response.status === 200) { + cache.put(event.request, response.clone()); + } + + return response; + } catch { + const response = await cache.match(event.request); + if (response) { + return response; + } + // if there's no cache, then just error out + return Response.error(); + } +} + const broadcast = new BroadcastChannel('immich'); // eslint-disable-next-line unicorn/prefer-add-event-listener broadcast.onmessage = (event) => {