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
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings :: Not Found</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>Not Found</h1>
<p>
Sorry, that couldn't be found. Please try again.
</p>
</main>
<script src="/js/blog.js"></script>
</body>
</html>
================================================
FILE: web/about.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings :: About</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>About</h1>
<p>
These are just some of my rambling thoughts. I hope they are interesting to some of you.
</p>
<p>
Please feel free to <a href="/contact">reach out</a> if you have any thoughts to share with me!
</p>
</main>
<script src="/js/blog.js"></script>
</body>
</html>
================================================
FILE: web/add-post.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings :: Add Post</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/logout">Logout</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>Add Post</h1>
<p>
Title: <input id="new-title" size="40">
</p>
<p>
<textarea id="new-post" cols="70" rows="15"></textarea>
</p>
<p>
<button type="button" id="btn-add-post">Add Post</button>
</p>
</main>
<script src="/js/external/idb-keyval-iife.min.js"></script>
<script src="/js/blog.js"></script>
<script src="/js/add-post.js"></script>
</body>
</html>
================================================
FILE: web/contact.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings :: Contact</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>Contact</h1>
<p>
If you'd like to reach out to me:
</p>
<ul>
<li><a href="https://twitter.com/getify">@getify</a></li>
<li><a href="mailto:getify@gmail.com">getify @ gmail</a></li>
</ul>
</main>
<script src="/js/blog.js"></script>
</body>
</html>
================================================
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
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>My Posts:</h1>
<ul id="my-posts">
<li>...</li>
</ul>
</main>
<script src="/js/blog.js"></script>
<script src="/js/home.js"></script>
</body>
</html>
================================================
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 = `<a href="/post/${postID}">Post-${+month}/${+day}/${year}-${postNum}</a>`;
postsList.appendChild(postEntry);
}
}
else {
postsList.innerHTML = "<li>-- nothing yet, check back soon! --</li>";
}
}
})();
================================================
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
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings :: Add Post</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>Login</h1>
<p>
Username: <input id="login-username">
</p>
<p>
Password: <input type="password" id="login-password">
</p>
<p>
<button type="button" id="btn-login">Login</button>
</p>
</main>
<script src="/js/blog.js"></script>
<script src="/js/login.js"></script>
</body>
</html>
================================================
FILE: web/offline.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings :: Offline!</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>Offline</h1>
<p>
It looks like you're offline and the page you requested couldn't be loaded. Please try again once you're back online.
</p>
</main>
<script src="/js/blog.js"></script>
</body>
</html>
================================================
FILE: web/posts/post.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Ramblings :: {{TITLE}}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>My Ramblings</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<div id="connectivity-status" class="hidden"></div>
</header>
<main>
<h1>{{TITLE}}</h1>
{{POST}}
</main>
<script src="/js/blog.js"></script>
</body>
</html>
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
SYMBOL INDEX (42 symbols across 6 files)
FILE: server.js
constant PORT (line 16) | const PORT = 8049;
constant WEB_DIR (line 17) | const WEB_DIR = path.join(__dirname,"web");
function handleRequest (line 61) | async function handleRequest(req,res) {
function serveFile (line 165) | function serveFile(url,statusCode,headers,req,res) {
function getPostIDs (line 173) | async function getPostIDs() {
function getPosts (line 190) | async function getPosts(req,res) {
function addPost (line 195) | async function addPost(newPostData,req,res) {
function validateSessionID (line 230) | function validateSessionID(req,res) {
function randomString (line 257) | async function randomString() {
function createSession (line 266) | async function createSession() {
function clearSession (line 275) | function clearSession(req,res) {
function getCookieHeaders (line 289) | function getCookieHeaders(sessionID,expires = null) {
function doLogin (line 304) | async function doLogin(loginData,req,res) {
function sendJSONResponse (line 322) | function sendJSONResponse(msg,res,otherHeaders = {}) {
FILE: web/js/add-post.js
function ready (line 13) | async function ready() {
function backupPost (line 31) | async function backupPost() {
function addPost (line 38) | async function addPost() {
FILE: web/js/blog.js
function ready (line 22) | function ready() {
function isBlogOnline (line 41) | function isBlogOnline() {
function initServiceWorker (line 45) | async function initServiceWorker() {
function onSWMessage (line 62) | function onSWMessage(evt) {
function sendStatusUpdate (line 75) | function sendStatusUpdate(target) {
function sendSWMessage (line 79) | function sendSWMessage(msg,target) {
FILE: web/js/home.js
function ready (line 11) | function ready() {
function main (line 16) | async function main() {
function renderPostIDs (line 30) | function renderPostIDs(postIDs) {
FILE: web/js/login.js
function ready (line 13) | function ready() {
function tryLogin (line 21) | async function tryLogin() {
FILE: web/js/sw.js
function main (line 40) | async function main() {
function onInstall (line 46) | function onInstall(evt) {
function onActivate (line 51) | function onActivate(evt) {
function handleActivation (line 55) | async function handleActivation() {
function clearCaches (line 65) | async function clearCaches() {
function cacheLoggedOutFiles (line 82) | async function cacheLoggedOutFiles(forceReload = false) {
function cacheAllPosts (line 112) | async function cacheAllPosts(forceReload = false) {
function sendMessage (line 208) | async function sendMessage(msg) {
function onMessage (line 219) | function onMessage({ data }) {
function onFetch (line 226) | function onFetch(evt) {
function router (line 230) | async function router(req) {
function safeRequest (line 408) | async function safeRequest(reqURL,req,options,cacheResponse = false,chec...
function notFoundResponse (line 446) | function notFoundResponse() {
function delay (line 453) | function delay(ms) {
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (38K chars).
[
{
"path": ".gitignore",
"chars": 26,
"preview": "node_modules\nweb/posts/2*\n"
},
{
"path": "README.md",
"chars": 868,
"preview": "# [Service Workers & Offline Course](https://frontendmasters.com/courses/service-workers/) by Kyle Simpson\n\nCode for the"
},
{
"path": "package.json",
"chars": 189,
"preview": "{\n\t\"name\": \"my-ramblings\",\n\t\"main\": \"server.js\",\n\t\"dependencies\": {\n\t\t\"cookie\": \"~0.3.1\",\n\t\t\"get-stream\": \"~5.1.0\",\n\t\t\"n"
},
{
"path": "server.js",
"chars": 7984,
"preview": "\"use strict\";\n\nvar util = require(\"util\");\nvar fs = require(\"fs\");\nvar path = require(\"path\");\nvar http = require(\"http\""
},
{
"path": "web/404.html",
"chars": 564,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Not Found</title>\n<link rel=\"stylesheet\" hre"
},
{
"path": "web/about.html",
"chars": 708,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: About</title>\n<link rel=\"stylesheet\" href=\"/"
},
{
"path": "web/add-post.html",
"chars": 843,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Add Post</title>\n<link rel=\"stylesheet\" href"
},
{
"path": "web/contact.html",
"chars": 686,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Contact</title>\n<link rel=\"stylesheet\" href="
},
{
"path": "web/css/style.css",
"chars": 1327,
"preview": "html {\n\tbox-sizing: border-box;\n\tfont-family: sans-serif;\n\tfont-size: 1.3em;\n}\n\n*,\n*::before,\n*::after {\n\tbox-sizing: in"
},
{
"path": "web/index.html",
"chars": 568,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings</title>\n<link rel=\"stylesheet\" href=\"/css/style"
},
{
"path": "web/js/add-post.js",
"chars": 1851,
"preview": "(function AddPost(){\n\t\"use strict\";\n\n\tvar titleInput;\n\tvar postInput;\n\tvar addPostBtn;\n\n\tdocument.addEventListener(\"DOMC"
},
{
"path": "web/js/blog.js",
"chars": 2285,
"preview": "(function Blog(global){\n\t\"use strict\";\n\n\tvar offlineIcon;\n\tvar isOnline = (\"onLine\" in navigator) && navigator.onLine;\n\t"
},
{
"path": "web/js/home.js",
"chars": 980,
"preview": "(function Home(){\n\t\"use strict\";\n\n\tvar postsList;\n\n\tdocument.addEventListener(\"DOMContentLoaded\",ready,false);\n\n\n\t// ***"
},
{
"path": "web/js/login.js",
"chars": 1290,
"preview": "(function Login(){\n\t\"use strict\";\n\n\tvar usernameInput;\n\tvar passwordInput;\n\tvar loginBtn;\n\n\tdocument.addEventListener(\"D"
},
{
"path": "web/js/sw.js",
"chars": 10224,
"preview": "\"use strict\";\n\nimportScripts(\"/js/external/idb-keyval-iife.min.js\");\n\nvar version = 8;\nvar isOnline = true;\nvar isLogged"
},
{
"path": "web/login.html",
"chars": 724,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Add Post</title>\n<link rel=\"stylesheet\" href"
},
{
"path": "web/offline.html",
"chars": 630,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: Offline!</title>\n<link rel=\"stylesheet\" href"
},
{
"path": "web/posts/post.html",
"chars": 512,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>My Ramblings :: {{TITLE}}</title>\n<link rel=\"stylesheet\" hre"
}
]
About this extraction
This page contains the full source code of the FrontendMasters/service-workers-offline GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (31.5 KB), approximately 9.6k tokens, and a symbol index with 42 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.