---
---
// Copyright (c) 2018 Florian Klampfer
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// ⚡️ DANGER ZONE ⚡️
// ================
// {% if jekyll.environment == 'production' or site.hydejack.offline.development %}
// The shell cache keeps "landmark" resources, like CSS and JS, web fonts, etc.
// which won't change between content updates.
// {% assign cv = site.hydejack.offline.cache_version | default:"1" %}
const SHELL_CACHE = "shell-8.1.0--v{{ cv }}--sw{{ '/' | relative_url }}";
// A separate assets cache that won't be invalidated when there's a newer version of Hydejack.
// NOTE: Whenever you make changes to any of the files in yor `assets` folder,
// increase the cache number, otherwise the changes will NEVER be visible to returning visitors.
const ASSETS_CACHE = "assets--v{{ cv }}--sw{{ '/' | relative_url }}";
// The cache for regular content, which will be invalidated every time you make a new build.
const CONTENT_CACHE = "content--{{ site.time | date_to_xmlschema }}--sw{{ '/' | relative_url }}";
// A URL search parameter you can add to external assets to cache them in the service worker.
const CACHE_SEARCH_PARAM = "sw-cache";
// The search parameter used to bypass the disk cache.
// https://jakearchibald.com/2016/caching-best-practices/#a-service-worker-can-extend-the-life-of-these-bugs
const RAND_SEARCH_PARAM = "rand";
// The regular expression used to find URLs in webfont style sheets.
const RE = /url\(['"]?(.*?)['"]?\)/gi;
const ICON_FONT = "{{ '/assets/icomoon/style.css' | relative_url }}";
// {% assign google_fonts = site.google_fonts | default:"Roboto+Slab:700|Noto+Sans:400,400i,700,700i" %}
// {% unless site.hydejack.no_google_fonts or site.no_google_fonts %}
// {% assign gf = true %}
const GOOGLE_FONTS = "https://fonts.googleapis.com/css?family={{ google_fonts | uri_escape }}";
// {% endunless %}
const SHELL_FILES = [
"{{ '/assets/bower_components/fontfaceobserver/fontfaceobserver.standalone.js' | relative_url }}",
"{{ '/assets/js/hydejack-8.1.0.js' | relative_url }}",
"{{ '/assets/css/hydejack-8.1.0.css' | relative_url }}",
"{{ '/assets/img/swipe.svg' | relative_url }}",
ICON_FONT,
/*{% if gf %}*/ GOOGLE_FONTS /*{% endif %}*/,
];
const ASSET_FILES = [
/*{% if site.accent_image %}{% unless site.accent_image.background %}*/ "{% include smart-url.txt url=site.accent_image %}" /*{% endunless %}{% endif %}*/,
/*{% if site.logo %}*/ "{% include smart-url.txt url=site.logo %}" /*{% endif %}*/,
/*{% for file in site.hydejack.offline.precache_assets %}*/ "{% include smart-url.txt url=file %}",
/*{% endfor %}*/
];
// Files we add on every service worker installation.
const CONTENT_FILES = [
"{{ '/' | relative_url }}",
"{{ '/?utm_source=homescreen' | relative_url }}",
"{{ '/assets/manifest.json' | relative_url }}",
/*{% for legal in site.legal %}*/ "{% include smart-url.txt url=legal.href %}",
/*{% endfor %}*/
];
const NOT_FOUND_PAGE = "{{ '/404.html' | relative_url }}";
self.addEventListener("install", e => e.waitUntil(onInstall(e)));
self.addEventListener("activate", e => e.waitUntil(onActivate(e)));
self.addEventListener("fetch", e => e.respondWith(onFetch(e)));
function dirname(path) {
return path.replace(/[^/]*$/, "");
}
function getMatches(text, re, i = 0) {
const res = [];
let match;
while ((match = re.exec(text))) {
res.push(match[i]);
}
return res;
}
function noCache(url) {
const url2 = new URL(url);
url2.searchParams.append(
RAND_SEARCH_PARAM,
Math.random()
.toString(36)
.substr(2)
);
return url2;
}
function noSWParam(url) {
const url2 = new URL(url);
url2.searchParams.delete(CACHE_SEARCH_PARAM);
return url2;
}
// TODO: transpile to ES5, or translate by hand.
async function getIconFontFiles() {
const iconFontURL = new URL(ICON_FONT, self.location);
const iconFontRes = await fetch(iconFontURL);
const text = await iconFontRes.text();
const dirPath = dirname(iconFontURL.pathname);
return getMatches(text, RE, 1)
.map(match => new URL(`${dirPath}${match}`, iconFontURL.origin))
.concat(ICON_FONT);
}
async function getGoogleFontsFiles() {
const googleFontRes = await fetch(GOOGLE_FONTS);
const text = await googleFontRes.text();
return getMatches(text, RE, 1).concat(GOOGLE_FONTS);
}
const toLocalURL = url => new URL(url, self.location);
function addAll(cache, urls) {
return Promise.all(
urls.map(url => fetch(noCache(toLocalURL(url))).then(res => cache.put(url, res)))
);
}
async function cache404(cache) {
const url = new URL(NOT_FOUND_PAGE, self.location);
const response = await fetch(noCache(url));
return cache.put(
url,
new Response(response.body, {
status: 598,
statusText: "Offline",
headers: response.headers,
})
);
}
async function cacheShell(cache) {
const [iconFontFiles, googleFontsFiles] = await Promise.all([
getIconFontFiles(),
/*{% if gf %}*/ getGoogleFontsFiles() /*{% endif %}*/,
]);
const urls = SHELL_FILES.concat(iconFontFiles, googleFontsFiles).filter(x => !!x);
return addAll(cache, urls);
}
async function cacheAssets(cache) {
const urls = ASSET_FILES.filter(x => !!x);
return addAll(cache, urls);
}
async function cacheContent(cache) {
const urls = CONTENT_FILES.filter(x => !!x);
return Promise.all([addAll(cache, urls), cache404(cache)]);
}
async function precache() {
const keys = await caches.keys();
if (keys.includes(SHELL_CACHE) && keys.includes(ASSETS_CACHE)) {
const contentCache = await caches.open(CONTENT_CACHE);
return cacheContent(contentCache);
} else {
const [shellCache, assetsCache, contentCache] = await Promise.all([
caches.open(SHELL_CACHE),
caches.open(ASSETS_CACHE),
caches.open(CONTENT_CACHE),
]);
return Promise.all([
cacheShell(shellCache),
cacheAssets(assetsCache),
cacheContent(contentCache),
]);
}
}
async function onInstall(e) {
await precache();
return self.skipWaiting();
}
function isSameSite({ origin, pathname }) {
return origin.startsWith("{{ site.url }}") && pathname.startsWith("{{ site.baseurl }}");
}
async function cacheResponse(cacheName, req, res) {
const cache = await caches.open(cacheName);
return cache.put(req, res);
}
async function fetchAndCache(e, request, cacheName) {
const response = await fetch(noCache(noSWParam(request.url)));
if (response.ok) e.waitUntil(cacheResponse(cacheName, request, response.clone()));
return response;
}
async function fromNetwork(e, request) {
const url = new URL(request.url);
// TODO: always cache GET requests from other domains!? Only images?
const hasSWParam = url.searchParams.has(CACHE_SEARCH_PARAM);
if (isSameSite(url) || hasSWParam) {
const isAsset = url.pathname.startsWith("{{ 'assets' | relative_url }}");
const cacheName = isAsset || hasSWParam ? ASSETS_CACHE : CONTENT_CACHE;
return fetchAndCache(e, request, cacheName);
}
// If the requested file isn't whitelisted we just send a regular request
return fetch(request);
}
async function onActivate(e) {
await self.clients.claim();
const keys = await caches.keys();
return Promise.all(
keys
// Only consider caches created by this baseurl, i.e. allow multiple Hydejack installations on same domain.
.filter(key => key.endsWith("sw{{ '/' | relative_url }}"))
// Delete old caches
.filter(key => key !== SHELL_CACHE && key !== ASSETS_CACHE && key !== CONTENT_CACHE)
.map(key => caches.delete(key))
);
}
async function onFetch(e) {
const { request } = e;
// Bypass
// ------
// Go to network for non-GET request and Google Analytics right away.
if (
request.method !== "GET" /*{% if site.google_analytics %}*/ ||
request.url.startsWith("https://www.google-analytics.com/collect") /*{% endif %}*/
) {
return fetch(request);
}
// Caches
// ------
// NOTE: `encodeURI` wtf?
const url = encodeURI(request.url);
// FIXME: don't wait for all promises to complete...
const [matching1, matching2, matching3] = await Promise.all([
caches.open(SHELL_CACHE).then(c => c.match(url)),
caches.open(ASSETS_CACHE).then(c => c.match(url)),
caches.open(CONTENT_CACHE).then(c => c.match(url)),
]);
if (matching1 || matching2 || matching3) return matching1 || matching2 || matching3;
// Network
// -------
// Got to network otherwise. Show 404 when there's a network error.
// TODO: Use separate offline site instead of 404!?
try {
return await fromNetwork(e, request);
} catch (err) {
const cache = await caches.open(CONTENT_CACHE);
return cache.match(NOT_FOUND_PAGE);
}
}
// {% comment %}
// TODO: We could add support for downloading the entire page.
const ALL_ASSETS = [
/*{% for file in site.static_files %}*/ "{{ file.path | relative_url }}",
/*{% endfor %}*/
];
const ALL_DOCUMENTS = [
/*{% for doc in site.documents %}*/ "{{ doc.url | relative_url }}",
/*{% endfor %}*/
];
const ALL_PAGES = [
/*{% for doc in site.pages %}*/ "{{ doc.url | relative_url }}",
/*{% endfor %}*/
];
// {% endcomment %}
// {% else %}
self.addEventListener("activate", e => e.waitUntil(onDeactivate(e)));
async function onDeactivate() {
await self.clients.claim();
const keys = await caches.keys();
return Promise.all(
keys
// Only consider caches created by this baseurl, i.e. allow multiple Hydejack installations on same domain.
.filter(key => key.endsWith("sw{{ '/' | relative_url }}"))
// Delete *all* caches
.map(key => caches.delete(key))
);
}
// {% endif %}