diff --git a/app/client/main.js b/app/client/main.js index 85626b55..4836e4d2 100644 --- a/app/client/main.js +++ b/app/client/main.js @@ -1,3 +1,4 @@ import '/imports/ui/vueSetup.js'; import '/imports/ui/styles/stylesIndex.js'; import '/imports/client/config.js'; +import '/imports/client/serviceWorker.js'; diff --git a/app/imports/client/serviceWorker.js b/app/imports/client/serviceWorker.js new file mode 100644 index 00000000..5caa9669 --- /dev/null +++ b/app/imports/client/serviceWorker.js @@ -0,0 +1,5 @@ +Meteor.startup(() => { + navigator.serviceWorker.register('/sw.js') + .then() + .catch(error => console.log('ServiceWorker registration failed: ', error)); +}); diff --git a/app/public/sw.js b/app/public/sw.js new file mode 100644 index 00000000..2294ca69 --- /dev/null +++ b/app/public/sw.js @@ -0,0 +1,95 @@ +const HTMLToCache = '/'; +const version = 'MSW V0.3'; + +self.addEventListener('install', (event) => { + event.waitUntil(caches.open(version).then((cache) => { + cache.add(HTMLToCache).then(self.skipWaiting()); + })); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then(cacheNames => Promise.all(cacheNames.map((cacheName) => { + if (version !== cacheName) return caches.delete(cacheName); + }))).then(self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + // only processes http:// & https:// requests, prevents chrome-extention:// errors + if (event.request.url.startsWith('http')){ + + const requestToFetch = event.request.clone(); + event.respondWith( + caches.match(event.request.clone()).then((cached) => { + // We don't return cached HTML (except if fetch failed) + if (cached) { + const resourceType = cached.headers.get('content-type'); + // We only return non css/js/html cached response e.g images + if (!hasHash(event.request.url) && !/text\/html/.test(resourceType)) { + return cached; + } + + // If the CSS/JS didn't change since it's been cached, return the cached version + if (hasHash(event.request.url) && hasSameHash(event.request.url, cached.url)) { + return cached; + } + } + return fetch(requestToFetch).then((response) => { + const clonedResponse = response.clone(); + const contentType = clonedResponse.headers.get('content-type'); + + if (!clonedResponse || clonedResponse.status !== 200 || clonedResponse.type !== 'basic' + || /\/sockjs\//.test(event.request.url)) { + return response; + } + + if (/html/.test(contentType)) { + caches.open(version).then(cache => cache.put(HTMLToCache, clonedResponse)); + } else { + // Delete old version of a file + if (hasHash(event.request.url)) { + caches.open(version).then(cache => cache.keys().then(keys => keys.forEach((asset) => { + if (new RegExp(removeHash(event.request.url)).test(removeHash(asset.url))) { + cache.delete(asset); + } + }))); + } + + caches.open(version).then(cache => cache.put(event.request, clonedResponse)); + } + return response; + }).catch(() => { + if (hasHash(event.request.url)) return caches.match(event.request.url); + // If the request URL hasn't been served from cache and isn't sockjs we suppose it's HTML + else if (!/\/sockjs\//.test(event.request.url)) return caches.match(HTMLToCache); + // Only for sockjs + return new Response('No connection to the server', { + status: 503, + statusText: 'No connection to the server', + headers: new Headers({ 'Content-Type': 'text/plain' }), + }); + }); + }) + ); + + } + +}); + +function removeHash(element) { + if (typeof element === 'string') return element.split('?hash=')[0]; +} + +function hasHash(element) { + if (typeof element === 'string') return /\?hash=.*/.test(element); +} + +function hasSameHash(firstUrl, secondUrl) { + if (typeof firstUrl === 'string' && typeof secondUrl === 'string') { + return /\?hash=(.*)/.exec(firstUrl)[1] === /\?hash=(.*)/.exec(secondUrl)[1]; + } +} + +// Service worker created by Ilan Schemoul alias NitroBAY as a specific Service Worker for Meteor +// Please see https://github.com/NitroBAY/meteor-service-worker for the official project source