Skip to content

Instantly share code, notes, and snippets.

@DannyMoerkerke
Last active December 1, 2020 08:54
Show Gist options
  • Save DannyMoerkerke/b204acfe6ed029d7e8a95f918c3553d9 to your computer and use it in GitHub Desktop.
Save DannyMoerkerke/b204acfe6ed029d7e8a95f918c3553d9 to your computer and use it in GitHub Desktop.
Full-fledged service worker which caches API requests and responses using IndexedDB
const version = 1;
const cacheName = `our_awesome_cache`
// the static files we want to cache
const staticFiles = [
'index.html',
'src/css/styles.css',
'src/img/logo.png',
'src/js/app.js'
];
// here we configure IndexedDB with two stores: one for api requests and one for api responses
// 'keyPath' is the key we use to retrieve the request or response, in this case it's the url
// of the request or response
const IDBConfig = {
name: 'my_awesome_idb',
version,
stores: [
{
name: 'api_requests',
keyPath: 'url'
},
{
name: 'api_responses',
keyPath: 'url'
}
]
};
// here we create our IndexedDB database
const createIndexedDB = ({name, version, stores}) => {
const request = self.indexedDB.open(name, version);
return new Promise((resolve, reject) => {
request.onupgradeneeded = e => {
const db = e.target.result;
// here we loop over our stores (one for api requests, one for api responses)
// we check if they already exist. If not, they are created
stores.map(({name, keyPath}) => {
if(!db.objectStoreNames.contains(name)) {
db.createObjectStore(name, {keyPath});
}
});
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
// here we prefetch the api call and cache the response so we can serve it later
const prefetch = async url => {
const response = await fetch(url);
// since a response can only be used once, we clone it to store it and then serve the original response
const clone = response.clone();
const json = await clone.json();
cacheApiResponse({url, json});
return response.json();
};
// factory function to get the correct store
// we pass it the name and version of the IndexedDB database name and version and it returns
// a function that we pass the name of the correct store (api requests or api responses)
const getStoreFactory = (dbName, version) => ({name}, mode = 'readonly') => {
return new Promise((resolve, reject) => {
const request = self.indexedDB.open(dbName, version);
request.onsuccess = e => {
const db = request.result;
const transaction = db.transaction(name, mode);
const store = transaction.objectStore(name);
return resolve(store);
};
request.onerror = e => reject(request.error);
});
};
// function to open the correct store, uses the factory function
const openStore = getStoreFactory(IDBConfig.name, version);
// function that caches a POST request made to our api so we can retry it when we're back online
const cacheApiRequest = async request => {
const headers = [...request.headers.entries()].reduce((obj, [key, value]) => Object.assign(obj, {[`${key}`]: value}), {});
const body = await request.text();
const serialized = {
headers,
body,
url: request.url,
method: request.method,
mode: request.mode,
credentials: request.credentials,
cache: request.cache,
redirect: request.redirect,
referrer: request.referrer
};
const requestStore = await openStore(IDBConfig.stores[0], 'readwrite');
requestStore.add(serialized);
};
// here we cache the api responses so we can use them later if needed when offline
const cacheApiResponse = async response => {
try {
const store = await openStore(IDBConfig.stores[1], 'readwrite');
store.add(response);
}
catch(error) {
console.log('idb error', error);
}
};
// here we return a cached api response whenever our api is not available
const getCachedApiResponse = request => {
return new Promise((resolve, reject) => {
openStore(IDBConfig.stores[1])
.then(store => {
const cachedRequest = store.get(request.url);
// when there's no error the 'onsuccess' handler is called, but that doesn't mean we have a response in our cache
// we still have to check if the result is not 'undefined'
cachedRequest.onsuccess = e => {
return cachedRequest.result === undefined ? resolve(null) : resolve(new Response(JSON.stringify(cachedRequest.result.json)));
};
cachedRequest.onerror = e => {
console.log('cached response error', e, cachedRequest.error);
return reject(cachedRequest.error);
};
});
});
};
// here we make a call to the api and then cache the response for later use
const networkThenCache = async request => {
const {method, url} = request;
const requestClone = request.clone();
try {
const response = await fetch(request);
const json = await response.clone().json();
if(method === 'GET') {
cacheApiResponse({url, json});
}
return response;
}
// if an error occured and it was a POST call, we cache the api request so we can retry it later
// otherwise we serve a fallback response since there is no response in the cache and the network is not available
catch(e) {
return method === 'POST' ? cacheApiRequest(requestClone) : new Response(JSON.stringify({message: 'no response'}));
}
};
// here we try to serve a cached api response and if it doesn't exist we try the network
const getCachedOrNetworkApiResponse = async request => await getCachedApiResponse(request) || networkThenCache(request);
// here we retry any cached api requests that were stored earlier when the network was not available
// this is typically called when the client comes back online and the 'sync' event is fired
const retryApiCalls = () => {
return new Promise((resolve, reject) => {
openStore(IDBConfig.stores[0])
.then(store => {
const cursor = store.openCursor();
cursor.onsuccess = e => {
const cursor = e.target.result;
if(cursor) {
fetch(new Request(cursor.value.url, cursor.value))
.then(() => resolve(true));
cursor.continue();
}
};
});
});
};
// when our service worker is installed we populate the cache with our static assets
const installHandler = e => {
e.waitUntil(
caches.open(cacheName)
.then(cache => cache.addAll(staticFiles))
);
};
const activateHandler = e => {
if(self.indexedDB) {
createIndexedDB(IDBConfig);
}
e.waitUntil(async function() {
const postings = [
'/api/blog/1',
'/api/blog/12',
'/api/blog/7',
];
await Promise.all(postings.map(url => prefetch(url)));
}());
};
// our fetch handler which is called on every outgoing request
const fetchHandler = async e => {
const {request} = e;
const {url} = request;
const {pathname} = new URL(url);
// if there's a call to our api, we try to serve a cached response, otherwise we call the api
// and cache the response for later use
if(url.includes('/api')) {
e.respondWith(getCachedOrNetworkApiResponse(request));
}
// if it's a request for a static asset we serve it from the cache and when it's not cached we
// fetch it from the network
else {
e.respondWith(
caches.match(request)
.then(response => response ? response : fetch(request))
);
}
};
// when we were offline and come back online, a 'sync' event is fired
// we can now retry any api requests that were cached earlier (if any)
const syncHandler = e => {
e.waitUntil(retryApiCalls());
};
self.addEventListener('install', installHandler);
self.addEventListener('activate', activateHandler);
self.addEventListener('fetch', fetchHandler);
self.addEventListener('sync', syncHandler);
@ankit-kumar-jat
Copy link

ankit-kumar-jat commented Dec 1, 2020

Thanks you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment