Last active
October 30, 2017 07:27
-
-
Save senthilp/2e230f6b762e14471d315d7ebaeff24b to your computer and use it in GitHub Desktop.
Offline Service Worker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const VERSION = '{%VERSION%}'; | |
const ASSETS = []; | |
const ALLOWED_URL_PATHS = [ | |
'/' | |
]; | |
let offlineReady = false; | |
let offlinePage = undefined; | |
function requestExpectsHTML(headers) { | |
if (!headers) { | |
return false; | |
} | |
const acceptHeader = headers.get("Accept"); | |
if (acceptHeader) { | |
return acceptHeader.indexOf('text/html') !== -1; | |
} | |
return false; | |
} | |
function isUrlPathAllowed(path) { | |
return ALLOWED_URL_PATHS.some(allowedPath => { | |
// Special check for root | |
if (allowedPath === '/') { | |
return allowedPath === path; | |
} | |
return new RegExp(allowedPath).test(path); | |
}); | |
} | |
async function swInstall() { | |
const cache = await caches.open(VERSION); | |
await cache.addAll(ASSETS); | |
await self.skipWaiting(); | |
} | |
async function swActivate() { | |
const keys = await caches.keys(); | |
const deletes = []; | |
for (const key of keys) { | |
if (key !== VERSION) { | |
deletes.push(caches.delete(key)); | |
} | |
} | |
await Promise.all(deletes); | |
if (self.registration.navigationPreload) { | |
// Enable navigation preloads! | |
await self.registration.navigationPreload.enable(); | |
} | |
await self.clients.claim(); | |
} | |
async function addToCache(req, res) { | |
const cache = await caches.open(VERSION); | |
cache.put(req, res); | |
} | |
async function updateCacheEntities(entitiesToKeep) { | |
const cache = await caches.open(VERSION); | |
const cacheKeys = await cache.keys(); | |
const existingEntities = cacheKeys.map(key => key.url); | |
const entitiesToDelete = existingEntities | |
.filter(entity => !entitiesToKeep.includes(entity) && !ASSETS.includes(entity)); | |
await Promise.all(entitiesToDelete.map(entityToDelete => cache.delete(entityToDelete))); | |
} | |
async function addCacheEntities(entities) { | |
const cache = await caches.open(VERSION); | |
const cacheKeys = await cache.keys(); | |
const existingEntities = cacheKeys.map(key => key.url); | |
const entitiesToAdd = entities.filter(entity => !existingEntities.includes(entity)); | |
await cache.addAll(entitiesToAdd); | |
} | |
async function fretchFromCache(req) { | |
const cache = await caches.open(VERSION); | |
const cacheRes = await cache.match(req); | |
if (!cacheRes) { | |
throw Error(`Item not found in cache`); | |
} | |
return cacheRes; | |
} | |
async function fetchFromNetworkAndCache(req) { | |
const res = await fetch(req); | |
addToCache(req, res.clone()); | |
return res; | |
} | |
async function fetchNetworkFirst(req) { // eslint-disable-line no-unused-vars | |
const reasons = []; | |
// Try netwrok first | |
try { | |
return await fetchFromNetworkAndCache(req); | |
} catch (e) { | |
reasons.push(e.message); | |
} | |
// Network failed so try cache | |
try { | |
return await fretchFromCache(req); | |
} catch (e) { | |
reasons.push(e.message); | |
} | |
// Even cache failed so fallback to browser default | |
throw Error(reasons.join(`, `)); | |
} | |
async function fetchFastest(req) { // eslint-disable-line no-unused-vars | |
return new Promise((resolve, reject) => { | |
const networkFetch = fetchFromNetworkAndCache(req); | |
const cacheFetch = fretchFromCache(req); | |
let rejected = false; | |
const reasons = []; | |
const maybeReject = reason => { | |
reasons.push(reason.toString()); | |
if (rejected) { | |
reject(Error(reasons.join(`, `))); | |
} else { | |
rejected = true; | |
} | |
}; | |
// Whichever resolves first will be the winner | |
cacheFetch.then(resolve, maybeReject); | |
networkFetch.then(resolve, maybeReject); | |
}); | |
} | |
async function prepOffline(e) { | |
offlineReady = false; | |
try { | |
const offlineDataRes = await fetch(e.data.offlineSrc); | |
const offlineData = await offlineDataRes.json(); | |
const offlineAssets = offlineData.assets; | |
await updateCacheEntities(offlineAssets); | |
// Set and add offline page to the asset queue | |
offlinePage = offlineData.page; | |
offlineAssets.push(offlinePage); | |
await addCacheEntities(offlineAssets); | |
offlineReady = true; | |
} catch (ex) { | |
// Offline Prep failed | |
} | |
} | |
async function swFetch(e) { | |
// Initial checks, return immediately if | |
// 1. user is online | |
// or | |
// 2. Offline cache is not ready | |
if (navigator.onLine || !offlineReady) { | |
return; | |
} | |
const req = e.request; | |
const url = new URL(req.url); | |
if (req.method !== 'GET') { | |
return; | |
} | |
if (requestExpectsHTML(req.headers)) { | |
if (url.origin === location.origin && isUrlPathAllowed(url.pathname)) { | |
e.respondWith(fretchFromCache(offlinePage)); | |
} | |
} else { | |
e.respondWith(fretchFromCache(req)); | |
} | |
} | |
self.addEventListener('install', e => e.waitUntil(swInstall())); | |
self.addEventListener('activate', e => e.waitUntil(swActivate())); | |
self.addEventListener('fetch', e => swFetch(e)); | |
self.addEventListener('message', e => prepOffline(e)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function swFetch(e) { | |
// Initial checks, return immediately if | |
// 1. user is online | |
// or | |
// 2. Offline cache is not ready | |
if (navigator.onLine || !offlineReady) { | |
return; | |
} | |
const req = e.request; | |
const url = new URL(req.url); | |
if (req.method !== 'GET') { | |
return; | |
} | |
if (requestExpectsHTML(req.headers)) { | |
if (url.origin === location.origin && isUrlPathAllowed(url.pathname)) { | |
e.respondWith(fretchFromCache(offlinePage)); | |
} | |
} else { | |
e.respondWith(fretchFromCache(req)); | |
} | |
} | |
self.addEventListener('fetch', e => swFetch(e)); |
If you're looking for "lie-fi" timeout code and don't want to adopt the full Workbox or sw-toolbox
libraries, you can borrow from one of these implementations:
event.request.mode === 'navigate'
is a great idea. Will incorporate that.
W.r.t. the message
passing, there is some tricky logic on the page side. So, for now, will stick with it.
Thanks for "lie-fi" timeout logic. Will try to incorporate that too.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As mentioned, you might be able to move from calling
prepOffline()
from within amessage
handler to wrapping it withevent.waitUntil()
and putting it in yourfetch
handler, after you callevent.respondWith()
with your navigation response.The one wrinkle is that
e.data.offlineSrc
property that you need withinprepOffline()
—if you can automatically infer it based on the navigation URL then that would work, but if you need to calculate it using logic on the client page, then I guess sticking withmessage
passing would make the most sense.