/* * * xvServiceWorker * Copyright 2019-2023 by xvanced.com. All rights reserved. * */ /* global self, caches, clients, fetch, location, URL, MessageChannel */ // Declare constants const DEBUG = new URL(location).searchParams.get('debugLog') === 'true'; const VERSION = '0.10.0'; const CACHE_PREFIX = `xv_application-`; const PRECACHE = CACHE_PREFIX + `precache-v${VERSION}`; const RUNTIME = CACHE_PREFIX + `runtime-v${VERSION}`; const OFFLINE_URL = '/offline/'; // A list of local resources we always want to be cached. const PRECACHE_URLS = [ `/`, OFFLINE_URL, `/favicon.ico` ]; let PRECACHE_BUILD_ASSETS = [ `https://cdn.xvanced.com/frontend/0.10.0/css/main.css`, `https://cdn.xvanced.com/frontend/0.10.0/js/vendors~index.bundle.js`, `https://cdn.xvanced.com/frontend/0.10.0/js/index.bundle.js` ]; // The install-handler takes care of pre-caching the resources we always need. self.addEventListener('install', event => { let logo = new URL(location).searchParams.get('logo'), urlsToCache = []; if (DEBUG) { console.log('xvServiceworker->install :: Installing service-worker:', event); console.log('xvServiceworker->install :: Logo:', logo); } urlsToCache = PRECACHE_URLS.concat(PRECACHE_BUILD_ASSETS); urlsToCache.push(logo); urlsToCache = urlsToCache.map(url => new URL(url, location.href)); if (DEBUG) { console.log('xvServiceworker->install :: urlsToCache: ', urlsToCache); } event.waitUntil( caches.open(PRECACHE) .then(cache => cache.addAll(urlsToCache)) .then(self.skipWaiting()) ); }); // The activate handler takes care of cleaning up old caches. self.addEventListener('activate', event => { const currentCaches = [PRECACHE, RUNTIME]; if (DEBUG) { console.log('xvServiceworker->activate :: Activating service-worker:', event); } event.waitUntil( caches.keys().then(cacheNames => { return cacheNames.filter(cacheName => !currentCaches.includes(cacheName)); }).then(cachesToDelete => { return Promise.all(cachesToDelete.map(cacheToDelete => { return caches.delete(cacheToDelete); })); }).then(() => self.clients.claim()) ); }); self.addEventListener('fetch', event => { let requestMode = event.request.mode, requestMethod = event.request.method, requestUrl = event.request.url, requestUrlObject = new URL(requestUrl), isSameOrigin = requestUrl.startsWith(self.location.origin), isFrameworkOrigin = requestUrl.startsWith('https://cdn.xvanced.com/frontend/'), logCss = 'font-weight: normal;'; // Skip cross-origin requests, like those for Google Analytics. if (!isSameOrigin && !isFrameworkOrigin) { if (DEBUG) { logCss += 'color: #ddd;'; console.log('%cxvServiceworker->fetch :: external URL', logCss); } return; } // Hotfix // https://github.com/paulirish/caltrainschedule.io/issues/49 if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { return; } if (isSameOrigin || isFrameworkOrigin) { // Don't interfere in following paths if (/^\/(assets\/components|connectors|core|manager|setup)\//.test(requestUrlObject.pathname) || /^\?add_to_cart/.test(requestUrlObject.search)) { if (DEBUG) { logCss += 'color: #dea;'; console.log('%cxvServiceworker->fetch :: wrong path', logCss); } return; } // Don't interfere with anything but GET if (requestMethod !== 'GET') { if (DEBUG) { logCss += 'color: #eda;'; console.log('%cxvServiceworker->fetch :: wrong requestMethod', logCss); } return; } if (DEBUG) { console.groupCollapsed('%cxvServiceworker->fetch [' + requestUrlObject.pathname + ']', logCss); console.log('xvServiceworker->fetch :: Event: ', event); console.log('xvServiceworker->fetch :: Url: ', requestUrl); console.log('xvServiceworker->fetch :: Mode: ', requestMode); console.log('xvServiceworker->fetch :: Method: ', requestMethod); } // When user is navigating, catch an error by returning the offline-page if (requestMode === 'navigate' || (requestMethod === 'GET' && event.request.headers.get('accept').includes('text/html') && !event.request.headers.get('range'))) { event.respondWith( // TODO: Implement "Cache, update and refresh"-strategy (https://serviceworke.rs/strategy-cache-update-and-refresh.html) fetch(event.request) .then(response => { if (DEBUG) { console.log('%cxvServiceworker->fetch :: Fetched a fresh copy of the document', 'font-style: italic;'); } return caches.open(RUNTIME).then(cache => { if (DEBUG) { console.log('%cxvServiceworker->fetch :: Put a copy of the document in the runtime cache', 'font-style: italic;'); } return cache.put(event.request, response.clone()).then(() => { if (DEBUG) { logCss += 'color: #399;'; console.log('%cxvServiceworker->fetch :: Return newly cloned document', logCss, 'to clientID: ' + event.clientID); } return response; }); }); }) .catch(error => { return caches.match(event.request).then(function (response) { if (response) { // Return from cache if (DEBUG) { console.log('%cxvServiceworker->fetch :: FAILED, but found in CACHE: [' + requestUrl + ']', 'color: #6a6; font-weight: bold;'); } return response; } else { // Return offline-page if (DEBUG) { logCss += 'color: #960;'; console.log('xvServiceworker->fetch :: FAILED', error); console.log('%cxvServiceworker->fetch :: returning offline page instead', logCss); } return caches.match(OFFLINE_URL); } }); }) ); } else if (event.request.headers.get('range')) { let pos = Number(/^bytes=(\d+)-$/g.exec(event.request.headers.get('range'))[1]); if (DEBUG) { logCss += 'color: #393;'; console.log('Range request for ' + event.request.url + ', starting position: ' + pos, logCss); } event.respondWith( caches.open(RUNTIME) .then(function (cache) { return cache.match(event.request.url); }).then(function (res) { if (!res) { return fetch(event.request) .then(res => { return res.arrayBuffer(); }); } return res.arrayBuffer(); }).then(function (ab) { return new Response( ab.slice(pos), { status: 206, statusText: 'Partial Content', headers: [ // ['Content-Type', 'video/webm'], ['Content-Range', 'bytes ' + pos + '-' + (ab.byteLength - 1) + '/' + ab.byteLength]] }); })); } else { // In any other event, return cached or network version of requested resource event.respondWith( caches.match(event.request).then(cachedResponse => { if (cachedResponse) { if (DEBUG) { logCss += 'color: #393;'; console.log('%cxvServiceworker->fetch :: returning cachedResponse', logCss); } return cachedResponse; } return caches.open(RUNTIME).then(cache => { return fetch(event.request).then(response => { if (DEBUG) { console.log('%cxvServiceworker->fetch :: Put a copy of the response in the runtime cache', logCss); } return cache.put(event.request, response.clone()).then(() => { if (DEBUG) { logCss += 'color: #399;'; console.log('%cxvServiceworker->fetch :: Return newly cloned response', logCss); } return response; }); }); }); }) ); } if (DEBUG) { console.groupEnd(); } } }); self.addEventListener('message', function (event) { if (DEBUG) { console.log('xvServiceworker->message :: SW received message:', event.data); console.log('xvServiceworker->message :: Message-Event:', event); } });