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('

Service Unavailable

', { + status: 503, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); } - - return cachedResponse; - } + }) + ); +}); + +self.addEventListener("activate", (event: any) => { + /* Just like with the install event, event.waitUntil blocks activate on a promise. + Activation will fail unless the promise is fulfilled. + */ + // eslint-disable-next-line no-console + console.log('WORKER: activate event in progress.'); - // Request from network. - return fetch(event.request); - }) + event.waitUntil( + caches + /* This method returns a promise which will resolve to an array of available + cache keys. + */ + .keys() + .then(function (keys) { + // We return a promise that settles when all outdated caches are deleted. + return Promise.all( + keys + .filter(function (key) { + // Filter by keys that don't start with the latest version prefix. + return !key.startsWith(version); + }) + .map(function (key) { + /* Return a promise that's fulfilled + when each outdated cache is deleted. + */ + return caches.delete(key); + }) + ); + }) + .then(function() { + // eslint-disable-next-line no-console + console.log('WORKER: activate completed.'); + }) ); }); diff --git a/static/manifest.json b/static/manifest.json index 0ad51ae2..95ff4e6c 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -4,7 +4,17 @@ "description": "Word dictionary for interslavic language", "lang": "en", "dir": "ltr", - "scope": "https://pr-342.interslavic-dictionary.pages.dev", + "scope": ".", + "screenshots": [ + { + "src": "screenshot/android.png", + "sizes": "576x1280", + "type": "image/png", + "form_factor": "narrow", + "platform": "android", + "label": "Android PWA" + } + ], "icons": [ { "src": "icons/icon-72x72.png", @@ -48,11 +58,11 @@ } ], "serviceworker": { - "src": "sw.prod.js", - "scope": "https://pr-342.interslavic-dictionary.pages.dev", + "src": "sw.js", + "scope": ".", "update_via_cache": "none" }, - "start_url": "https://pr-342.interslavic-dictionary.pages.dev", + "start_url": "/", "background_color": "#272727", "display": "standalone", "orientation": "portrait-primary", diff --git a/static/screenshots/android.png b/static/screenshots/android.png new file mode 100644 index 00000000..e297610d Binary files /dev/null and b/static/screenshots/android.png differ