Skip to content

Instantly share code, notes, and snippets.

@prof3ssorSt3v3
Created March 13, 2021 22:17
Show Gist options
  • Save prof3ssorSt3v3/7724c092b7acd45048a2499c3ba223b4 to your computer and use it in GitHub Desktop.
Save prof3ssorSt3v3/7724c092b7acd45048a2499c3ba223b4 to your computer and use it in GitHub Desktop.
Code for Service Workers 9 - Integrating IndexedDB into site with a Service Worker
const APP = {
SW: null,
DB: null, //TODO:
init() {
//called after DOMContentLoaded
//register our service worker
APP.registerSW();
document
.getElementById('colorForm')
.addEventListener('submit', APP.saveColor);
//TODO:
APP.openDB();
},
registerSW() {
if ('serviceWorker' in navigator) {
// Register a service worker hosted at the root of the site
navigator.serviceWorker.register('/sw.js').then(
(registration) => {
APP.SW =
registration.installing ||
registration.waiting ||
registration.active;
},
(error) => {
console.log('Service worker registration failed:', error);
}
);
//listen for the latest sw
navigator.serviceWorker.addEventListener('controllerchange', async () => {
APP.SW = navigator.serviceWorker.controller;
});
//listen for messages from the service worker
navigator.serviceWorker.addEventListener('message', APP.onMessage);
} else {
console.log('Service workers are not supported.');
}
},
saveColor(ev) {
ev.preventDefault();
let name = document.getElementById('name');
let color = document.getElementById('color');
let strName = name.value.trim();
let strColor = color.value.trim();
if (strName && strColor) {
let person = {
id: Date.now(),
name: strName,
color: strColor,
};
console.log('Save', person);
//send the data to the service worker
//, otherAction: 'hello'
APP.sendMessage({ addPerson: person });
}
},
sendMessage(msg) {
//send some structured-cloneable data from the webpage to the sw
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(msg);
}
},
onMessage({ data }) {
//got a message from the service worker
console.log('Web page receiving', data);
//TODO: check for savedPerson and build the list and clear the form
if ('savedPerson' in data) {
APP.showPeople();
document.getElementById('name').value = '';
}
},
showPeople() {
//TODO: check for DB
if (!APP.DB) {
APP.openDB();
}
//TODO: start transaction to read names and build the list
let tx = APP.DB.transaction('colorStore', 'readonly');
let store = tx.objectStore('colorStore');
let req = store.getAll();
req.onsuccess = (ev) => {
let list = document.getElementById('people');
let ppl = ev.target.result;
list.innerHTML = ppl
.map((person) => {
console.log('show', person);
return `<li data-id="${person.id}">
${person.name}
<input type="color" value="${person.color}" disabled />
</li>`;
})
.join('\n');
};
},
openDB() {
let req = indexedDB.open('colorDB');
req.onsuccess = (ev) => {
APP.DB = ev.target.result;
APP.showPeople();
};
req.onerror = (err) => {
console.warn(err);
//NOT calling showPeople()
};
},
};
document.addEventListener('DOMContentLoaded', APP.init);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Intro to Service Workers</title>
<!-- import google fonts -->
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Lato&family=Montserrat:wght@400;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<header>
<h1>Intro to Service Workers</h1>
<h2>Integrating with IndexedDB</h2>
</header>
<main>
<form id="colorForm" name="colorForm">
<p>
<label for="name">Character Name</label>
<input type="text" id="name" name="name" />
</p>
<p>
<label for="color">Favourite Colour</label>
<input type="color" id="color" name="color" />
</p>
<p>
<button id="btnSave">Save</button>
</p>
</form>
<ul id="people">
<!-- TODO: list of people and their colors -->
</ul>
</main>
<script defer src="./js/app.js"></script>
</body>
</html>
html {
font-size: 20px;
font-family: 'Montserrat', sans-serif;
line-height: 1.5;
background-color: #222;
color: #eee;
}
body {
min-height: 100vh;
background-color: inherit;
color: inherit;
}
header,
main {
margin: 1rem 2rem;
}
main {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
justify-content: space-around;
}
h1 {
color: orangered;
font-weight: 700;
}
h2 {
color: orange;
font-weight: 700;
}
p {
font-family: 'Lato', sans-serif;
font-weight: 400;
}
form {
outline: 1px solid #999;
}
form p {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 0.5rem 1rem;
}
label,
input {
font-size: 1rem;
margin: 0.5rem 0;
}
main img {
width: clamp(200px, 400px, 600px);
}
main a {
color: orange;
text-decoration-style: wavy;
}
button {
font-size: 1rem;
background-color: cornflowerblue;
color: white;
padding: 0.25rem 2rem;
border: none;
}
const version = 3;
let staticName = `staticCache-${version}`;
let dynamicName = `dynamicCache`;
let imageName = `imageCache-${version}`;
let options = {
ignoreSearch: false,
ignoreMethod: false,
ignoreVary: false,
};
//starter html and css and js files
let assets = ['/', '/index.html', '/css/main.css', '/js/app.js', '/404.html'];
//starter images
let imageAssets = ['/img/1011-800x600.jpg', '/img/distracted-boyfriend.jpg'];
//TODO:
let DB = null;
self.addEventListener('install', (ev) => {
// service worker has been installed.
//Extendable Event
console.log(`Version ${version} installed`);
// build a cache
ev.waitUntil(
caches
.open(staticName)
.then((cache) => {
cache.addAll(assets).then(
() => {
//addAll == fetch() + put()
// console.log(`${staticName} has been updated.`);
},
(err) => {
console.warn(`failed to update ${staticName}.`);
}
);
})
.then(() => {
caches.open(imageName).then((cache) => {
cache.addAll(imageAssets).then(
() => {
console.log(`${imageName} has been updated.`);
},
(err) => {
console.warn(`failed to update ${staticName}.`);
}
);
});
})
);
});
self.addEventListener('activate', (ev) => {
// when the service worker has been activated to replace an old one.
//Extendable Event
console.log('activated');
// delete old versions of caches.
ev.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter((key) => {
if (key != staticName && key != imageName) {
return true;
}
})
.map((key) => caches.delete(key))
).then((empties) => {
//empties is an Array of boolean values.
//one for each cache deleted
//TODO:
openDB();
});
})
);
});
self.addEventListener('fetch', (ev) => {
// Extendable Event.
ev.respondWith(
caches.match(ev.request).then((cacheRes) => {
return (
cacheRes ||
Promise.resolve().then(() => {
let opts = {
mode: ev.request.mode, //cors, no-cors, same-origin, navigate
cache: 'no-cache',
};
if (!ev.request.url.startsWith(location.origin)) {
//not on the same domain as my html file
opts.mode = 'cors';
opts.credentials = 'omit';
}
return fetch(ev.request.url, opts).then(
(fetchResponse) => {
//we got a response from the server.
if (fetchResponse.ok) {
return handleFetchResponse(fetchResponse, ev.request);
}
//not ok 404 error
if (fetchResponse.status == 404) {
if (ev.request.url.match(/\.html/i)) {
return caches.open(staticName).then((cache) => {
return cache.match('/404.html');
});
}
if (
ev.request.url.match(/\.jpg$/i) ||
ev.request.url.match(/\.png$/i)
) {
return caches.open(imageName).then((cache) => {
return cache.match('/img/distracted-boyfriend.jpg');
});
}
}
},
(err) => {
//this is the network failure
//return the 404.html file if it is a request for an html file
if (ev.request.url.match(/\.html/i)) {
return caches.open(staticName).then((cache) => {
return cache.match('/404.html');
});
}
}
);
})
);
}) //end of match().then()
); //end of respondWith
}); //end of fetch listener
const handleFetchResponse = (fetchResponse, request) => {
let type = fetchResponse.headers.get('content-type');
// console.log('handle request for', type, request.url);
if (type && type.match(/^image\//i)) {
//save the image in image cache
// console.log(`SAVE ${request.url} in image cache`);
return caches.open(imageName).then((cache) => {
cache.put(request, fetchResponse.clone());
return fetchResponse;
});
} else {
//save in dynamic cache - html, css, fonts, js, etc
// console.log(`SAVE ${request.url} in dynamic cache`);
return caches.open(dynamicName).then((cache) => {
cache.put(request, fetchResponse.clone());
return fetchResponse;
});
}
};
self.addEventListener('message', (ev) => {
let data = ev.data;
//console.log({ ev });
let clientId = ev.source.id;
// console.log('Service Worker received', data, clientId);
if ('addPerson' in data) {
//TODO: really do something with the data
//TODO: open the database and wait for success
//TODO: start a transaction
//TODO: put() the data
//console.log({ DB });
//TODO: check for db then openDB or savePerson
if (DB) {
savePerson(data.addPerson, clientId);
} else {
openDB(() => {
savePerson(data.addPerson, clientId);
});
}
}
if ('otherAction' in data) {
let msg = 'Hola';
sendMessage({
code: 0,
message: msg,
});
}
});
const savePerson = (person, clientId) => {
if (person && DB) {
let tx = DB.transaction('colorStore', 'readwrite');
tx.onerror = (err) => {
//failed transaction
};
tx.oncomplete = (ev) => {
//finished saving... send the message
let msg = 'Thanks. The data was saved.';
sendMessage(
{
code: 0,
message: msg,
savedPerson: person,
},
clientId
);
};
let store = tx.objectStore('colorStore');
let req = store.put(person);
req.onsuccess = (ev) => {
//saved the person
//tx.commit() will be called automatically
//and will trigger tx.oncomplete next
};
} else {
let msg = 'No data was provided.';
sendMessage(
{
code: 0,
message: msg,
},
clientId
);
}
};
const sendMessage = async (msg, clientId) => {
let allClients = [];
if (clientId) {
let client = await clients.get(clientId);
allClients.push(client);
} else {
allClients = await clients.matchAll({ includeUncontrolled: true });
}
return Promise.all(
allClients.map((client) => {
// console.log('postMessage', msg, 'to', client.id);
return client.postMessage(msg);
})
);
};
const openDB = (callback) => {
let req = indexedDB.open('colorDB', version);
req.onerror = (err) => {
//could not open db
console.warn(err);
DB = null;
};
req.onupgradeneeded = (ev) => {
let db = ev.target.result;
if (!db.objectStoreNames.contains('colorStore')) {
db.createObjectStore('colorStore', {
keyPath: 'id',
});
}
};
req.onsuccess = (ev) => {
DB = ev.target.result;
console.log('db opened and upgraded as needed');
if (callback) {
callback();
}
};
};
@Md-shefat-masum
Copy link

thanks for making a great tutorial.

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