Skip to content
sw.js 4.86 KiB
Newer Older
importScripts('vendor/localforage.min.js');

var CACHE = 'calc';

/**
 * get path from URL.
 * drop schema, host, query string.
 */
const getUrlPath = function (url) {
    var path = '/' + url.split('/').slice(3).join('/');
    var paramIndex = path.indexOf('?');
    if (paramIndex !== -1) {
	return path.substring(0, paramIndex);
    }
    return path;
};
const cachePathList = ['/', '/calc.html'];
const cacheFileExtList = ['.js', '.css', '.png', '.jpg', '.gif'];
/**
 * return true if this URL should be cached by service worker.
 */
const shouldCache = function (url) {
    const path = getUrlPath(url);
    for (let cachePath of cachePathList) {
	// since sw.js is scoped at ./, this should work.
	if (path.endsWith(cachePath)) {
    	    return true;
	}
    }
    return cacheFileExtList.some(function (ext, index, ar) {
    	return path.endsWith(ext);
    });
};
/**
 * return true if this URL is a vendor asset.
 */
const isVendorAsset = function (url) {
    const path = getUrlPath(url);
    return path.includes('/vendor/');
};
/**
 * return true if this URL is static asset. see cacheFileExtList.
 * files ending in those ext are considered to be static asset.
 */
const isStaticAsset = function (url) {
    const path = getUrlPath(url);
    if (path.endsWith('/sw.js')) {
	return false;    // do not cache service worker js file.
    }
    return cacheFileExtList.some(function (ext, index, ar) {
	return path.endsWith(ext);
    });
};

/**
 * cache timestamp key used in indexeddb.
 * each cached url can have a cache timestamp with it.
 */
const cacheTimestampKey = function (url) {
    return "timestamp:" + url;
};

// ===============
//  event handler
// ===============

self.addEventListener('fetch', function(event) {
    /**
     * used as fetch(event.request)'s then function.
     * cache the resp in service worker's req/resp cache, then return the resp.
     *
     * if response is not 2xx, do not cache.
     */
    const cacheRespAndReturn = function (resp) {
	const respClone = resp.clone();
	if (! resp.ok) {
	    return resp;
	}
	caches.open(CACHE).then(function (cache) {
	    cache.put(event.request, respClone);
	    console.log("cached " + event.request.url);
	});
	return resp;
    };

    /**
     * use cache if it exists. Otherwise, fetch and cache it.
     */
    const useServiceWorkerCache = function () {
	return event.respondWith(
	    caches.match(event.request).then(function (resp) {
		if (resp) {
		    return resp;
		} else {
		    return fetch(event.request).then(cacheRespAndReturn);
		}
	    })
	);
    };

    /**
     * use cached Response if it's not expired or when network is down.
     * try renew cache if it's expired.
     */
    const useServiceWorkerCacheIfNotExpired = function () {
	// console.log("use service worker cache if not expired");
	const url = event.request.url;
	const tsKey = cacheTimestampKey(url);
	const expireSeconds = 24 * 3600;
	/**
	 * try update service worker req/resp cache and the ts cache.
	 */
	const tryUpdate = function () {
	    return fetch(event.request).then(function (resp) {
		const respClone = resp.clone();
		if (! resp.ok) {
		    console.log("HTTP ERROR: " + resp.status);
		    return resp;    // just return, don't cache.
		}
		caches.open(CACHE).then(function (cache) {
		    cache.put(event.request, respClone);
		    console.log("cached " + event.request.url);
		    const ts = new Date().toJSON();
		    localforage.setItem(tsKey, ts).then(function () {
			console.log("cache ts updated to " + ts + ", url=" + event.request.url);
		    }).catch(function (err) {
			console.log("cache ts update failed: " + err);
		    });
		});
		return resp;
	    }).catch(function (_err) {
		console.log("fetch failed. try use cached resp.");
		return caches.match(event.request);    // nothing to do if
						       // match failed.
	    });
	};
	return event.respondWith(
	    localforage.getItem(tsKey).then(function (tsString) {
		if (tsString !== null) {
		    const now = new Date();
		    const cacheDate = new Date(tsString);
		    const diffSeconds = (now - cacheDate) / 1000;
		    if (diffSeconds > expireSeconds) {
			console.log("cache expired");
			return tryUpdate();
		    }
		    console.log("cache is valid according to cache ts, url=",
				event.request.url);
		    return caches.match(event.request).then(function (resp) {
			if (resp) {
			    return resp;
			}
			return fetch(event.request);
		    });
		} else {
		    console.log("no ts cache");
		    return tryUpdate();
		}
	    }).catch(function (_err) {
		console.log("no ts cache");
		return tryUpdate();
	    })
	);
    };

    var url = event.request.url;

    if (! shouldCache(url)) {
	console.log("not caching " + url);
	return fetch(event.request).catch(function (err) {
	    console.log(err);
	});
    }
    // console.log("will use sw cache on " + url);
    if (isVendorAsset(url)) {
	return useServiceWorkerCache();
    } else {
	return useServiceWorkerCacheIfNotExpired();
    }
});