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 @@
///
///
///
-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();
// 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) => {