mirror of
https://github.com/JeremyLikness/vanillajs-deck
synced 2024-11-15 01:54:57 +00:00
Refactor and document service worker
This commit is contained in:
parent
9287b2b802
commit
41ed91a49d
272
pwa.js
272
pwa.js
@ -1,6 +1,52 @@
|
|||||||
const CACHE_VERSION = 1.0;
|
// @ts-check
|
||||||
|
|
||||||
const BASE_CACHE_FILES = [
|
/**
|
||||||
|
* @typedef {object} CacheVersions
|
||||||
|
* @property {string} assets Cache name for base assets
|
||||||
|
* @property {string} content Cache for pages
|
||||||
|
* @property {string} offline Cache for offline content
|
||||||
|
* @property {string} notFound Cache for 404 content
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {function} AddEventListener
|
||||||
|
* @param {string} eventName Name of event to add
|
||||||
|
* @param {EventListener} callback Callback when event is fired
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ServiceWorkerGlobalScope
|
||||||
|
* @property {function} skipWaiting skip the waiting
|
||||||
|
* @property {AddEventListener} addEventListener Add event listener
|
||||||
|
* @property {any} clients Service worker clients
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service worker for Progressive Web App
|
||||||
|
* */
|
||||||
|
class Pwa {
|
||||||
|
/**
|
||||||
|
* Create a new instance
|
||||||
|
* @param {ServiceWorkerGlobalScope} self
|
||||||
|
*/
|
||||||
|
constructor(self) {
|
||||||
|
/**
|
||||||
|
* Global scope
|
||||||
|
* @type {ServiceWorkerGlobalScope}
|
||||||
|
*/
|
||||||
|
this.scope = self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache version
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.CACHE_VERSION = 1.0;
|
||||||
|
/**
|
||||||
|
* Pre-emptive files to cache
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
this.BASE_CACHE_FILES = [
|
||||||
'/index.html',
|
'/index.html',
|
||||||
'/pwa/404.html',
|
'/pwa/404.html',
|
||||||
'/pwa/offline.html',
|
'/pwa/offline.html',
|
||||||
@ -10,57 +56,86 @@ const BASE_CACHE_FILES = [
|
|||||||
'/images/logo.png',
|
'/images/logo.png',
|
||||||
'/js/app.js',
|
'/js/app.js',
|
||||||
];
|
];
|
||||||
|
/**
|
||||||
const OFFLINE_CACHE_FILES = [
|
* Files needed for offline mode
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
this.OFFLINE_CACHE_FILES = [
|
||||||
'/pwa/offline.html'
|
'/pwa/offline.html'
|
||||||
];
|
];
|
||||||
|
/**
|
||||||
const NOT_FOUND_CACHE_FILES = [
|
* Files needed for 404 page
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
this.NOT_FOUND_CACHE_FILES = [
|
||||||
'/pwa/404.html'
|
'/pwa/404.html'
|
||||||
];
|
];
|
||||||
|
/**
|
||||||
const OFFLINE_PAGE = '/pwa/offline.html';
|
* Page to redirect to when offline
|
||||||
const NOT_FOUND_PAGE = '/pwa/404.html';
|
* @type {string}
|
||||||
|
*/
|
||||||
const CACHE_VERSIONS = {
|
this.OFFLINE_PAGE = '/pwa/offline.html';
|
||||||
assets: 'assets-v' + CACHE_VERSION,
|
/**
|
||||||
content: 'content-v' + CACHE_VERSION,
|
* Page to show when not found (404)
|
||||||
offline: 'offline-v' + CACHE_VERSION,
|
* @type {string}
|
||||||
notFound: '404-v' + CACHE_VERSION,
|
*/
|
||||||
|
this.NOT_FOUND_PAGE = '/pwa/404.html';
|
||||||
|
/**
|
||||||
|
* Versioned caches
|
||||||
|
* @type {CacheVersions}
|
||||||
|
*/
|
||||||
|
this.CACHE_VERSIONS = {
|
||||||
|
assets: `assets-v${this.CACHE_VERSION}`,
|
||||||
|
content: `content-v${this.CACHE_VERSION}`,
|
||||||
|
offline: `offline-v${this.CACHE_VERSION}`,
|
||||||
|
notFound: `404-v${this.CACHE_VERSION}`,
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
// Define MAX_TTL's in SECONDS for specific file extensions
|
* The time to live in cache
|
||||||
const MAX_TTL = {
|
* @type {object}
|
||||||
|
*/
|
||||||
|
this.MAX_TTL = {
|
||||||
|
/** @type {number} Default time to live in seconds */
|
||||||
'/': 3600,
|
'/': 3600,
|
||||||
|
/** @type {number} Time to live for pages in seconds */
|
||||||
html: 43200,
|
html: 43200,
|
||||||
|
/** @type {number} Time to live for JSON in seconds */
|
||||||
json: 43200,
|
json: 43200,
|
||||||
|
/** @type {number} Time to live for scripts in seconds */
|
||||||
js: 86400,
|
js: 86400,
|
||||||
|
/** @type {number} Time to live for stylesheets in seconds */
|
||||||
css: 86400,
|
css: 86400,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUPPORTED_METHODS = [
|
/**
|
||||||
|
* Supported methods for HTTP requests
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
this.SUPPORTED_METHODS = [
|
||||||
'GET',
|
'GET',
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getFileExtension
|
* Get the extension of a file from URL
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getFileExtension(url) {
|
getFileExtension(url) {
|
||||||
let extension = url.split('.').reverse()[0].split('?')[0];
|
let extension = url.split('.').reverse()[0].split('?')[0];
|
||||||
return (extension.endsWith('/')) ? '/' : extension;
|
return (extension.endsWith('/')) ? '/' : extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getTTL
|
* Get time to live for cache by extension
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
|
* @returns {number} Time to live in seconds
|
||||||
*/
|
*/
|
||||||
function getTTL(url) {
|
getTTL(url) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
let extension = getFileExtension(url);
|
const extension = this.getFileExtension(url);
|
||||||
if (typeof MAX_TTL[extension] === 'number') {
|
if (typeof this.MAX_TTL[extension] === 'number') {
|
||||||
return MAX_TTL[extension];
|
return this.MAX_TTL[extension];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -69,23 +144,19 @@ function getTTL(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async installServiceWorker() {
|
||||||
* installServiceWorker
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
async function installServiceWorker() {
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
caches.open(CACHE_VERSIONS.assets).then((cache) => {
|
caches.open(this.CACHE_VERSIONS.assets).then((cache) => {
|
||||||
return cache.addAll(BASE_CACHE_FILES);
|
return cache.addAll(this.BASE_CACHE_FILES);
|
||||||
}, err => console.error(`Error with ${CACHE_VERSIONS.assets}`, err)),
|
}, err => console.error(`Error with ${this.CACHE_VERSIONS.assets}`, err)),
|
||||||
caches.open(CACHE_VERSIONS.offline).then((cache_1) => {
|
caches.open(this.CACHE_VERSIONS.offline).then((cache_1) => {
|
||||||
return cache_1.addAll(OFFLINE_CACHE_FILES);
|
return cache_1.addAll(this.OFFLINE_CACHE_FILES);
|
||||||
}, err_1 => console.error(`Error with ${CACHE_VERSIONS.offline}`, err_1)),
|
}, err_1 => console.error(`Error with ${this.CACHE_VERSIONS.offline}`, err_1)),
|
||||||
caches.open(CACHE_VERSIONS.notFound).then((cache_2) => {
|
caches.open(this.CACHE_VERSIONS.notFound).then((cache_2) => {
|
||||||
return cache_2.addAll(NOT_FOUND_CACHE_FILES);
|
return cache_2.addAll(this.NOT_FOUND_CACHE_FILES);
|
||||||
}, err_2 => console.error(`Error with ${CACHE_VERSIONS.notFound}`, err_2))]);
|
}, err_2 => console.error(`Error with ${this.CACHE_VERSIONS.notFound}`, err_2))]);
|
||||||
return self.skipWaiting();
|
return this.scope.skipWaiting();
|
||||||
}
|
}
|
||||||
catch (err_3) {
|
catch (err_3) {
|
||||||
return console.error("Error with installation: ", err_3);
|
return console.error("Error with installation: ", err_3);
|
||||||
@ -93,29 +164,28 @@ async function installServiceWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cleanupLegacyCache
|
* Removes prior cache version
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
function cleanupLegacyCache() {
|
cleanupLegacyCache() {
|
||||||
|
|
||||||
const currentCaches = Object.keys(CACHE_VERSIONS).map((key) => {
|
const currentCaches = Object.keys(this.CACHE_VERSIONS).map((key) => {
|
||||||
return CACHE_VERSIONS[key];
|
return this.CACHE_VERSIONS[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise(
|
return new Promise(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
|
|
||||||
caches.keys().then((keys) => {
|
caches.keys().then((keys) => {
|
||||||
return legacyKeys = keys.filter((key) => {
|
return keys.filter((key) => {
|
||||||
return !~currentCaches.indexOf(key);
|
return !~currentCaches.indexOf(key);
|
||||||
});
|
});
|
||||||
}).then((legacy) => {
|
}).then((legacy) => {
|
||||||
if (legacy.length) {
|
if (legacy.length) {
|
||||||
Promise.all(legacy.map((legacyKey) => {
|
Promise.all(legacy.map((legacyKey) => {
|
||||||
return caches.delete(legacyKey)
|
return caches.delete(legacyKey);
|
||||||
})
|
})
|
||||||
).then(() => {
|
).then(() => {
|
||||||
resolve()
|
resolve();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error("Error in legacy cleanup: ", err);
|
console.error("Error in legacy cleanup: ", err);
|
||||||
reject(err);
|
reject(err);
|
||||||
@ -127,12 +197,15 @@ function cleanupLegacyCache() {
|
|||||||
console.error("Error in legacy cleanup: ", err);
|
console.error("Error in legacy cleanup: ", err);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function preCacheUrl(url) {
|
/**
|
||||||
caches.open(CACHE_VERSIONS.content).then((cache) => {
|
* Pre-fetches URL to store to cache
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
preCacheUrl(url) {
|
||||||
|
caches.open(this.CACHE_VERSIONS.content).then((cache) => {
|
||||||
cache.match(url).then((response) => {
|
cache.match(url).then((response) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return fetch(url);
|
return fetch(url);
|
||||||
@ -148,46 +221,49 @@ function preCacheUrl(url) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
/**
|
||||||
|
* Registers the various service worker functions
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
register() {
|
||||||
|
this.scope.addEventListener('install', event => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
Promise.all([
|
Promise.all([
|
||||||
installServiceWorker(),
|
this.installServiceWorker(),
|
||||||
self.skipWaiting(),
|
this.scope.skipWaiting(),
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
// The activate handler takes care of cleaning up old caches.
|
// The activate handler takes care of cleaning up old caches.
|
||||||
self.addEventListener('activate', event => {
|
this.scope.addEventListener('activate', event => {
|
||||||
event.waitUntil(Promise.all(
|
event.waitUntil(Promise.all(
|
||||||
[cleanupLegacyCache(),
|
[this.cleanupLegacyCache(),
|
||||||
self.clients.claim(),
|
this.scope.clients.claim(),
|
||||||
self.skipWaiting()]).catch((err) => {
|
this.scope.skipWaiting()]).catch((err) => {
|
||||||
console.error("Activation error: ", err);
|
console.error("Activation error: ", err);
|
||||||
event.skipWaiting();
|
event.skipWaiting();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', event => {
|
this.scope.addEventListener('fetch', event => {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.open(CACHE_VERSIONS.content).then((cache) => {
|
caches.open(this.CACHE_VERSIONS.content)
|
||||||
return cache.match(event.request).then((response) => {
|
.then(async (cache) => {
|
||||||
|
try {
|
||||||
|
const response = await cache.match(event.request);
|
||||||
if (response) {
|
if (response) {
|
||||||
let headers = response.headers.entries();
|
const headers = response.headers.entries();
|
||||||
let date = null;
|
let date = null;
|
||||||
|
|
||||||
for (let pair of headers) {
|
for (let pair of headers) {
|
||||||
if (pair[0] === 'date') {
|
if (pair[0] === 'date') {
|
||||||
date = new Date(pair[1]);
|
date = new Date(pair[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (date) {
|
if (date) {
|
||||||
let age = parseInt((new Date().getTime() - date.getTime()) / 1000);
|
let age = parseInt(((new Date().getTime() - date.getTime()) / 1000).toString());
|
||||||
let ttl = getTTL(event.request.url);
|
let ttl = this.getTTL(event.request.url);
|
||||||
|
|
||||||
if (ttl && age > ttl) {
|
if (ttl && age > ttl) {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
try {
|
try {
|
||||||
@ -203,63 +279,47 @@ self.addEventListener('fetch', event => {
|
|||||||
catch (e) {
|
catch (e) {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => response);
|
||||||
return response;
|
}
|
||||||
});
|
else {
|
||||||
} else {
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).then((response) => {
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
} else {
|
|
||||||
return fetch(event.request.clone()).then(
|
|
||||||
(response) => {
|
|
||||||
if (response.status < 400) {
|
|
||||||
if (~SUPPORTED_METHODS.indexOf(event.request.method)) {
|
|
||||||
cache.put(event.request, response.clone());
|
|
||||||
}
|
}
|
||||||
return response;
|
catch (error) {
|
||||||
} else {
|
|
||||||
return caches.open(CACHE_VERSIONS.notFound).then((cache) => {
|
|
||||||
return cache.match(NOT_FOUND_PAGE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).then((response) => {
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
return caches.open(CACHE_VERSIONS.offline).then(
|
|
||||||
(offlineCache) => {
|
|
||||||
return offlineCache.match(OFFLINE_PAGE)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Error in fetch handler:', error);
|
console.error('Error in fetch handler:', error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
this.scope.addEventListener('message', (event) => {
|
||||||
if (typeof event.data === 'object' &&
|
if (typeof event.data === 'object' &&
|
||||||
typeof event.data.action === 'string') {
|
typeof event.data.action === 'string') {
|
||||||
switch (event.data.action) {
|
switch (event.data.action) {
|
||||||
case 'cache':
|
case 'cache':
|
||||||
preCacheUrl(event.data.url);
|
this.preCacheUrl(event.data.url);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Unknown action: ' + event.data.action);
|
console.log(`Unknown action: ${event.data.action}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sadly this is a pathetic hack to workaround JsDoc limitations
|
||||||
|
* @type {any} */
|
||||||
|
const _self = self;
|
||||||
|
var pwa = new Pwa(/**@type {ServiceWorkerGlobalScope}*/(_self));
|
||||||
|
pwa.register();
|
Loading…
Reference in New Issue
Block a user