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
Not Found
Sorry, that couldn't be found. Please try again.
================================================
FILE: web/about.html
================================================
My Ramblings :: About
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
Add Post
Title:
================================================
FILE: web/contact.html
================================================
My Ramblings :: Contact
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 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
Login
Username:
Password:
================================================
FILE: web/offline.html
================================================
My Ramblings :: Offline!
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}}
{{TITLE}}
{{POST}}