diff --git a/.gitignore b/.gitignore
index 00cbbdf..dbe3e1a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,3 +57,4 @@ typings/
# dotenv environment variables file
.env
+.vscode/
diff --git a/LICENSES.txt b/LICENSES.txt
new file mode 100644
index 0000000..5388ac7
--- /dev/null
+++ b/LICENSES.txt
@@ -0,0 +1,3 @@
+Background Image
+https://www.pexels.com/photo/air-atmosphere-blue-blue-sky-96622/
+CC0 License
\ No newline at end of file
diff --git a/README.md b/README.md
index 70c03e9..6308d09 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,49 @@
-# serviceworker-demo
-Service Worker demos
+# Service Worker App Demo
+
+This is a demo for service workers and web push notifications that work in modern web browsers.
+
+## How to use
+
+First, install all Node.js dependencies via `npm install`.
+
+This demo requires access to a mongodb instance for storing push subscription info to send push updates at some other point in time. It also requires specifying a public and private key for identifying your server to the push service's server. These keys, known as VAPID public/private keys, can be generated and printed to the console when first executing the site. The site can be executed by running `node index.js` which will start a server on port `2020`. You'll need to populate those keys as environment variables and execute `node index.js` again to ensure that push messages can be configured from your server.
+
+If you are using VS Code you can set the environment variables mentioned above in your `launch.json` file as follows:
+
+```js
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch demo",
+ "program": "${workspaceFolder}/serve.js",
+ "env": {
+ "DATABASE_CONNECTION_URI": "YOUR CONNECTION STRING",
+ "VAPID_PUBLIC_KEY": "YOUR PUBLIC KEY",
+ "VAPID_PRIVATE_KEY": "YOUR PRIVATE KEY",
+ }
+ },
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Job",
+ "program": "${workspaceFolder}/notify.js",
+ "env": {
+ "DATABASE_CONNECTION_URI": "YOUR CONNECTION STRING",
+ "VAPID_PUBLIC_KEY": "YOUR PUBLIC KEY",
+ "VAPID_PRIVATE_KEY": "YOUR PRIVATE KEY",
+ }
+ }
+ ]
+}
+```
+
+Alternatively, you can modify `configured-web-push.js` and `db.js` and set the values there explicitly:
+
+```js
+const databaseConnectionURI = process.env.DATABASE_CONNECTION_URI || '';
+const vapidPublicKey = process.env.VAPID_PUBLIC_KEY || '';
+const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY || '';
+```
\ No newline at end of file
diff --git a/configured-web-push.js b/configured-web-push.js
new file mode 100644
index 0000000..9ed3e25
--- /dev/null
+++ b/configured-web-push.js
@@ -0,0 +1,21 @@
+const webPush = require('web-push');
+
+const vapidPublicKey = process.env.VAPID_PUBLIC_KEY || '';
+const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY || '';
+
+if (vapidPublicKey === '' || vapidPrivateKey === '') {
+ console.log("You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " +
+ "environment variables. You can use the following ones:");
+ console.log(webPush.generateVAPIDKeys());
+} else {
+ webPush.setVapidDetails(
+ 'mailto:email@outlook.com',
+ vapidPublicKey,
+ vapidPrivateKey
+ );
+}
+
+module.exports = {
+ webPush: webPush,
+ vapidPublicKey: vapidPublicKey
+}
diff --git a/db.js b/db.js
new file mode 100644
index 0000000..60453f9
--- /dev/null
+++ b/db.js
@@ -0,0 +1,35 @@
+const databaseConnectionURI = process.env.DATABASE_CONNECTION_URI || '';
+
+if (databaseConnectionURI === '') {
+ console.error('Database connection URI not defined.');
+}
+
+const mongoose = require('mongoose');
+mongoose.Promise = global.Promise;
+
+const { Schema } = mongoose;
+
+const SubscriptionSchema = new Schema({
+ endpoint: { type: String, index: true },
+ keys: {
+ auth: { type: String },
+ p256dh: { type: String }
+ },
+ created: {type: Date, default: Date.now }
+}, { collection: 'serviceworkerapp' });
+
+const Subscription = mongoose.model('Subscription', SubscriptionSchema);
+
+const connect = async function() {
+ try {
+ await mongoose.connect(databaseConnectionURI);
+ } catch (e) {
+ console.error('Connection the database failed.');
+ }
+}
+
+connect();
+
+module.exports = {
+ Subscription
+};
diff --git a/notify.js b/notify.js
new file mode 100644
index 0000000..a7b7a9b
--- /dev/null
+++ b/notify.js
@@ -0,0 +1,44 @@
+const { Subscription } = require('./db');
+const configuredWebPush = require('./configured-web-push');
+
+const init = async function() {
+ let pushMessage = "Extreme weather! Come check it out!";
+
+ try {
+ const cursor = Subscription.find().cursor();
+ await cursor.eachAsync(function(sub) {
+ return configuredWebPush.webPush.sendNotification({
+ endpoint: sub.endpoint,
+ keys: {
+ auth: sub.keys.auth,
+ p256dh: sub.keys.p256dh
+ }
+ }, pushMessage, {contentEncoding: 'aes128gcm'})
+ .then(function(push) {
+ console.log(push);
+ })
+ .catch(function(e) {
+ // 404 for FCM AES128GCM
+ if (e.statusCode === 410 || e.statusCode === 404) {
+ // delete invalid registration
+ return Subscription.remove({endpoint: sub.endpoint}).exec()
+ .then(function(sub) {
+ console.log('Deleted: ' + sub.endpoint);
+ })
+ .catch(function(sub) {
+ console.error('Failed to delete: ' + sub.endpoint);
+ });
+ }
+ });
+ });
+ } catch (e) {
+ console.log(e);
+ }
+
+ console.log('Job executed correctly');
+};
+
+init().catch(function(err) {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d014fa9
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "serviceworker-demo",
+ "version": "1.0.0",
+ "description": "Service Worker demos",
+ "main": "serve.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/MicrosoftEdge/serviceworker-demo.git"
+ },
+ "author": "aliams",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/MicrosoftEdge/serviceworker-demo/issues"
+ },
+ "homepage": "https://github.com/MicrosoftEdge/serviceworker-demo#readme",
+ "dependencies": {
+ "express": "^4.16.3",
+ "mongoose": "^5.0.17",
+ "web-push": "^3.3.0"
+ }
+}
diff --git a/public-common/bg.jpg b/public-common/bg.jpg
new file mode 100644
index 0000000..e48d36f
Binary files /dev/null and b/public-common/bg.jpg differ
diff --git a/public-common/common.js b/public-common/common.js
new file mode 100644
index 0000000..e0a4fe8
--- /dev/null
+++ b/public-common/common.js
@@ -0,0 +1,44 @@
+function updatePage(details) {
+ document.querySelector('.current').innerText = details.temp;
+ document.querySelector('.weather-info .description').innerText = details.description;
+
+ document.querySelector('.weather-info .details').innerHTML = '';
+ for (var i = 0; i < details.details.length; i++) {
+ var li = document.createElement('li');
+ var span = document.createElement('span');
+ span.innerText = details.details[i].label;
+ var text = document.createTextNode(' ' + details.details[i].value);
+ li.appendChild(span);
+ li.appendChild(text);
+ document.querySelector('.weather-info .details').appendChild(li);
+ }
+
+ var time = new Date(details.time);
+ var hours = time.getHours();
+ var ampm = hours >= 12 ? 'PM' : 'AM';
+ var minutes = time.getMinutes();
+ hours = hours % 12;
+ hours = hours ? hours : 12;
+ minutes = (minutes < 10 ? '0' : '') + minutes;
+ var lastUpdated = hours + ':' + minutes + ' ' + ampm;
+
+ document.querySelector('.weather-info .last-updated .time').innerText = lastUpdated;
+
+ document.querySelector('.wrapper').style.opacity = 1;
+}
+
+function displayNetworkError() {
+ var wrapperDiv = document.createElement('div');
+ wrapperDiv.className = 'wrapper';
+ var infoDiv = document.createElement('div');
+ infoDiv.className = 'weather-info';
+ var textSpan = document.createElement('span');
+ textSpan.className = 'last-updated';
+ textSpan.style.textAlign = 'center';
+ textSpan.style.paddingTop = '18rem';
+ textSpan.innerText = 'Please connect to the internet and retry.';
+ infoDiv.appendChild(textSpan);
+ wrapperDiv.appendChild(infoDiv);
+ document.querySelector('.wrapper').style.display = 'none';
+ document.querySelector('.bg').appendChild(wrapperDiv);
+}
\ No newline at end of file
diff --git a/public-common/fallback.html b/public-common/fallback.html
new file mode 100644
index 0000000..99cd750
--- /dev/null
+++ b/public-common/fallback.html
@@ -0,0 +1,19 @@
+
+
+
+ Weather
+
+
+
+
+
+
+
+
+ Please connect to the internet and retry.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public-common/fonts/segoeui.ttf b/public-common/fonts/segoeui.ttf
new file mode 100644
index 0000000..edf6731
Binary files /dev/null and b/public-common/fonts/segoeui.ttf differ
diff --git a/public-common/fonts/segoeui.woff b/public-common/fonts/segoeui.woff
new file mode 100644
index 0000000..cf15299
Binary files /dev/null and b/public-common/fonts/segoeui.woff differ
diff --git a/public-common/fonts/segoeui.woff2 b/public-common/fonts/segoeui.woff2
new file mode 100644
index 0000000..3bab420
Binary files /dev/null and b/public-common/fonts/segoeui.woff2 differ
diff --git a/public-common/fonts/segoeuilight.ttf b/public-common/fonts/segoeuilight.ttf
new file mode 100644
index 0000000..08bb6dd
Binary files /dev/null and b/public-common/fonts/segoeuilight.ttf differ
diff --git a/public-common/fonts/segoeuilight.woff b/public-common/fonts/segoeuilight.woff
new file mode 100644
index 0000000..e80798d
Binary files /dev/null and b/public-common/fonts/segoeuilight.woff differ
diff --git a/public-common/fonts/segoeuilight.woff2 b/public-common/fonts/segoeuilight.woff2
new file mode 100644
index 0000000..94c86be
Binary files /dev/null and b/public-common/fonts/segoeuilight.woff2 differ
diff --git a/public-common/fonts/segoeuisemibold.ttf b/public-common/fonts/segoeuisemibold.ttf
new file mode 100644
index 0000000..0fcc0f7
Binary files /dev/null and b/public-common/fonts/segoeuisemibold.ttf differ
diff --git a/public-common/fonts/segoeuisemibold.woff b/public-common/fonts/segoeuisemibold.woff
new file mode 100644
index 0000000..9ee8e4c
Binary files /dev/null and b/public-common/fonts/segoeuisemibold.woff differ
diff --git a/public-common/fonts/segoeuisemibold.woff2 b/public-common/fonts/segoeuisemibold.woff2
new file mode 100644
index 0000000..6b6fab2
Binary files /dev/null and b/public-common/fonts/segoeuisemibold.woff2 differ
diff --git a/public-common/index.html b/public-common/index.html
new file mode 100644
index 0000000..9dfc31e
--- /dev/null
+++ b/public-common/index.html
@@ -0,0 +1,28 @@
+
+
+
+ Weather
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public-common/styles.css b/public-common/styles.css
new file mode 100644
index 0000000..2affd8c
--- /dev/null
+++ b/public-common/styles.css
@@ -0,0 +1,142 @@
+body {
+ background-color: #01579b;
+}
+
+body.desaturate {
+ background-color: #4a4a4a;
+}
+
+*, ::after, ::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+ul {
+ list-style: none;
+}
+
+a.refresh {
+ text-decoration: none;
+ color: #fff;
+}
+
+li a {
+ color: #fff;
+ text-decoration: none;
+}
+
+li a:hover {
+ text-decoration: underline;
+}
+
+.desaturate .bg {
+ filter: grayscale(100%); /* Current draft standard */
+ -webkit-filter: grayscale(100%); /* New WebKit */
+ -moz-filter: grayscale(100%);
+ -ms-filter: grayscale(100%);
+ -o-filter: grayscale(100%); /* Not yet supported in Gecko, Opera or IE */
+ filter: url(resources.svg#desaturate); /* Gecko */
+ filter: gray; /* IE */
+ -webkit-filter: grayscale(1); /* Old WebKit */
+}
+
+.bg {
+ background: #01579b 0 0 no-repeat, url(bg.jpg);
+ background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* FF3.6+ */
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255, 255, 255, 0)), color-stop(100%,rgba(1, 87, 155, 1))), url(bg.jpg); /* Chrome,Safari4+ */
+ background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* Chrome10+,Safari5.1+ */
+ background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* IE10+ */
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%,rgba(1, 87, 155, 1) 100%), url(bg.jpg); /* W3C */
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#01579b',GradientType=0 ); /* IE6-9 */
+ height: 640px;
+}
+
+.wrapper {
+ color: #fff;
+ width: 24rem;
+ margin: 0 auto;
+ padding-top: .5rem;
+}
+
+.wrapper .current-info,
+.wrapper .current,
+.wrapper .type {
+ display: inline-block;
+}
+
+.wrapper .current-info .current {
+ font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif;
+ font-size: 16rem;
+ line-height: 1;
+ margin-left: -1.2rem;
+}
+
+.wrapper .current-info .type,
+.wrapper .current-info .location {
+ font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif;
+ font-size: 6rem;
+ line-height: 1.167;
+ vertical-align: top;
+ margin-top: 1.5rem;
+}
+
+.wrapper .current-info .location {
+ font-size: 3rem;
+ display: block;
+ margin-bottom: -1.4rem;
+}
+
+.wrapper .weather-info>span {
+ font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif;
+ font-size: 3.2rem;
+ line-height: 1.167;
+ display: block;
+ margin-bottom: .5rem;
+}
+
+.wrapper .weather-info .last-updated {
+ margin-bottom: 1.5rem;
+}
+
+.wrapper .weather-info ul,
+.wrapper .weather-info .last-updated {
+ font-family: "Segoe UI", "Segoe UI", Arial, Sans-Serif;
+ font-size: 1.3rem;
+ line-height: 1.384;
+ color: #fff;
+}
+
+.wrapper .weather-info ul li {
+ display: inline-block;
+ width: 100%;
+}
+
+.wrapper .weather-info ul span,
+.wrapper .weather-info .last-updated {
+ font-family: "Segoe UI Light", "Segoe UI", Arial, Sans-Serif;
+ opacity: .8;
+ padding-right: .3rem;
+}
+
+@font-face {
+ font-family: "Segoe UI Light";
+ font-style: normal;
+ font-weight: normal;
+ src: local('Segoe UI Light'), url(fonts/segoeuilight.woff2) format("woff2"), url(fonts/segoeuilight.woff) format("woff"), url(fonts/segoeuilight.ttf) format("truetype");
+}
+
+@font-face {
+ font-family: "Segoe UI";
+ font-style: normal;
+ font-weight: normal;
+ src: local('Segoe UI'), url(fonts/segoeui.woff2) format("woff2"), url(fonts/segoeui.woff) format("woff"), url(fonts/segoeui.ttf) format("truetype");
+}
+
+@font-face {
+ font-family: "Segoe UI Semibold";
+ font-style: normal;
+ font-weight: normal;
+ src: local('Segoe UI Semibold'), url(fonts/segoeuisemibold.woff2) format("woff2"), url(fonts/segoeuisemibold.woff) format("woff"), url(fonts/segoeuisemibold.ttf) format("truetype");
+}
diff --git a/public-common/util.js b/public-common/util.js
new file mode 100644
index 0000000..085784c
--- /dev/null
+++ b/public-common/util.js
@@ -0,0 +1,53 @@
+function urlB64ToUint8Array(base64String) {
+ const padding = '='.repeat((4 - base64String.length % 4) % 4);
+ const base64 = (base64String + padding)
+ .replace(/\-/g, '+')
+ .replace(/_/g, '/');
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+
+ return outputArray;
+}
+
+function subscribePush(registration) {
+ return getPublicKey().then(function(key) {
+ return registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: key
+ });
+ });
+}
+
+function getPublicKey() {
+ return fetch('/push/api/key')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(data) {
+ return urlB64ToUint8Array(data.key);
+ });
+}
+
+function publishSubscription(subscription, remove) {
+ return fetch('/push/api/' + (remove ? 'un' : '') + 'subscribe', {
+ method: 'post',
+ headers: {
+ 'Content-type': 'application/json'
+ },
+ body: JSON.stringify({
+ subscription: subscription
+ })
+ });
+}
+
+function saveSubscription(subscription) {
+ return publishSubscription(subscription);
+}
+
+function deleteSubscription(subscription) {
+ return publishSubscription(subscription, true);
+}
diff --git a/public/appcache/appcache.manifest b/public/appcache/appcache.manifest
new file mode 100644
index 0000000..6de6f2a
--- /dev/null
+++ b/public/appcache/appcache.manifest
@@ -0,0 +1,15 @@
+CACHE MANIFEST
+# 2018-05-08 v2
+
+CACHE:
+./styles.css
+./bg.jpg
+./weather.json
+./common.js
+./main.js
+
+NETWORK:
+*
+
+FALLBACK:
+./contact-us.html ./fallback.html
diff --git a/public/appcache/index.html b/public/appcache/index.html
new file mode 100644
index 0000000..e66a962
--- /dev/null
+++ b/public/appcache/index.html
@@ -0,0 +1,27 @@
+
+
+
+ Weather
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/appcache/main.js b/public/appcache/main.js
new file mode 100644
index 0000000..d6370d2
--- /dev/null
+++ b/public/appcache/main.js
@@ -0,0 +1,11 @@
+document.addEventListener('DOMContentLoaded', function() {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status == 200) {
+ updatePage(JSON.parse(this.responseText));
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ }
+ };
+ xhr.open('GET', './weather.json', true);
+ xhr.send();
+});
diff --git a/public/basic/main.js b/public/basic/main.js
new file mode 100644
index 0000000..9043f99
--- /dev/null
+++ b/public/basic/main.js
@@ -0,0 +1,36 @@
+if (navigator.serviceWorker) {
+ navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!'); });
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelector('.refresh').addEventListener('click', function(event) {
+ event.preventDefault();
+ document.querySelector('.wrapper').style.opacity = .5;
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ })
+ .catch(function() {
+ document.querySelector('.wrapper').style.opacity = 1;
+ document.body.className = 'desaturate';
+ });
+ });
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ })
+ .catch(function() {
+ displayNetworkError();
+ });
+});
diff --git a/public/basic/sw.js b/public/basic/sw.js
new file mode 100644
index 0000000..0bc6344
--- /dev/null
+++ b/public/basic/sw.js
@@ -0,0 +1,5 @@
+self.onfetch = function(event) {
+ event.respondWith(
+ fetch(event.request)
+ );
+}
diff --git a/public/basic1/main.js b/public/basic1/main.js
new file mode 100644
index 0000000..9043f99
--- /dev/null
+++ b/public/basic1/main.js
@@ -0,0 +1,36 @@
+if (navigator.serviceWorker) {
+ navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!'); });
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelector('.refresh').addEventListener('click', function(event) {
+ event.preventDefault();
+ document.querySelector('.wrapper').style.opacity = .5;
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ })
+ .catch(function() {
+ document.querySelector('.wrapper').style.opacity = 1;
+ document.body.className = 'desaturate';
+ });
+ });
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ })
+ .catch(function() {
+ displayNetworkError();
+ });
+});
diff --git a/public/basic1/sw.js b/public/basic1/sw.js
new file mode 100644
index 0000000..6f20dc7
--- /dev/null
+++ b/public/basic1/sw.js
@@ -0,0 +1,40 @@
+self.oninstall = function(event) {
+ event.waitUntil(
+ caches.open('assets-v1').then(function(cache) {
+ return cache.addAll([
+ './',
+ './index.html',
+ './styles.css',
+ './bg.jpg',
+ './common.js',
+ './util.js',
+ './main.js',
+ './fallback.html'
+ ]);
+ })
+ );
+}
+
+self.onactivate = function(event) {
+ var keepList = ['assets-v1'];
+
+ event.waitUntil(
+ caches.keys().then(function(cacheNameList) {
+ return Promise.all(cacheNameList.map(function(cacheName) {
+ if (keepList.indexOf(cacheName) === -1) {
+ return caches.delete(cacheName);
+ }
+ }));
+ })
+ );
+}
+
+self.onfetch = function(event) {
+ event.respondWith(
+ caches.match(event.request).then(function(response) {
+ return response || fetch(event.request).catch(function() {
+ return caches.match('./fallback.html');
+ });
+ })
+ );
+}
diff --git a/public/basic2/main.js b/public/basic2/main.js
new file mode 100644
index 0000000..551a53d
--- /dev/null
+++ b/public/basic2/main.js
@@ -0,0 +1,35 @@
+if (navigator.serviceWorker) {
+ navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!'); });
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelector('.refresh').addEventListener('click', function(event) {
+ event.preventDefault();
+ document.querySelector('.wrapper').style.opacity = .5;
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ })
+ .catch(function() {
+ document.querySelector('.wrapper').style.opacity = 1;
+ document.body.className = 'desaturate';
+ });
+ });
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ })
+ .catch(function() {
+ displayNetworkError();
+ });
+});
diff --git a/public/basic2/sw.js b/public/basic2/sw.js
new file mode 100644
index 0000000..a7407f1
--- /dev/null
+++ b/public/basic2/sw.js
@@ -0,0 +1,43 @@
+self.oninstall = function(event) {
+ event.waitUntil(
+ caches.open('assets-v1').then(function(cache) {
+ return cache.addAll([
+ './',
+ './index.html',
+ './styles.css',
+ './bg.jpg',
+ './common.js',
+ './util.js',
+ './main.js',
+ './fallback.html'
+ ]);
+ })
+ );
+}
+
+self.onactivate = function(event) {
+ var keepList = ['assets-v1'];
+
+ event.waitUntil(
+ caches.keys().then(function(cacheNameList) {
+ return Promise.all(cacheNameList.map(function(cacheName) {
+ if (keepList.indexOf(cacheName) === -1) {
+ return caches.delete(cacheName);
+ }
+ }));
+ })
+ );
+}
+
+self.onfetch = function(event) {
+ event.respondWith(
+ caches.match(event.request).then(function(response) {
+ return response || fetch(event.request).then(function(response) {
+ caches.open('dynamic').then(function(cache) {
+ cache.put(event.request, response);
+ });
+ return response.clone();
+ });
+ })
+ );
+}
diff --git a/public/basic3/main.js b/public/basic3/main.js
new file mode 100644
index 0000000..3c02041
--- /dev/null
+++ b/public/basic3/main.js
@@ -0,0 +1,46 @@
+if (navigator.serviceWorker) {
+ navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!') });
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelector('.refresh').addEventListener('click', function(event) {
+ event.preventDefault();
+ document.querySelector('.wrapper').style.opacity = .5;
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ })
+ .catch(function() {
+ document.querySelector('.wrapper').style.opacity = 1;
+ document.body.className = 'desaturate';
+ });
+ });
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ })
+ .catch(function() {
+ return caches.match('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ });
+ })
+ .catch(function() {
+ displayNetworkError();
+ });
+});
diff --git a/public/basic3/sw.js b/public/basic3/sw.js
new file mode 100644
index 0000000..4cebac4
--- /dev/null
+++ b/public/basic3/sw.js
@@ -0,0 +1,51 @@
+self.oninstall = function(event) {
+ event.waitUntil(
+ caches.open('assets-v1').then(function(cache) {
+ return cache.addAll([
+ './',
+ './index.html',
+ './styles.css',
+ './bg.jpg',
+ './common.js',
+ './util.js',
+ './main.js',
+ './fallback.html'
+ ]);
+ })
+ );
+}
+
+self.onactivate = function(event) {
+ var keepList = ['assets-v1'];
+
+ event.waitUntil(
+ caches.keys().then(function(cacheNameList) {
+ return Promise.all(cacheNameList.map(function(cacheName) {
+ if (keepList.indexOf(cacheName) === -1) {
+ return caches.delete(cacheName);
+ }
+ }));
+ })
+ );
+}
+
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('weather.json') !== -1) {
+ event.respondWith(fetchAndCache(event.request));
+ } else {
+ event.respondWith(
+ caches.match(event.request).then(function(response) {
+ return response || fetchAndCache(event.request);
+ })
+ );
+ }
+}
+
+function fetchAndCache(request) {
+ return fetch(request).then(function(response) {
+ caches.open('dynamic').then(function(cache) {
+ cache.put(request, response);
+ });
+ return response.clone();
+ });
+}
diff --git a/public/improved/main.js b/public/improved/main.js
new file mode 100644
index 0000000..e8007d6
--- /dev/null
+++ b/public/improved/main.js
@@ -0,0 +1,62 @@
+if (navigator.serviceWorker) {
+ navigator.serviceWorker.register('sw.js').then(function() { console.log('SW registered!') });
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelector('.refresh').addEventListener('click', function(event) {
+ event.preventDefault();
+ document.querySelector('.wrapper').style.opacity = .5;
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ })
+ .catch(function() {
+ document.querySelector('.wrapper').style.opacity = 1;
+ document.body.className = 'desaturate';
+ });
+ });
+
+ function showLoading() {
+
+ }
+
+ function hideLoading() {
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ }
+
+ showLoading();
+
+ var networkDone = false;
+
+ var networkRequest = fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ networkDone = true;
+ updatePage(json);
+ document.body.className = '';
+ });
+
+ caches.match('./weather.json')
+ .then(function(response) {
+ if (!response) {
+ throw Error('No data');
+ }
+
+ return response.json();
+ })
+ .then(function(json) {
+ if (!networkDone) updatePage(json);
+ })
+ .catch(function() {return networkRequest;})
+ .catch(function() {
+ displayNetworkError();
+ })
+ .then(hideLoading);
+});
diff --git a/public/improved/sw.js b/public/improved/sw.js
new file mode 100644
index 0000000..4cebac4
--- /dev/null
+++ b/public/improved/sw.js
@@ -0,0 +1,51 @@
+self.oninstall = function(event) {
+ event.waitUntil(
+ caches.open('assets-v1').then(function(cache) {
+ return cache.addAll([
+ './',
+ './index.html',
+ './styles.css',
+ './bg.jpg',
+ './common.js',
+ './util.js',
+ './main.js',
+ './fallback.html'
+ ]);
+ })
+ );
+}
+
+self.onactivate = function(event) {
+ var keepList = ['assets-v1'];
+
+ event.waitUntil(
+ caches.keys().then(function(cacheNameList) {
+ return Promise.all(cacheNameList.map(function(cacheName) {
+ if (keepList.indexOf(cacheName) === -1) {
+ return caches.delete(cacheName);
+ }
+ }));
+ })
+ );
+}
+
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('weather.json') !== -1) {
+ event.respondWith(fetchAndCache(event.request));
+ } else {
+ event.respondWith(
+ caches.match(event.request).then(function(response) {
+ return response || fetchAndCache(event.request);
+ })
+ );
+ }
+}
+
+function fetchAndCache(request) {
+ return fetch(request).then(function(response) {
+ caches.open('dynamic').then(function(cache) {
+ cache.put(request, response);
+ });
+ return response.clone();
+ });
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..ab924dd
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,32 @@
+
+
+
+ Service Worker Demos
+
+
+
+
+
+
+
+
+
Service Worker
+
Weather App Demos
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/push/main.js b/public/push/main.js
new file mode 100644
index 0000000..0be0afa
--- /dev/null
+++ b/public/push/main.js
@@ -0,0 +1,126 @@
+if (navigator.serviceWorker) {
+ registerServiceWorker().then(function() {
+ registerPush().then(function(sub) {
+ sendMessage(sub, 'Extreme weather! Come check it out!');
+ });
+ });
+} else {
+ // service worker is not supported, so it won't work!
+ console.log('SW & Push are Not Supported');
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ document.querySelector('.refresh').addEventListener('click', function(event) {
+ event.preventDefault();
+ document.querySelector('.wrapper').style.opacity = .5;
+
+ fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ updatePage(json);
+ document.body.className = '';
+ })
+ .catch(function() {
+ document.querySelector('.wrapper').style.opacity = 1;
+ document.body.className = 'desaturate';
+ });
+ });
+
+ function showLoading() {
+
+ }
+
+ function hideLoading() {
+ document.querySelector('.wrapper').style.visibility = 'visible';
+ }
+
+ showLoading();
+
+ var networkDone = false;
+
+ var networkRequest = fetch('./weather.json')
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ networkDone = true;
+ updatePage(json);
+ document.body.className = '';
+ });
+
+ caches.match('./weather.json')
+ .then(function(response) {
+ if (!response) {
+ throw Error('No data');
+ }
+
+ return response.json();
+ })
+ .then(function(json) {
+ if (!networkDone) updatePage(json);
+ })
+ .catch(function() {return networkRequest;})
+ .catch(function() {
+ displayNetworkError();
+ })
+ .then(hideLoading);
+});
+
+function registerServiceWorker() {
+ return navigator.serviceWorker.register('sw.js');
+}
+
+function resetServiceWorkerAndPush() {
+ return navigator.serviceWorker.getRegistration()
+ .then(function(registration) {
+ if (registration) {
+ return registration.unregister();
+ }
+ })
+ .then(function() {
+ return registerServiceWorker().then(function(registration) {
+ return registerPush();
+ });
+ });
+}
+
+function registerPush() {
+ return navigator.serviceWorker.ready
+ .then(function(registration) {
+ return registration.pushManager.getSubscription().then(function(subscription) {
+ if (subscription) {
+ // renew subscription if we're within 5 days of expiration
+ if (subscription.expirationTime && Date.now() > subscription.expirationTime - 432000000) {
+ return subscription.unsubscribe().then(function() {
+ return subscribePush(registration);
+ });
+ }
+
+ return subscription;
+ }
+
+ return subscribePush(registration);
+ });
+ })
+ .then(function(subscription) {
+ saveSubscription(subscription);
+ return subscription;
+ });
+}
+
+function sendMessage(sub, message) {
+ const data = {
+ subscription: sub,
+ payload: message
+ }
+
+ return fetch('./api/notify', {
+ method: 'post',
+ headers: {
+ 'Content-type': 'application/json'
+ },
+ body: JSON.stringify(data)
+ });
+}
diff --git a/public/push/sw.js b/public/push/sw.js
new file mode 100644
index 0000000..f201193
--- /dev/null
+++ b/public/push/sw.js
@@ -0,0 +1,94 @@
+importScripts('./util.js');
+
+self.oninstall = function(event) {
+ event.waitUntil(
+ caches.open('assets-v1').then(function(cache) {
+ return cache.addAll([
+ './',
+ './index.html',
+ './styles.css',
+ './bg.jpg',
+ './common.js',
+ './util.js',
+ './main.js'
+ ]);
+ })
+ );
+}
+
+self.onactivate = function(event) {
+ var keepList = ['assets-v1'];
+
+ event.waitUntil(
+ caches.keys().then(function(cacheNameList) {
+ return Promise.all(cacheNameList.map(function(cacheName) {
+ if (keepList.indexOf(cacheName) === -1) {
+ return caches.delete(cacheName);
+ }
+ }));
+ })
+ );
+}
+
+function fetchAndCache(request) {
+ return fetch(request).then(function(response) {
+ caches.open('dynamic').then(function(cache) {
+ cache.put(request, response);
+ });
+ return response.clone();
+ });
+}
+
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('weather.json') !== -1) {
+ event.respondWith(fetchAndCache(event.request));
+ } else {
+ event.respondWith(
+ caches.match(event.request).then(function(response) {
+ return response || fetchAndCache(event.request);
+ })
+ );
+ }
+}
+
+self.onpush = function(event) {
+ const payload = event.data ? event.data.text() : 'no payload';
+ event.waitUntil(
+ registration.showNotification('Weather Advisory', {
+ body: payload,
+ icon: 'icon.png'
+ })
+ );
+}
+
+self.onnotificationclick = function(event) {
+ event.notification.close();
+
+ event.waitUntil(
+ clients.matchAll({ type: 'window', includeUncontrolled: true })
+ .then(function(clientList) {
+ if (clientList.length > 0) {
+ let client = clientList[0];
+ for (let i = 0; i < clientList.length; i++) {
+ if (clientList[i].focused) { client = clientList[i]; }
+ }
+ return client.focus();
+ }
+
+ return clients.openWindow('/push/');
+ })
+ );
+}
+
+self.onpushsubscriptionchange = function(event) {
+ event.waitUntil(
+ Promise.all([
+ Promise.resolve(event.oldSubscription ?
+ deleteSubscription(event.oldSubscription) : true),
+ Promise.resolve(event.newSubscription ?
+ event.newSubscription :
+ subscribePush(registration))
+ .then(function(sub) { return saveSubscription(sub); })
+ ])
+ );
+}
diff --git a/serve-push.js b/serve-push.js
new file mode 100644
index 0000000..4b575a6
--- /dev/null
+++ b/serve-push.js
@@ -0,0 +1,74 @@
+const express = require('express');
+const bodyParser = require('body-parser');
+const configuredWebPush = require('./configured-web-push');
+const { Subscription } = require('./db');
+const router = express.Router();
+router.use(bodyParser.json());
+
+// Push Logic
+router.get('/api/key', function(req, res) {
+ if (configuredWebPush.vapidPublicKey !== '') {
+ res.send({
+ key: configuredWebPush.vapidPublicKey
+ });
+ } else {
+ res.status(500).send({
+ key: 'VAPID KEYS ARE NOT SET'
+ });
+ }
+});
+
+router.post('/api/subscribe', async function(req, res) {
+ try {
+ const sub = req.body.subscription;
+
+ // Find if user is already subscribed searching by `endpoint`
+ const exists = await Subscription.findOne({ endpoint: sub.endpoint });
+
+ if (exists) {
+ res.status(400).send('Subscription already exists');
+
+ return;
+ }
+
+ await (new Subscription(sub)).save();
+
+ res.status(200).send('Success');
+ } catch (e) {
+ res.status(500).send(e.message);
+ }
+});
+
+router.post('/api/unsubscribe', async function(req, res) {
+ try {
+ const sub = req.body.subscription;
+
+ await Subscription.remove({endpoint: sub.endpoint});
+ console.log('Deleted: ' + sub.endpoint);
+
+ res.status(200).send('Success');
+ } catch (e) {
+ res.status(500).send(e.message);
+ }
+});
+
+router.post('/api/notify', async function(req, res) {
+ try {
+ const data = req.body;
+
+ await configuredWebPush.webPush.sendNotification(data.subscription, data.payload, { contentEncoding: data.encoding })
+ .then(function (response) {
+ console.log('Response: ' + JSON.stringify(response, null, 4));
+ res.status(201).send(response);
+ })
+ .catch(function (e) {
+ console.log('Error: ' + JSON.stringify(e, null, 4));
+ res.status(201).send(e);
+ });
+ } catch (e) {
+ res.status(500)
+ .send(e.message);
+ }
+});
+
+module.exports = router;
diff --git a/serve.js b/serve.js
new file mode 100644
index 0000000..2fb799f
--- /dev/null
+++ b/serve.js
@@ -0,0 +1,91 @@
+const express = require('express');
+
+const app = express();
+const port = process.env.PORT || 2020;
+
+app.use(express.static('public'));
+app.use('/push', require('./serve-push'));
+app.disable('x-powered-by');
+
+app.get('/appcache/appcache.manifest', function(req, res) {
+ res.set('Content-type', 'text/cache-manifest').status(200).send();
+});
+
+const { lstatSync, readdirSync } = require('fs');
+const { join } = require('path');
+
+const isDirectory = source => lstatSync(source).isDirectory();
+const getDirectories = source =>
+ readdirSync(source).map(name => name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'))//.filter(isDirectory).map(name => name);
+
+const dirs = getDirectories('public').join('|');
+
+// const files = getDirectories('public-common');
+
+// for (file of files) {
+// app.get(getRegExp(file), function (req, res) {
+// res.sendFile()
+// });
+// }
+
+function getRegExp(path) {
+ return new RegExp(`/(${dirs})/${path}`);
+}
+
+app.get(getRegExp('weather.json'), async function(req, res) {
+ let temp;
+ let description;
+ let details;
+
+ function getRandomInt(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ }
+
+ function pad(num) {
+ return (num < 10 ? '0' : '') + num;
+ }
+
+ weatherTypes = [
+ 'Mostly Sunny',
+ 'Partly Sunny',
+ 'Mostly Cloudy',
+ 'Partly Cloudy',
+ 'Rain',
+ 'Snow'
+ ];
+
+ temp = getRandomInt(0, 99);
+ description = weatherTypes[getRandomInt(0, weatherTypes.length - 1)];
+ details = [
+ {label: 'Feels Like', value: getRandomInt(temp - 10, temp + 10) + '°'},
+ {label: 'Wind', value: getRandomInt(0, 35) + ' mph'},
+ {label: 'Barometer', value: getRandomInt(29, 31) + '.' + pad(getRandomInt(0, 99)) + ' in'},
+ {label: 'Visibility', value: getRandomInt(1, 50) + ' mi'},
+ {label: 'Humidity', value: getRandomInt(20, 90) + '%'},
+ {label: 'Dew Point', value: getRandomInt(temp - 20, temp - 5) + '°'}
+ ];
+
+ setTimeout(function() {
+ res.status(200).send({
+ temp: temp,
+ description: description,
+ details: details,
+ time: Date.now()
+ });
+ }, 2000);
+});
+
+app.get('/appcache(/index.html)?', function(req, res) {
+ res.sendFile(__dirname + '/public/' + req.params[1] + '/index.html');
+});
+
+app.get(getRegExp('fonts/(.*)'), function(req, res) {
+ res.sendFile(__dirname + '/public-common/fonts/' + req.params[1]);
+});
+app.get(getRegExp('(index.html|bg.jpg|fallback.html|styles.css|common.js|util.js)?'), function(req, res) {
+ res.sendFile(__dirname + '/public-common/' + (req.params[1] === undefined ? 'index.html' : req.params[1]));
+});
+
+app.listen(port, function() {
+ console.log('Server listening on port ' + port);
+});
diff --git a/web.config b/web.config
new file mode 100644
index 0000000..ba8f527
--- /dev/null
+++ b/web.config
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+