/**************************************************** WORKBOX IMPORT ****************************************************/ // The configuration is set to use Workbox // The following code will import Workbox from CDN or public URL // Import from public URL importScripts('/workbox/workbox-sw.js'); importScripts('/idb/umd.js'); workbox.setConfig({modulePathPrefix: '/workbox'}); /**************************************************** END WORKBOX IMPORT ****************************************************/ function registerCacheFirst(routeMatchFn, cacheName, plugins = []) { const strategy = new workbox.strategies.CacheFirst({ cacheName, plugins }); workbox.routing.registerRoute(routeMatchFn, strategy); return strategy; } function precacheResources(strategy, resourceList, event) { if (!(event instanceof ExtendableEvent)) { throw new Error("precacheResources needs a valid ExtendableEvent"); } return Promise.all(resourceList.map(path => strategy.handleAll({ event, request: new Request(path), })[1] )); } function createBackgroundSyncPlugin(queueName, maxRetentionTime = 2880, forceSyncFallback = false) { return new workbox.backgroundSync.BackgroundSyncPlugin(queueName, { maxRetentionTime, forceSyncFallback }); } function createBackgroundSyncPluginWithBroadcast(queueName, channelName, maxRetentionTime = 2880, forceSyncFallback = false) { const queue = new workbox.backgroundSync.Queue(queueName, { maxRetentionTime, forceSyncFallback, }); const bc = new BroadcastChannel(channelName); const replayQueueWithProgress = async () => { let entry; let successCount = 0; let failureCount = 0; const total = (await queue.getAll()).length; while ((entry = await queue.shiftRequest())) { try { await fetch(entry.request.clone()); successCount++; } catch (error) { failureCount++; await queue.unshiftRequest(entry); throw error; } finally { const remaining = (await queue.getAll()).length; bc.postMessage({ name: queueName, replaying: true, remaining }); } } const remaining = (await queue.getAll()).length; bc.postMessage({ name: queueName, replayed: true, remaining, successCount, failureCount, }); }; bc.onmessage = async (event) => { if (event.data?.type === 'status-request') { const entries = await queue.getAll(); bc.postMessage({ name: queueName, remaining: entries.length }); } if (event.data?.type === 'replay-request') { try { await replayQueueWithProgress(); } catch (error) { const entries = await queue.getAll(); bc.postMessage({ name: queueName, replayed: false, remaining: entries.length, error: error.message, }); } } }; return { fetchDidFail: async ({ request }) => { await queue.pushRequest({ request }); const entries = await queue.getAll(); bc.postMessage({ name: queueName, remaining: entries.length }); }, onSync: async () => { await replayQueueWithProgress(); }, }; } const messageTasks = []; function registerMessageTask(callback) { messageTasks.push(callback); } self.addEventListener('message', (event) => { event.waitUntil( messageTasks.reduce( (chain, task) => chain.then(() => task(event)), Promise.resolve() ) ); }); const installTasks = []; function registerInstallTask(callback, priority = 100) { installTasks.push({ callback: (event) => { const result = callback(event); if (!result?.then) console.warn("Install task did not return a Promise"); return result; }, priority, }); } self.addEventListener('install', (event) => { event.waitUntil( installTasks .sort((a, b) => a.priority - b.priority) .reduce( (chain, task) => chain.then(() => task.callback(event)), Promise.resolve() ) ); }); function statusGuard(min, max) { return { fetchDidSucceed: ({ response }) => { if (response.status >= min && response.status <= max) { throw new Error(`Server error: ${response.status}`); } return response; } }; } registerMessageTask(async (event) => { if (event.data?.type === 'SKIP_WAITING') { await self.skipWaiting(); } }); const usedCacheNames = new Set(); function registerCacheName(name) { usedCacheNames.add(name); return name; } async function openBackgroundFetchDatabase() { return await self.idb.openDB('bgfetch-completed', 1, { upgrade(db) { if (!db.objectStoreNames.contains('files')) { db.createObjectStore('files', { keyPath: 'id' }); } if (!db.objectStoreNames.contains('chunks')) { const store = db.createObjectStore('chunks', { keyPath: ['id', 'index'] }); store.createIndex('by-id', 'id'); } else { const store = db.transaction.objectStore('chunks'); if (!Array.from(store.indexNames).includes('by-id')) { store.createIndex('by-id', 'id'); } } } }); } const bgFetchMetadata = new Map(); const bgFetchChannel = new BroadcastChannel('bg-fetch'); bgFetchChannel.onmessage = async (event) => { const { type, id, meta } = event.data || {}; switch (type) { case 'register-meta': bgFetchMetadata.set(id, meta); break; case 'get-meta': const metadata = bgFetchMetadata.get(id) || null; bgFetchChannel.postMessage({ type: 'meta-response', id, metadata }); break; case 'clear-meta': bgFetchMetadata.delete(id); break; case 'list-stored-files': { const db = await openBackgroundFetchDatabase(); const files = await db.getAll('files'); bgFetchChannel.postMessage({ type: 'stored-files', files }); break; } case 'delete-stored-file': { const name = event.data.name; const db = await openBackgroundFetchDatabase(); const allFiles = await db.getAll('files'); const target = allFiles.find(f => f.name === name); if (target) { await db.delete('files', target.id); let index = 0; while (await db.get('chunks', [target.id, index])) { await db.delete('chunks', [target.id, index++]); } } break; } } }; const backgroundFetchTasks = { click: [], success: [], fail: [], }; function registerBackgroundFetchTask(type, callback, priority = 100) { if (!backgroundFetchTasks[type]) { throw new Error(`Unknown background fetch event type: ${type}`); } backgroundFetchTasks[type].push({ callback: (event) => { const result = callback(event); if (!result?.then) { console.warn(`[${type}] task did not return a Promise`); } return result; }, priority, }); } function runBackgroundFetchTasks(type, event) { const tasks = backgroundFetchTasks[type] ?? []; return tasks .sort((a, b) => a.priority - b.priority) .reduce( (chain, task) => chain.then(() => task.callback(event)), Promise.resolve() ); } self.addEventListener('backgroundfetchclick', (event) => { event.waitUntil(runBackgroundFetchTasks('click', event)); }); self.addEventListener('backgroundfetchsuccess', (event) => { event.waitUntil(runBackgroundFetchTasks('success', event)); }); self.addEventListener('backgroundfetchfail', (event) => { event.waitUntil(runBackgroundFetchTasks('fail', event)); }); const pushTasks = []; function registerPushTask(callback) { pushTasks.push(callback); } self.addEventListener('push', (event) => { if (!(self.Notification && self.Notification.permission === 'granted')) { return; } event.waitUntil( pushTasks.reduce( (chain, task) => chain.then(() => task(event)), Promise.resolve() ) ); }); const notificationActionHandlers = new Map(); function registerNotificationAction(actionName, handler) { notificationActionHandlers.set(actionName, handler); } self.addEventListener('notificationclick', event => { event.notification.close(); const action = event.action || ""; const promises = []; const specificHandler = notificationActionHandlers.get(action); if (typeof specificHandler === 'function') { promises.push(Promise.resolve(specificHandler(event))); } const wildcardHandler = notificationActionHandlers.get('*'); if (typeof wildcardHandler === 'function') { promises.push(Promise.resolve(wildcardHandler(event))); } if (promises.length > 0) { event.waitUntil(Promise.all(promises)); } }); const structuredPushNotificationSupport = (event) => { const {data} = event; const sendNotification = response => { const {title, options} = JSON.parse(response); return self.registration.showNotification(title, options); }; if (data) { const message = data.text(); event.waitUntil(sendNotification(message)); } } function simplePushNotificationSupport(event) { const { data } = event; if (!data) return; const message = data.text(); const sendNotification = (text) => { return self.registration.showNotification('Notification', { body: text }); }; event.waitUntil(sendNotification(message)); } const periodicSyncTasks = new Map(); function registerPeriodicSyncTask(tag, callback, priority = 100) { if (!periodicSyncTasks.has(tag)) { periodicSyncTasks.set(tag, []); } periodicSyncTasks.get(tag).push({ priority, callback: (event) => { const result = callback(event); if (!result?.then) console.warn(`[${tag}] periodic sync task did not return a Promise`); return result; } }); } async function runPeriodicSyncTasks(tag, event) { const tasks = periodicSyncTasks.get(tag) ?? []; return tasks .sort((a, b) => a.priority - b.priority) .reduce( (chain, task) => chain.then(() => task.callback(event)), Promise.resolve() ); } self.addEventListener('periodicsync', event => { event.waitUntil(runPeriodicSyncTasks(event.tag, event)); }); const periodicChannel = new BroadcastChannel('periodic-sync'); function notifyPeriodicSyncClients(tag, payload = {}) { periodicChannel.postMessage({ type: 'periodic-sync-update', tag, timestamp: Date.now(), ...payload }); } const cacheInstances = new Map(); async function openCache(name) { if (!cacheInstances.has(name)) { const cache = await caches.open(name); cacheInstances.set(name, cache); } return cacheInstances.get(name); } /**************************************************** CACHE STRATEGY ****************************************************/ // Strategy: CacheFirst // Match: ({url}) => url.pathname.startsWith('/assets') // Cache Name: assets // Enabled: 1 // Needs Workbox: 1 // Method: // 1. Creation of the Workbox Cache Strategy object // 2. Register the route with the Workbox Router // 3. Add the assets to the cache when the service worker is installed const cache_0_0 = new workbox.strategies.CacheFirst({ cacheName: registerCacheName('assets'),plugins: [new workbox.expiration.ExpirationPlugin({ "maxEntries": 68, "maxAgeSeconds": 31536000 })] }); workbox.routing.registerRoute(({url}) => url.pathname.startsWith('/assets'),cache_0_0); registerInstallTask((event) => precacheResources(cache_0_0, [ "/assets/@spomky-labs/pwa-bundle/badge_controller-fxsMWke.js", "/assets/@spomky-labs/pwa-bundle/connection-status_controller-a6qV4RW.js", "/assets/@spomky-labs/pwa-bundle/abstract_controller-lHossdH.js", "/assets/@spomky-labs/pwa-bundle/capture_controller-yP__Kii.js", "/assets/@spomky-labs/pwa-bundle/install_controller-TRkeV12.js", "/assets/@spomky-labs/pwa-bundle/prefetch-on-demand_controller-EGnyR3S.js", "/assets/@spomky-labs/pwa-bundle/fullscreen_controller-cDz4gEY.js", "/assets/@spomky-labs/pwa-bundle/barcode-detection_controller-2wM_rT7.js", "/assets/@spomky-labs/pwa-bundle/web-share_controller-WHj4CRa.js", "/assets/@spomky-labs/pwa-bundle/file-handling_controller-F0Y1cZK.js", "/assets/@spomky-labs/pwa-bundle/device-orientation_controller-IHb4jim.js", "/assets/@spomky-labs/pwa-bundle/touch_controller-mAGtlT0.js", "/assets/@spomky-labs/pwa-bundle/battery_controller-1zvUlM8.js", "/assets/@spomky-labs/pwa-bundle/service-worker_controller-rj3_R_-.js", "/assets/@spomky-labs/pwa-bundle/network-information_controller-a36uKfa.js", "/assets/@spomky-labs/pwa-bundle/backgroundsync-queue_controller-GzsTYhv.js", "/assets/@spomky-labs/pwa-bundle/device-motion_controller-Vn1KsVn.js", "/assets/@spomky-labs/pwa-bundle/vibration_controller-WWNgRJt.js", "/assets/@spomky-labs/pwa-bundle/backgroundsync-form_controller-gK5yd8x.js", "/assets/@spomky-labs/pwa-bundle/web-push_controller-czf9Gth.js", "/assets/@spomky-labs/pwa-bundle/geolocation_controller-eLT6hhX.js", "/assets/@spomky-labs/pwa-bundle/jwt_signer-rMDxy1b.js", "/assets/@spomky-labs/pwa-bundle/speech-synthesis_controller-P10zBDz.js", "/assets/@spomky-labs/pwa-bundle/presentation_controller-Ja4LWIU.js", "/assets/@spomky-labs/pwa-bundle/receiver_controller-auggO9d.js", "/assets/@spomky-labs/pwa-bundle/background-fetch_controller-RRspbNT.js", "/assets/@spomky-labs/pwa-bundle/wake-lock_controller-AZuUNNo.js", "/assets/@spomky-labs/pwa-bundle/picture-in-picture_controller-BzBogPR.js", "/assets/@spomky-labs/pwa-bundle/helpers-jYq9oR_.js", "/assets/@spomky-labs/pwa-bundle/index-2g1hp95.js", "/assets/@spomky-labs/pwa-bundle/contact_controller--bz1Lhb.js", "/assets/PaymentForm-9z-bqNC.js", "/assets/admin-EgoTwn3.js", "/assets/app-NCj0QLm.js" ], event)); /**************************************************** END CACHE STRATEGY ****************************************************/ /**************************************************** CACHE STRATEGY ****************************************************/ // Strategy: CacheFirst // Match: ({request}) => request.destination === 'font' // Cache Name: fonts // Enabled: 1 // Needs Workbox: 1 // Method: GET // 1. Creation of the Workbox Cache Strategy object // 2. Register the route with the Workbox Router // 3. Add the assets to the cache when the service worker is installed const cache_2_0 = new workbox.strategies.CacheFirst({ cacheName: registerCacheName('fonts'),plugins: [new workbox.cacheableResponse.CacheableResponsePlugin({ "statuses": [ 0, 200 ] }), new workbox.expiration.ExpirationPlugin({ "maxEntries": 60, "maxAgeSeconds": 31536000 })] }); workbox.routing.registerRoute(({request}) => request.destination === 'font',cache_2_0,'GET'); /**************************************************** END CACHE STRATEGY ****************************************************/ /**************************************************** CACHE STRATEGY ****************************************************/ // Strategy: StaleWhileRevalidate // Match: ({url}) => url.origin === 'https://fonts.googleapis.com' // Cache Name: google-fonts-stylesheets // Enabled: 1 // Needs Workbox: 1 // Method: // 1. Creation of the Workbox Cache Strategy object // 2. Register the route with the Workbox Router // 3. Add the assets to the cache when the service worker is installed const cache_3_0 = new workbox.strategies.StaleWhileRevalidate({ cacheName: registerCacheName('google-fonts-stylesheets'),plugins: [] }); workbox.routing.registerRoute(({url}) => url.origin === 'https://fonts.googleapis.com',cache_3_0); /**************************************************** END CACHE STRATEGY ****************************************************/ /**************************************************** CACHE STRATEGY ****************************************************/ // Strategy: CacheFirst // Match: ({url}) => url.origin === 'https://fonts.gstatic.com' // Cache Name: google-fonts-webfonts // Enabled: 1 // Needs Workbox: 1 // Method: // 1. Creation of the Workbox Cache Strategy object // 2. Register the route with the Workbox Router // 3. Add the assets to the cache when the service worker is installed const cache_3_1 = new workbox.strategies.CacheFirst({ cacheName: registerCacheName('google-fonts-webfonts'),plugins: [new workbox.cacheableResponse.CacheableResponsePlugin({ "statuses": [ 0, 200 ] }), new workbox.expiration.ExpirationPlugin({ "maxEntries": 30, "maxAgeSeconds": 31536000 })] }); workbox.routing.registerRoute(({url}) => url.origin === 'https://fonts.gstatic.com',cache_3_1); /**************************************************** END CACHE STRATEGY ****************************************************/ /**************************************************** CACHE STRATEGY ****************************************************/ // Strategy: CacheFirst // Match: ({request, url}) => (request.destination === 'image' && !url.pathname.startsWith('/assets')) // Cache Name: images // Enabled: 1 // Needs Workbox: 1 // Method: // 1. Creation of the Workbox Cache Strategy object // 2. Register the route with the Workbox Router // 3. Add the assets to the cache when the service worker is installed const cache_4_0 = new workbox.strategies.CacheFirst({ cacheName: registerCacheName('images'),plugins: [] }); workbox.routing.registerRoute(({request, url}) => (request.destination === 'image' && !url.pathname.startsWith('/assets')),cache_4_0); /**************************************************** END CACHE STRATEGY ****************************************************/ /**************************************************** CACHE CLEAR ****************************************************/ // The configuration is set to clear the cache on each install event // The following code will remove all the caches registerInstallTask(() => caches.keys().then(keys => Promise.all( keys .filter(k => usedCacheNames.has(k)) .map(k => caches.delete(k)) ) ) , 0); /**************************************************** END CACHE CLEAR ****************************************************/ /**************************************************** SKIP WAITING ****************************************************/ // The configuration is set to skip waiting on each install event registerInstallTask(() => self.skipWaiting(), 5); self.addEventListener("activate", function (event) { event.waitUntil(self.clients.claim()); }); /**************************************************** END SKIP WAITING ****************************************************/