import webpDataUrl from 'url-loader!../codecs/tiny.webp'; // Give TypeScript the correct global. declare var self: ServiceWorkerGlobalScope; export function cacheOrNetwork(event: FetchEvent): void { event.respondWith(async function () { const cachedResponse = await caches.match(event.request, { ignoreSearch: true }); return cachedResponse || fetch(event.request); }()); } export function cacheOrNetworkAndCache(event: FetchEvent, cacheName: string): void { event.respondWith(async function () { const { request } = event; // Return from cache if possible. const cachedResponse = await caches.match(request); if (cachedResponse) return cachedResponse; // Else go to the network. const response = await fetch(request); const responseToCache = response.clone(); event.waitUntil(async function () { // Cache what we fetched. const cache = await caches.open(cacheName); await cache.put(request, responseToCache); }()); // Return the network response. return response; }()); } export function serveShareTarget(event: FetchEvent): void { const dataPromise = event.request.formData(); // Redirect so the user can refresh the page without resending data. // @ts-ignore It doesn't like me giving a response to respondWith, although it's allowed. event.respondWith(Response.redirect('/?share-target')); event.waitUntil(async function () { // The page sends this message to tell the service worker it's ready to receive the file. await nextMessage('share-ready'); const client = await self.clients.get(event.resultingClientId); const data = await dataPromise; const file = data.get('file'); client.postMessage({ file, action: 'load-image' }); }()); } export function cleanupCache(event: FetchEvent, cacheName: string, keepAssets: string[]) { event.waitUntil(async function () { const cache = await caches.open(cacheName); // Clean old entries from the dynamic cache. const requests = await cache.keys(); const promises = requests.map((cachedRequest) => { // Get pathname without leading / const assetPath = new URL(cachedRequest.url).pathname.slice(1); // If it isn't one of our keepAssets, we don't need it anymore. if (!keepAssets.includes(assetPath)) return cache.delete(cachedRequest); }); await Promise.all(promises); }()); } function getAssetsWithPrefix(assets: string[], prefixes: string[]) { return assets.filter( asset => prefixes.some(prefix => asset.startsWith(prefix)), ); } export async function cacheBasics(cacheName: string, buildAssets: string[]) { const toCache = ['/', '/assets/favicon.ico']; const prefixesToCache = [ // Main app JS & CSS: 'main-app.', // Service worker handler: 'offliner.', // Little icons for the demo images on the homescreen: 'icon-demo-', // Site logo: 'logo.', ]; const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache); toCache.push(...prefixMatches); const cache = await caches.open(cacheName); await cache.addAll(toCache); } export async function cacheAdditionalProcessors(cacheName: string, buildAssets: string[]) { let toCache = []; const prefixesToCache = [ // Worker which handles image processing: 'processor-worker.', // processor-worker imports: 'process-', ]; const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache); const wasm = buildAssets.filter(asset => asset.endsWith('.wasm')); toCache.push(...prefixMatches, ...wasm); const supportsWebP = await (async () => { if (!self.createImageBitmap) return false; const response = await fetch(webpDataUrl); const blob = await response.blob(); return createImageBitmap(blob).then(() => true, () => false); })(); // No point caching the WebP decoder if it's supported natively: if (supportsWebP) { toCache = toCache.filter(asset => !/webp[\-_]dec/.test(asset)); } const cache = await caches.open(cacheName); await cache.addAll(toCache); } const nextMessageResolveMap = new Map void)[]>(); /** * Wait on a message with a particular event.data value. * * @param dataVal The event.data value. */ function nextMessage(dataVal: string): Promise { return new Promise((resolve) => { if (!nextMessageResolveMap.has(dataVal)) { nextMessageResolveMap.set(dataVal, []); } nextMessageResolveMap.get(dataVal)!.push(resolve); }); } self.addEventListener('message', (event) => { const resolvers = nextMessageResolveMap.get(event.data); if (!resolvers) return; nextMessageResolveMap.delete(event.data); for (const resolve of resolvers) resolve(); });