diff --git a/web/src/lib/utils/sw-messaging.ts b/web/src/lib/utils/sw-messaging.ts index 1a19d3c134..13b5275a37 100644 --- a/web/src/lib/utils/sw-messaging.ts +++ b/web/src/lib/utils/sw-messaging.ts @@ -1,8 +1,39 @@ const broadcast = new BroadcastChannel('immich'); +let isLoadedReplyListeners: ((url: string, isUrlCached: boolean) => void)[] = []; +broadcast.addEventListener('message', (event) => { + if (event.data.type == 'isImageUrlCachedReply') { + for (const listener of isLoadedReplyListeners) { + listener(event.data.url, event.data.isImageUrlCached); + } + } +}); + export function cancelImageUrl(url: string) { broadcast.postMessage({ type: 'cancel', url }); } + export function preloadImageUrl(url: string) { broadcast.postMessage({ type: 'preload', url }); } + +export function isImageUrlCached(url: string) { + if (!globalThis.isSecureContext) { + return Promise.resolve(false); + } + return new Promise((resolve) => { + const listener = (urlReply: string, isUrlCached: boolean) => { + if (urlReply === url) { + cleanup(isUrlCached); + } + }; + const cleanup = (isUrlCached: boolean) => { + isLoadedReplyListeners = isLoadedReplyListeners.filter((element) => element !== listener); + resolve(isUrlCached); + }; + isLoadedReplyListeners.push(listener); + broadcast.postMessage({ type: 'isImageUrlCached', url }); + + setTimeout(() => cleanup(false), 5000); + }); +} diff --git a/web/src/service-worker/broadcast-channel.ts b/web/src/service-worker/broadcast-channel.ts index ae6f1e1be6..62f90bbcc5 100644 --- a/web/src/service-worker/broadcast-channel.ts +++ b/web/src/service-worker/broadcast-channel.ts @@ -1,7 +1,8 @@ -import { handleCancel, handlePreload } from './request'; +import { handleCancel, handleIsUrlCached, handlePreload } from './request'; + +export const broadcast = new BroadcastChannel('immich'); export const installBroadcastChannelListener = () => { - const broadcast = new BroadcastChannel('immich'); // eslint-disable-next-line unicorn/prefer-add-event-listener broadcast.onmessage = (event) => { if (!event.data) { @@ -20,6 +21,15 @@ export const installBroadcastChannelListener = () => { handleCancel(url); break; } + + case 'isImageUrlCached': { + void handleIsUrlCached(url); + break; + } } }; }; + +export const replyIsImageUrlCached = (url: string, isImageUrlCached: boolean) => { + broadcast.postMessage({ type: 'isImageUrlCachedReply', url, isImageUrlCached }); +}; diff --git a/web/src/service-worker/cache.ts b/web/src/service-worker/cache.ts index f91d8366ea..8ddf3e6ec2 100644 --- a/web/src/service-worker/cache.ts +++ b/web/src/service-worker/cache.ts @@ -30,7 +30,11 @@ export const put = async (key: string, response: Response) => { return; } - cache.put(key, response.clone()); + try { + await cache.put(key, response.clone()); + } catch (error) { + console.error('Ignoring error during cache put', error); + } }; export const prune = async () => { diff --git a/web/src/service-worker/request.ts b/web/src/service-worker/request.ts index aeb63be899..a5189fd548 100644 --- a/web/src/service-worker/request.ts +++ b/web/src/service-worker/request.ts @@ -1,3 +1,4 @@ +import { replyIsImageUrlCached } from './broadcast-channel'; import { get, put } from './cache'; const pendingRequests = new Map(); @@ -44,7 +45,7 @@ export const handleRequest = async (request: URL | Request) => { const response = await fetch(request, { signal: cancelToken.signal }); assertResponse(response); - put(cacheKey, response); + await put(cacheKey, response); return response; } catch (error) { @@ -71,3 +72,9 @@ export const handleCancel = (url: URL) => { pendingRequest.abort(); pendingRequests.delete(cacheKey); }; + +export const handleIsUrlCached = async (url: URL) => { + const cacheKey = getCacheKey(url); + const isImageUrlCached = !!(await get(cacheKey)); + replyIsImageUrlCached(url.pathname + url.search + url.hash, isImageUrlCached); +};