Repository: FrontendMasters/service-workers-offline Branch: master Commit: f08e2dd68494 Files: 18 Total size: 31.5 KB Directory structure: gitextract_wan5_mb4/ ├── .gitignore ├── README.md ├── package.json ├── server.js └── web/ ├── 404.html ├── about.html ├── add-post.html ├── contact.html ├── css/ │ └── style.css ├── index.html ├── js/ │ ├── add-post.js │ ├── blog.js │ ├── home.js │ ├── login.js │ └── sw.js ├── login.html ├── offline.html └── posts/ └── post.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules web/posts/2* ================================================ FILE: README.md ================================================ # [Service Workers & Offline Course](https://frontendmasters.com/courses/service-workers/) by Kyle Simpson Code for the Service Workers / PWA section of the Service Workers and Offline course by Kyle Simpson ## Starter Exercise Files To get started, download the [Starter Files (ZIP)](https://static.frontendmasters.com/resources/2019-05-10-service-worker-pwa/service-workers-starter.zip) ### Web Workers Solution The solution for the Web Workers section of the course: https://github.com/FrontendMasters/web-workers ### Service Workers & PWA Solution This repository has the final code for the course. You may refer to the [code commits on May 10th](https://github.com/FrontendMasters/service-workers-offline/commits/master), to walk through the course code as Kyle is completing it throughout the course. Don't forget to `npm install` the necessary modules. ================================================ FILE: package.json ================================================ { "name": "my-ramblings", "main": "server.js", "dependencies": { "cookie": "~0.3.1", "get-stream": "~5.1.0", "node-static-alias": "~1.1.2", "random-number-csprng": "~1.0.2" } } ================================================ FILE: server.js ================================================ "use strict"; var util = require("util"); var fs = require("fs"); var path = require("path"); var http = require("http"); var nodeStaticAlias = require("node-static-alias"); var getStream = require("get-stream"); var cookie = require("cookie"); var rand = require("random-number-csprng"); var fsReadDir = util.promisify(fs.readdir); var fsReadFile = util.promisify(fs.readFile); var fsWriteFile = util.promisify(fs.writeFile); const PORT = 8049; const WEB_DIR = path.join(__dirname,"web"); var httpServer = http.createServer(handleRequest); var staticServer = new nodeStaticAlias.Server(WEB_DIR,{ serverInfo: "My Ramblings", cache: 1, alias: [ { // basic static page friendly URL rewrites match: /^\/(?:index)?(?:[#?]|$)/, serve: "index.html", force: true, }, { // basic static page friendly URL rewrites match: /^\/(?:about|contact|login|404|offline)(?:[#?]|$)/, serve: "<% basename %>.html", force: true, }, { // URL rewrites for individual posts match: /^\/post\/[\w\d-]+(?:[#?]|$)/, serve: "posts/<% basename %>.html", force: true, }, { // match (with force) static files match: /^\/(?:(?:(?:js|css|images)\/.+))$/, serve: ".<% reqPath %>", force: true, }, ], }); httpServer.listen(PORT); console.log(`Server started on http://localhost:${PORT}...`); // ******************************* var sessions = []; async function handleRequest(req,res) { // parse cookie values? if (req.headers.cookie) { req.headers.cookie = cookie.parse(req.headers.cookie); } // handle API calls if ( ["GET","POST"].includes(req.method) && /^\/api\/.+$/.test(req.url) ) { if (req.url == "/api/get-posts") { await getPosts(req,res); return; } else if (req.url == "/api/login") { let loginData = JSON.parse(await getStream(req)); await doLogin(loginData,req,res); return; } else if ( req.url == "/api/add-post" && validateSessionID(req,res) ) { let newPostData = JSON.parse(await getStream(req)); await addPost(newPostData,req,res); return; } // didn't recognize the API request res.writeHead(404); res.end(); } // handle all other file requests else if (["GET","HEAD"].includes(req.method)) { // special handling for empty favicon if (req.url == "/favicon.ico") { res.writeHead(204,{ "Content-Type": "image/x-icon", "Cache-Control": "public, max-age: 604800" }); res.end(); return; } // special handling for service-worker (virtual path) if (/^\/sw\.js(?:[?#].*)?$/.test(req.url)) { serveFile("/js/sw.js",200,{ "cache-control": "max-age=0", },req,res) .catch(console.error); return; } // handle admin pages if (/^\/(?:add-post)(?:[#?]|$)/.test(req.url)) { // page not allowed without active session if (validateSessionID(req,res)) { await serveFile("/add-post.html",200,{},req,res); } // show the login page instead else { await serveFile("/login.html",200,{},req,res); } return; } // login page when already logged in? if ( /^\/(?:login)(?:[#?]|$)/.test(req.url) && validateSessionID(req,res) ) { res.writeHead(307,{ Location: "/add-post", }); res.end(); return; } // handle logout if (/^\/(?:logout)(?:[#?]|$)/.test(req.url)) { clearSession(req,res); res.writeHead(307,{ Location: "/", }); res.end(); return; } // handle other static files staticServer.serve(req,res,function onStaticComplete(err){ if (err) { if (req.headers["accept"].includes("text/html")) { serveFile("/404.html",200,{ "X-Not-Found": "1" },req,res) .catch(console.error); } else { res.writeHead(404); res.end(); } } }); } // Oops, invalid/unrecognized request else { res.writeHead(404); res.end(); } } function serveFile(url,statusCode,headers,req,res) { var listener = staticServer.serveFile(url,statusCode,headers,req,res); return new Promise(function c(resolve,reject){ listener.on("success",resolve); listener.on("error",reject); }); } async function getPostIDs() { var files = await fsReadDir(path.join(WEB_DIR,"posts")); return ( files .filter(function onlyPosts(filename){ return /^\d+\.html$/.test(filename); }) .map(function postID(filename){ let [,postID] = filename.match(/^(\d+)\.html$/); return Number(postID); }) .sort(function desc(x,y){ return y - x; }) ); } async function getPosts(req,res) { var postIDs = await getPostIDs(); sendJSONResponse(postIDs,res); } async function addPost(newPostData,req,res) { if ( newPostData.title.length > 0 && newPostData.post.length > 0 ) { let postTemplate = await fsReadFile(path.join(WEB_DIR,"posts","post.html"),"utf-8"); let newPost = postTemplate .replace(/\{\{TITLE\}\}/g,newPostData.title) .replace(/\{\{POST\}\}/,newPostData.post); let postIDs = await getPostIDs(); let newPostCount = 1; let [,year,month,day] = (new Date()).toISOString().match(/^(\d{4})-(\d{2})-(\d{2})/); if (postIDs.length > 0) { let [,latestYear,latestMonth,latestDay,latestCount] = String(postIDs[0]).match(/^(\d{4})(\d{2})(\d{2})(\d+)/); if ( latestYear == year && latestMonth == month && latestDay == day ) { newPostCount = Number(latestCount) + 1; } } let newPostID = `${year}${month}${day}${newPostCount}`; try { await fsWriteFile(path.join(WEB_DIR,"posts",`${newPostID}.html`),newPost,"utf8"); sendJSONResponse({ OK: true, postID: newPostID },res); return; } catch (err) {} } sendJSONResponse({ failed: true },res); } function validateSessionID(req,res) { if (req.headers.cookie && req.headers.cookie["sessionId"]) { let isLoggedIn = Number(req.headers.cookie["isLoggedIn"]); let sessionID = req.headers.cookie["sessionId"]; let session; if ( isLoggedIn == 1 && sessions.includes(sessionID) ) { req.sessionID = sessionID; // update cookie headers res.setHeader( "Set-Cookie", getCookieHeaders(sessionID,new Date(Date.now() + /*1 hour in ms*/3.6E5).toUTCString()) ); return true; } else { clearSession(req,res); } } return false; } async function randomString() { var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"; var str = ""; for (let i = 0; i < 20; i++) { str += chars[ await rand(0,63) ]; } return str; } async function createSession() { var sessionID; do { sessionID = await randomString(); } while (sessions.includes(sessionID)); sessions.push(sessionID); return sessionID; } function clearSession(req,res) { var sessionID = req.sessionID || (req.headers.cookie && req.headers.cookie.sessionId); if (sessionID) { sessions = sessions.filter(function removeSession(sID){ return sID !== sessionID; }); } res.setHeader("Set-Cookie",getCookieHeaders(null,new Date(0).toUTCString())); } function getCookieHeaders(sessionID,expires = null) { var cookieHeaders = [ `sessionId=${sessionID || ""}; HttpOnly; Path=/`, `isLoggedIn=${sessionID ? "1" : ""}; Path=/`, ]; if (expires != null) { cookieHeaders = cookieHeaders.map(function addExpires(headerVal){ return `${headerVal}; Expires=${expires}`; }); } return cookieHeaders; } async function doLogin(loginData,req,res) { // WARNING: This is absolutely NOT how you should handle logins, // having credentials hard-coded. Hash all credentials and store // them in a secure database. if (loginData.username == "admin" && loginData.password == "changeme") { let sessionID = await createSession(); sendJSONResponse({ OK: true },res,{ "Set-Cookie": getCookieHeaders( sessionID, new Date(Date.now() + /*1 hour in ms*/3.6E5).toUTCString() ) }); } else { sendJSONResponse({ failed: true },res); } } function sendJSONResponse(msg,res,otherHeaders = {}) { res.writeHead(200,{ "Content-Type": "application/json", "Cache-Control": "private, no-cache, no-store, must-revalidate, max-age=0", ...otherHeaders }); res.end(JSON.stringify(msg)); } ================================================ FILE: web/404.html ================================================ My Ramblings :: Not Found

My Ramblings

Not Found

Sorry, that couldn't be found. Please try again.

================================================ FILE: web/about.html ================================================ My Ramblings :: About

My Ramblings

About

These are just some of my rambling thoughts. I hope they are interesting to some of you.

Please feel free to reach out if you have any thoughts to share with me!

================================================ FILE: web/add-post.html ================================================ My Ramblings :: Add Post

My Ramblings

Add Post

Title:

================================================ FILE: web/contact.html ================================================ My Ramblings :: Contact

My Ramblings

Contact

If you'd like to reach out to me:

================================================ FILE: web/css/style.css ================================================ html { box-sizing: border-box; font-family: sans-serif; font-size: 1.3em; } *, *::before, *::after { box-sizing: inherit; } html input, html button, html textarea { font-size: 1em; } html, body { background-color: #e5efff; } #connectivity-status { position: absolute; top: 5px; right: 5px; width: 55px; height: 49px; background: url(/images/offline.png) 0px 0px/55px 49px no-repeat; } #connectivity-status.hidden { display: none; } header { position: relative; max-width: 800px; margin: 0px auto; color: #222; } header h1 { position: relative; padding-left: 90px; } header h1::before { content: ""; display: block; position: absolute; left: 0px; top: 0px; width: 63px; height: 75px; background: url(/images/logo.gif) 0px 0px/63px 75px no-repeat; } nav { background-color: #5078ba; color: #fff; } nav ul { padding: 0px; padding-left: 20px; margin: 0px; list-style: none; } nav ul li { display: inline-block; padding: 10px; } nav ul li a { color: #fff; text-decoration: none; } main { margin: 0px auto; max-width: 800px; background-color: #fff; color: #000; padding: 30px; } main a { color: #000; } main > h1:first-child { margin-top: 0px; } #my-posts { list-style: none; } #my-posts > li { margin-bottom: 12px; } #my-posts > li:last-child { margin-bottom: 0px; } ================================================ FILE: web/index.html ================================================ My Ramblings

My Ramblings

My Posts:

================================================ FILE: web/js/add-post.js ================================================ (function AddPost(){ "use strict"; var titleInput; var postInput; var addPostBtn; document.addEventListener("DOMContentLoaded",ready,false); // ********************************** async function ready() { titleInput = document.getElementById("new-title"); postInput = document.getElementById("new-post"); addPostBtn = document.getElementById("btn-add-post"); addPostBtn.addEventListener("click",addPost,false); titleInput.addEventListener("change",backupPost,false); postInput.addEventListener("change",backupPost,false); // restore a backup? var addPostBackup = await idbKeyval.get("add-post-backup"); if (addPostBackup) { titleInput.value = addPostBackup.title || ""; postInput.value = addPostBackup.post || ""; } } // save backup of post (in case posting fails or offline) async function backupPost() { await idbKeyval.set("add-post-backup",{ title: titleInput.value, post: postInput.value }); } async function addPost() { if ( titleInput.value.length > 0 && postInput.value.length > 0 ) { // don't try posting while offline if (!isBlogOnline()) { alert("You seem to be offline currently. Please try posting once you come back online."); return; } try { let res = await fetch("/api/add-post",{ method: "POST", credentials: "same-origin", body: JSON.stringify({ title: titleInput.value, post: postInput.value }) }); if (res && res.ok) { let result = await res.json(); if (result.OK) { titleInput.value = ""; postInput.value = ""; document.location.href = `/post/${result.postID}`; return; } } } catch (err) { console.error(err); } alert("Posting failed. Try again."); } else { alert("Please enter a title and some blog post content."); } } })(); ================================================ FILE: web/js/blog.js ================================================ (function Blog(global){ "use strict"; var offlineIcon; var isOnline = ("onLine" in navigator) && navigator.onLine; var isLoggedIn = /isLoggedIn=1/.test(document.cookie.toString() || ""); var usingSW = ("serviceWorker" in navigator); var swRegistration; var svcworker; if (usingSW) { initServiceWorker().catch(console.error); } global.isBlogOnline = isBlogOnline; document.addEventListener("DOMContentLoaded",ready,false); // ********************************** function ready() { offlineIcon = document.getElementById("connectivity-status"); if (!isOnline) { offlineIcon.classList.remove("hidden"); } window.addEventListener("online",function online(){ offlineIcon.classList.add("hidden"); isOnline = true; sendStatusUpdate(); },false); window.addEventListener("offline",function offline(){ offlineIcon.classList.remove("hidden"); isOnline = false; sendStatusUpdate(); },false); } function isBlogOnline() { return isOnline; } async function initServiceWorker() { swRegistration = await navigator.serviceWorker.register("/sw.js",{ updateViaCache: "none", }); svcworker = swRegistration.installing || swRegistration.waiting || swRegistration.active; sendStatusUpdate(svcworker); // listen for new service worker to take over navigator.serviceWorker.addEventListener("controllerchange",async function onController(){ svcworker = navigator.serviceWorker.controller; sendStatusUpdate(svcworker); }); navigator.serviceWorker.addEventListener("message",onSWMessage,false); } function onSWMessage(evt) { var { data } = evt; if (data.statusUpdateRequest) { console.log("Status update requested from service worker, responding..."); sendStatusUpdate(evt.ports && evt.ports[0]); } else if (data == "force-logout") { document.cookie = "isLoggedIn="; isLoggedIn = false; sendStatusUpdate(); } } function sendStatusUpdate(target) { sendSWMessage({ statusUpdate: { isOnline, isLoggedIn } },target); } function sendSWMessage(msg,target) { if (target) { target.postMessage(msg); } else if (svcworker) { svcworker.postMessage(msg); } else if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage(msg); } } })(window); ================================================ FILE: web/js/home.js ================================================ (function Home(){ "use strict"; var postsList; document.addEventListener("DOMContentLoaded",ready,false); // ********************************** function ready() { postsList = document.getElementById("my-posts"); main().catch(console.error); } async function main() { var postIDs; try { var res = await fetch("/api/get-posts"); if (res && res.ok) { postIDs = await res.json(); } } catch (err) {} renderPostIDs(postIDs || []); } function renderPostIDs(postIDs) { if (postIDs.length > 0) { postsList.innerHTML = ""; for (let postID of postIDs) { let [,year,month,day,postNum] = String(postID).match(/^(\d{4})(\d{2})(\d{2})(\d+)$/); let postEntry = document.createElement("li"); postEntry.innerHTML = `Post-${+month}/${+day}/${year}-${postNum}`; postsList.appendChild(postEntry); } } else { postsList.innerHTML = "
  • -- nothing yet, check back soon! --
  • "; } } })(); ================================================ FILE: web/js/login.js ================================================ (function Login(){ "use strict"; var usernameInput; var passwordInput; var loginBtn; document.addEventListener("DOMContentLoaded",ready,false); // ********************************** function ready() { usernameInput = document.getElementById("login-username"); passwordInput = document.getElementById("login-password"); loginBtn = document.getElementById("btn-login"); loginBtn.addEventListener("click",tryLogin,false); } async function tryLogin() { if ( usernameInput.value.length > 3 && passwordInput.value.length > 7 ) { try { let res = await fetch("/api/login",{ method: "POST", credentials: "same-origin", body: JSON.stringify({ username: usernameInput.value, password: passwordInput.value }) }); if (res && res.ok) { let result = await res.json(); usernameInput.value = ""; passwordInput.value = ""; if (result.OK) { if (document.location.href == "/add-post") { document.location.reload(); } else { document.location.href = "/add-post"; } return; } } } catch (err) { console.error(err); } alert("Login failed. Try again."); } else { alert("Please enter a sufficient username and password."); } } })(); ================================================ FILE: web/js/sw.js ================================================ "use strict"; importScripts("/js/external/idb-keyval-iife.min.js"); var version = 8; var isOnline = true; var isLoggedIn = false; var cacheName = `ramblings-${version}`; var allPostsCaching = false; var urlsToCache = { loggedOut: [ "/", "/about", "/contact", "/404", "/login", "/offline", "/css/style.css", "/js/blog.js", "/js/home.js", "/js/login.js", "/js/add-post.js", "/js/external/idb-keyval-iife.min.js", "/images/logo.gif", "/images/offline.png" ] }; self.addEventListener("install",onInstall); self.addEventListener("activate",onActivate); self.addEventListener("message",onMessage); self.addEventListener("fetch",onFetch); main().catch(console.error); // **************************** async function main() { await sendMessage({ statusUpdateRequest: true }); await cacheLoggedOutFiles(); return cacheAllPosts(); } function onInstall(evt) { console.log(`Service Worker (v${version}) installed`); self.skipWaiting(); } function onActivate(evt) { evt.waitUntil(handleActivation()); } async function handleActivation() { await clearCaches(); await cacheLoggedOutFiles(/*forceReload=*/true); await clients.claim(); console.log(`Service Worker (v${version}) activated`); // spin off background caching of all past posts (over time) cacheAllPosts(/*forceReload=*/true).catch(console.error); } async function clearCaches() { var cacheNames = await caches.keys(); var oldCacheNames = cacheNames.filter(function matchOldCache(cacheName){ var [,cacheNameVersion] = cacheName.match(/^ramblings-(\d+)$/) || []; cacheNameVersion = cacheNameVersion != null ? Number(cacheNameVersion) : cacheNameVersion; return ( cacheNameVersion > 0 && version !== cacheNameVersion ); }); await Promise.all( oldCacheNames.map(function deleteCache(cacheName){ return caches.delete(cacheName); }) ); } async function cacheLoggedOutFiles(forceReload = false) { var cache = await caches.open(cacheName); return Promise.all( urlsToCache.loggedOut.map(async function requestFile(url){ try { let res; if (!forceReload) { res = await cache.match(url); if (res) { return; } } let fetchOptions = { method: "GET", cache: "no-store", credentials: "omit" }; res = await fetch(url,fetchOptions); if (res.ok) { return cache.put(url,res); } } catch (err) {} }) ); } async function cacheAllPosts(forceReload = false) { // already caching the posts? if (allPostsCaching) { return; } allPostsCaching = true; await delay(5000); var cache = await caches.open(cacheName); var postIDs; try { if (isOnline) { let fetchOptions = { method: "GET", cache: "no-store", credentials: "omit" }; let res = await fetch("/api/get-posts",fetchOptions); if (res && res.ok) { await cache.put("/api/get-posts",res.clone()); postIDs = await res.json(); } } else { let res = await cache.match("/api/get-posts"); if (res) { let resCopy = res.clone(); postIDs = await res.json(); } // caching not started, try to start again (later) else { allPostsCaching = false; return cacheAllPosts(forceReload); } } } catch (err) { console.error(err); } if (postIDs && postIDs.length > 0) { return cachePost(postIDs.shift()); } else { allPostsCaching = false; } // ************************* async function cachePost(postID) { var postURL = `/post/${postID}`; var needCaching = true; if (!forceReload) { let res = await cache.match(postURL); if (res) { needCaching = false; } } if (needCaching) { await delay(10000); if (isOnline) { try { let fetchOptions = { method: "GET", cache: "no-store", credentials: "omit" }; let res = await fetch(postURL,fetchOptions); if (res && res.ok) { await cache.put(postURL,res.clone()); needCaching = false; } } catch (err) {} } // failed, try caching this post again? if (needCaching) { return cachePost(postID); } } // any more posts to cache? if (postIDs.length > 0) { return cachePost(postIDs.shift()); } else { allPostsCaching = false; } } } async function sendMessage(msg) { var allClients = await clients.matchAll({ includeUncontrolled: true, }); return Promise.all( allClients.map(function sendTo(client){ var chan = new MessageChannel(); chan.port1.onmessage = onMessage; return client.postMessage(msg,[chan.port2]); }) ); } function onMessage({ data }) { if ("statusUpdate" in data) { ({ isOnline, isLoggedIn } = data.statusUpdate); console.log(`Service Worker (v${version}) status update... isOnline:${isOnline}, isLoggedIn:${isLoggedIn}`); } } function onFetch(evt) { evt.respondWith(router(evt.request)); } async function router(req) { var url = new URL(req.url); var reqURL = url.pathname; var cache = await caches.open(cacheName); // request for site's own URL? if (url.origin == location.origin) { // are we making an API request? if (/^\/api\/.+$/.test(reqURL)) { let fetchOptions = { credentials: "same-origin", cache: "no-store" }; let res = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/false,/*checkCacheFirst=*/false,/*checkCacheLast=*/true,/*useRequestDirectly=*/true); if (res) { if (req.method == "GET") { await cache.put(reqURL,res.clone()); } // clear offline-backup of successful post? else if (reqURL == "/api/add-post") { await idbKeyval.del("add-post-backup"); } return res; } return notFoundResponse(); } // are we requesting a page? else if (req.headers.get("Accept").includes("text/html")) { // login-aware requests? if (/^\/(?:login|logout|add-post)$/.test(reqURL)) { let res; if (reqURL == "/login") { if (isOnline) { let fetchOptions = { method: req.method, headers: req.headers, credentials: "same-origin", cache: "no-store", redirect: "manual" }; res = await safeRequest(reqURL,req,fetchOptions); if (res) { if (res.type == "opaqueredirect") { return Response.redirect("/add-post",307); } return res; } if (isLoggedIn) { return Response.redirect("/add-post",307); } res = await cache.match("/login"); if (res) { return res; } return Response.redirect("/",307); } else if (isLoggedIn) { return Response.redirect("/add-post",307); } else { res = await cache.match("/login"); if (res) { return res; } return cache.match("/offline"); } } else if (reqURL == "/logout") { if (isOnline) { let fetchOptions = { method: req.method, headers: req.headers, credentials: "same-origin", cache: "no-store", redirect: "manual" }; res = await safeRequest(reqURL,req,fetchOptions); if (res) { if (res.type == "opaqueredirect") { return Response.redirect("/",307); } return res; } if (isLoggedIn) { isLoggedIn = false; await sendMessage("force-logout"); await delay(100); } return Response.redirect("/",307); } else if (isLoggedIn) { isLoggedIn = false; await sendMessage("force-logout"); await delay(100); return Response.redirect("/",307); } else { return Response.redirect("/",307); } } else if (reqURL == "/add-post") { if (isOnline) { let fetchOptions = { method: req.method, headers: req.headers, credentials: "same-origin", cache: "no-store" }; res = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/true); if (res) { return res; } res = await cache.match( isLoggedIn ? "/add-post" : "/login" ); if (res) { return res; } return Response.redirect("/",307); } else if (isLoggedIn) { res = await cache.match("/add-post"); if (res) { return res; } return cache.match("/offline"); } else { res = await cache.match("/login"); if (res) { return res; } return cache.match("/offline"); } } } // otherwise, just use "network-and-cache" else { let fetchOptions = { method: req.method, headers: req.headers, cache: "no-store" }; let res = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/false,/*checkCacheFirst=*/false,/*checkCacheLast=*/true); if (res) { if (!res.headers.get("X-Not-Found")) { await cache.put(reqURL,res.clone()); } else { await cache.delete(reqURL); } return res; } // otherwise, return an offline-friendly page return cache.match("/offline"); } } // all other files use "cache-first" else { let fetchOptions = { method: req.method, headers: req.headers, cache: "no-store" }; let res = await safeRequest(reqURL,req,fetchOptions,/*cacheResponse=*/true,/*checkCacheFirst=*/true); if (res) { return res; } // otherwise, force a network-level 404 response return notFoundResponse(); } } } async function safeRequest(reqURL,req,options,cacheResponse = false,checkCacheFirst = false,checkCacheLast = false,useRequestDirectly = false) { var cache = await caches.open(cacheName); var res; if (checkCacheFirst) { res = await cache.match(reqURL); if (res) { return res; } } if (isOnline) { try { if (useRequestDirectly) { res = await fetch(req,options); } else { res = await fetch(req.url,options); } if (res && (res.ok || res.type == "opaqueredirect")) { if (cacheResponse) { await cache.put(reqURL,res.clone()); } return res; } } catch (err) {} } if (checkCacheLast) { res = await cache.match(reqURL); if (res) { return res; } } } function notFoundResponse() { return new Response("",{ status: 404, statusText: "Not Found" }); } function delay(ms) { return new Promise(function c(res){ setTimeout(res,ms); }); } ================================================ FILE: web/login.html ================================================ My Ramblings :: Add Post

    My Ramblings

    Login

    Username:

    Password:

    ================================================ FILE: web/offline.html ================================================ My Ramblings :: Offline!

    My Ramblings

    Offline

    It looks like you're offline and the page you requested couldn't be loaded. Please try again once you're back online.

    ================================================ FILE: web/posts/post.html ================================================ My Ramblings :: {{TITLE}}

    My Ramblings

    {{TITLE}}

    {{POST}}