diff --git a/src/index.tsx b/src/index.tsx index c7f610d4..8dcefbb8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,20 +22,18 @@ declare global { setInitialPage(); -if (SW) { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register(`sw.js`, { - scope: '.' // <--- THIS BIT IS REQUIRED +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register(`sw.js`, { + scope: '.' // <--- THIS BIT IS REQUIRED + }) + .then((registration) => { + // eslint-disable-next-line no-console + console.log('Registration successful, scope is:', registration.scope); }) - .then((registration) => { - // eslint-disable-next-line no-console - console.log('Registration successful, scope is:', registration.scope); - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.log('Service worker registration failed, error:', error); - }); - } + .catch((error) => { + // eslint-disable-next-line no-console + console.log('Service worker registration failed, error:', error); + }); } diff --git a/src/sw.ts b/src/sw.ts index dd98b513..55fba97c 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -1,91 +1,226 @@ import { addLangs } from 'consts'; -const CACHE_NAME = 'interslavic-dictionary'; -const cacheUrls = [ - 'index.html', - 'data/basic.json', - 'data/translateStatistic.json', - ...addLangs.map((lang) => `data/${lang}.json`), - 'grammarComponent.js', - 'communityComponent.js', - 'index.js', - 'sw.js', - 'styles/grammarComponent.css', - 'styles/communityComponent.css', - 'styles/index.css', +const urlsToPrefetch = [ + '/', + '/data/basic.json', + '/data/translateStatistic.json', + ...addLangs.map((lang) => `/data/${lang}.json`), + '/grammarComponent.js', + '/communityComponent.js', + '/index.js', + '/sw.js', + '/styles/grammarComponent.css', + '/styles/communityComponent.css', + '/styles/index.css', + '/icons/android-icon-36x36.png', + '/icons/android-icon-48x48.png', + '/icons/android-icon-72x72.png', + '/icons/android-icon-96x96.png', + '/icons/android-icon-144x144.png', + '/icons/android-icon-192x192.png', + '/icons/apple-icon.png', + '/icons/apple-icon-57x57.png', + '/icons/apple-icon-60x60.png', + '/icons/apple-icon-72x72.png', + '/icons/apple-icon-76x76.png', + '/icons/apple-icon-114x114.png', + '/icons/apple-icon-120x120.png', + '/icons/apple-icon-144x144.png', + '/icons/apple-icon-152x152.png', + '/icons/apple-icon-180x180.png', + '/icons/apple-icon-precomposed.png', + '/icons/discord-icon-330x102.png', + '/icons/favicon.ico', + '/icons/favicon-16x16.png', + '/icons/favicon-32x32.png', + '/icons/favicon-96x96.png', + '/icons/icon-72x72.png', + '/icons/icon-96x96.png', + '/icons/icon-128x128.png', + '/icons/icon-144x144.png', + '/icons/icon-152x152.png', + '/icons/icon-192x192.png', + '/icons/icon-384x384.png', + '/icons/icon-512x512.png', + '/icons/manifest.json', + '/icons/ms-icon-70x70.png', + '/icons/ms-icon-144x144.png', + '/icons/ms-icon-150x150.png', + '/icons/ms-icon-310x310.png', ]; -self.addEventListener('install', (event: any) => { +const version = '1.0.0' + +self.addEventListener("install", (event: any) => { + // eslint-disable-next-line no-console + console.log('WORKER: install event in progress.'); event.waitUntil( - caches.open(CACHE_NAME).then((cache) => cache.addAll(cacheUrls)) + /* The caches built-in is a promise-based API that helps you cache responses, + as well as finding and deleting them. + */ + caches + /* You can open a cache by name, and this method returns a promise. We use + a versioned cache name here so that we can remove old cache entries in + one fell swoop later, when phasing out an older service worker. + */ + .open(version + 'fundamentals') + .then(function(cache) { + /* After the cache is opened, we can fill it with the offline fundamentals. + The method below will add all resources we've indicated to the cache, + after making HTTP requests for each of them. + */ + return cache.addAll(urlsToPrefetch); + }) + .then(function() { + // eslint-disable-next-line no-console + console.log('WORKER: install completed'); + }) ); }); -self.addEventListener("activate", (event: any) => { - async function deleteOldCaches() { - // List all caches by their names. - const names = await caches.keys(); - await Promise.all(names.map(name => { - if (name !== CACHE_NAME) { - // If a cache's name is the current name, delete it. - return caches.delete(name); - } - })); +self.addEventListener("fetch", (event: any) => { + // eslint-disable-next-line no-console + console.log('WORKER: fetch event in progress.'); + + /* We should only cache GET requests, and deal with the rest of method in the + client-side, by handling failed POST,PUT,PATCH,etc. requests. + */ + if (event.request.method !== 'GET') { + /* If we don't block the event as shown below, then the request will go to + the network as usual. + */ + // eslint-disable-next-line no-console + console.log('WORKER: fetch event ignored.', event.request.method, event.request.url); + + return; } + /* Similar to event.waitUntil in that it blocks the fetch event on a promise. + Fulfillment result will be used as the response, and rejection will end in a + HTTP response indicating failure. + */ + event.respondWith( + caches + /* This method returns a promise that resolves to a cache entry matching + the request. Once the promise is settled, we can then provide a response + to the fetch request. + */ + .match(event.request) + .then(function(cached) { + /* Even if the response is in our cache, we go to the network as well. + This pattern is known for producing "eventually fresh" responses, + where we return cached responses immediately, and meanwhile pull + a network response and store that in the cache. + Read more: + https://ponyfoo.com/articles/progressive-networking-serviceworker + */ + const networked = fetch(event.request) + // We handle the network request with success and failure scenarios. + .then(fetchedFromNetwork, unableToResolve) + // We should catch errors on the fetchedFromNetwork handler as well. + .catch(unableToResolve); - event.waitUntil(deleteOldCaches()); -}); + /* We return the cached response immediately if there is one, and fall + back to waiting on the network as usual. + */ + // eslint-disable-next-line no-console + console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url); -window.addEventListener("online", () => { - // eslint-disable-next-line no-console - console.log("You are online!"); -}); -window.addEventListener("offline",() => { - // eslint-disable-next-line no-console - console.log("Network connection lost!"); -}); + return cached || networked; -const MAX_AGE = 1000 * 60 * 10; // 10 minutes. + function fetchedFromNetwork(response: any) { + /* We copy the response before replying to the network request. + This is the response that will be stored on the ServiceWorker cache. + */ + const cacheCopy = response.clone(); -self.addEventListener('fetch', (event: any) => { - event.respondWith( - // Trying find resource in cache. - caches.match( - event.request, - { - ignoreSearch: event.request.url.indexOf('?') != -1, - }, - ).then((cachedResponse) => { - let lastModified; - let fetchRequest; - - // If exist. - if (cachedResponse) { - // Get date of last update. - lastModified = new Date(cachedResponse.headers.get('last-modified')); - // If it is expired - if (lastModified && (Date.now() - lastModified.getTime()) > MAX_AGE) { - fetchRequest = event.request.clone(); - - // Cretae new. - return fetch(fetchRequest).then((response) => { - // If error then load from cache. - if (!response || response.status !== 200) { - return cachedResponse; - } - // Update cache. - caches.open(CACHE_NAME).then((cache) => cache.put(event.request, response)); - - // Return new data. - return response.clone(); - }).catch(() => cachedResponse); + // eslint-disable-next-line no-console + console.log('WORKER: fetch response from network.', event.request.url); + + caches + // We open a cache to store the response for this request. + .open(version + 'pages') + .then(function add(cache) { + /* We store the response for this request. It'll later become + available to caches.match(event.request) calls, when looking + for cached responses. + */ + cache.put(event.request, cacheCopy); + }) + .then(function() { + // eslint-disable-next-line no-console + console.log('WORKER: fetch response stored in cache.', event.request.url); + }); + + // Return the response so that the promise is settled in fulfillment. + return response; + } + + /* When this method is called, it means we were unable to produce a response + from either the cache or the network. This is our opportunity to produce + a meaningful response even when all else fails. It's the last chance, so + you probably want to display a "Service Unavailable" view or a generic + error response. + */ + function unableToResolve () { + /* There's a couple of things we can do here. + - Test the Accept header and then return one of the `offlineFundamentals` + e.g: `return caches.match('/some/cached/image.png')` + - You should also consider the origin. It's easier to decide what + "unavailable" means for requests against your origins than for requests + against a third party, such as an ad provider + - Generate a Response programmaticaly, as shown below, and return that + */ + + // eslint-disable-next-line no-console + console.log('WORKER: fetch request failed in both cache and network.'); + + /* Here we're creating a response programmatically. The first parameter is the + response body, and the second one defines the options for the response. + */ + return new Response('