From 85a014bd337de356f1bb737c944d64e26b6690d6 Mon Sep 17 00:00:00 2001 From: Sven SAULEAU Date: Tue, 15 Nov 2016 22:40:13 +0100 Subject: [PATCH 01/19] feat: support offline with service worker --- scripts/index.js | 9 +++++ service-worker.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 service-worker.js diff --git a/scripts/index.js b/scripts/index.js index 9b146fab72..2055c84a5a 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -29,3 +29,12 @@ $(document).ready(function() { }] }); }); + +if ('serviceWorker' in navigator) { + + navigator.serviceWorker.register('/service-worker.js').then(function() { + console.log('CLIENT: service worker registration complete.'); + }, function() { + console.log('CLIENT: service worker registration failure.'); + }); +} diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 0000000000..bbcfd18a6b --- /dev/null +++ b/service-worker.js @@ -0,0 +1,98 @@ +--- +--- + +var version = 'v1'; + +self.addEventListener("install", function(event) { + console.log('WORKER: install event in progress.'); + event.waitUntil( + + caches + .open(version) + .then(function(cache) { + + return cache.addAll([ + '/', + '{{ "/css/main.css" | prepend: site.baseurl }}?t={{ site.time | date_to_xmlschema }}"', + {% for page in site.pages %} + '{{ page.url }}', + {% endfor %} + + ]); + }) + .then(function() { + console.log('WORKER: install completed'); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + console.log('WORKER: fetch event in progress.'); + + event.respondWith( + caches + .match(event.request) + .then(function(cached) { + + var networked = fetch(event.request) + .then(fetchedFromNetwork, unableToResolve) + .catch(unableToResolve); + + console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url); + return cached || networked; + + function fetchedFromNetwork(response) { + var cacheCopy = response.clone(); + + console.log('WORKER: fetch response from network.', event.request.url); + + caches + .open(version + 'pages') + .then(function add(cache) { + cache.put(event.request, cacheCopy); + }) + .then(function() { + console.log('WORKER: fetch response stored in cache.', event.request.url); + }); + + return response; + } + + function unableToResolve () { + console.log('WORKER: fetch request failed in both cache and network.'); + + return new Response('

Service Unavailable

', { + status: 503, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); + } + }) + ); +}); + +self.addEventListener("activate", function(event) { + console.log('WORKER: activate event in progress.'); + + event.waitUntil( + caches + .keys() + .then(function (keys) { + + return Promise.all( + keys + .filter(function (key) { + return !key.startsWith(version); + }) + .map(function (key) { + return caches.delete(key); + }) + ); + }) + .then(function() { + console.log('WORKER: activate completed.'); + }) + ); +}); From 1bd6f2d0bed174527cf961232b5d438332d52b76 Mon Sep 17 00:00:00 2001 From: Sven SAULEAU Date: Mon, 21 Nov 2016 08:33:57 +0100 Subject: [PATCH 02/19] refactor(offline): use sw-toolbox --- scripts/sw-toolbox.js | 16 +++++++ service-worker.js | 103 +++++------------------------------------- 2 files changed, 28 insertions(+), 91 deletions(-) create mode 100644 scripts/sw-toolbox.js diff --git a/scripts/sw-toolbox.js b/scripts/sw-toolbox.js new file mode 100644 index 0000000000..413fc72a11 --- /dev/null +++ b/scripts/sw-toolbox.js @@ -0,0 +1,16 @@ +/* + Copyright 2016 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.toolbox=e()}}(function(){return function e(t,n,r){function o(c,s){if(!n[c]){if(!t[c]){var a="function"==typeof require&&require;if(!s&&a)return a(c,!0);if(i)return i(c,!0);var u=new Error("Cannot find module '"+c+"'");throw u.code="MODULE_NOT_FOUND",u}var f=n[c]={exports:{}};t[c][0].call(f.exports,function(e){var n=t[c][1][e];return o(n?n:e)},f,f.exports,e,t,n,r)}return n[c].exports}for(var i="function"==typeof require&&require,c=0;ct.value[l]){var r=t.value[p];c.push(r),a.delete(r),t.continue()}},s.oncomplete=function(){r(c)},s.onabort=o}):Promise.resolve([])}function s(e,t){return t?new Promise(function(n,r){var o=[],i=e.transaction(h,"readwrite"),c=i.objectStore(h),s=c.index(l),a=s.count();s.count().onsuccess=function(){var e=a.result;e>t&&(s.openCursor().onsuccess=function(n){var r=n.target.result;if(r){var i=r.value[p];o.push(i),c.delete(i),e-o.length>t&&r.continue()}})},i.oncomplete=function(){n(o)},i.onabort=r}):Promise.resolve([])}function a(e,t,n,r){return c(e,n,r).then(function(n){return s(e,t).then(function(e){return n.concat(e)})})}var u="sw-toolbox-",f=1,h="store",p="url",l="timestamp",d={};t.exports={getDb:o,setTimestampForUrl:i,expireEntries:a}},{}],3:[function(e,t,n){"use strict";function r(e){var t=a.match(e.request);t?e.respondWith(t(e.request)):a.default&&"GET"===e.request.method&&0===e.request.url.indexOf("http")&&e.respondWith(a.default(e.request))}function o(e){s.debug("activate event fired");var t=u.cache.name+"$$$inactive$$$";e.waitUntil(s.renameCache(t,u.cache.name))}function i(e){return e.reduce(function(e,t){return e.concat(t)},[])}function c(e){var t=u.cache.name+"$$$inactive$$$";s.debug("install event fired"),s.debug("creating cache ["+t+"]"),e.waitUntil(s.openCache({cache:{name:t}}).then(function(e){return Promise.all(u.preCacheItems).then(i).then(s.validatePrecacheInput).then(function(t){return s.debug("preCache list: "+(t.join(", ")||"(none)")),e.addAll(t)})}))}e("serviceworker-cache-polyfill");var s=e("./helpers"),a=e("./router"),u=e("./options");t.exports={fetchListener:r,activateListener:o,installListener:c}},{"./helpers":1,"./options":4,"./router":6,"serviceworker-cache-polyfill":16}],4:[function(e,t,n){"use strict";var r;r=self.registration?self.registration.scope:self.scope||new URL("./",self.location).href,t.exports={cache:{name:"$$$toolbox-cache$$$"+r+"$$$",maxAgeSeconds:null,maxEntries:null},debug:!1,networkTimeoutSeconds:null,preCacheItems:[],successResponses:/^0|([123]\d\d)|(40[14567])|410$/}},{}],5:[function(e,t,n){"use strict";var r=new URL("./",self.location),o=r.pathname,i=e("path-to-regexp"),c=function(e,t,n,r){t instanceof RegExp?this.fullUrlRegExp=t:(0!==t.indexOf("/")&&(t=o+t),this.keys=[],this.regexp=i(t,this.keys)),this.method=e,this.options=r,this.handler=n};c.prototype.makeHandler=function(e){var t;if(this.regexp){var n=this.regexp.exec(e);t={},this.keys.forEach(function(e,r){t[e.name]=n[r+1]})}return function(e){return this.handler(e,t,this.options)}.bind(this)},t.exports=c},{"path-to-regexp":15}],6:[function(e,t,n){"use strict";function r(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var o=e("./route"),i=e("./helpers"),c=function(e,t){for(var n=e.entries(),r=n.next(),o=[];!r.done;){var i=new RegExp(r.value[0]);i.test(t)&&o.push(r.value[1]),r=n.next()}return o},s=function(){this.routes=new Map,this.routes.set(RegExp,new Map),this.default=null};["get","post","put","delete","head","any"].forEach(function(e){s.prototype[e]=function(t,n,r){return this.add(e,t,n,r)}}),s.prototype.add=function(e,t,n,c){c=c||{};var s;t instanceof RegExp?s=RegExp:(s=c.origin||self.location.origin,s=s instanceof RegExp?s.source:r(s)),e=e.toLowerCase();var a=new o(e,t,n,c);this.routes.has(s)||this.routes.set(s,new Map);var u=this.routes.get(s);u.has(e)||u.set(e,new Map);var f=u.get(e),h=a.regexp||a.fullUrlRegExp;f.has(h.source)&&i.debug('"'+t+'" resolves to same regex as existing route.'),f.set(h.source,a)},s.prototype.matchMethod=function(e,t){var n=new URL(t),r=n.origin,o=n.pathname;return this._match(e,c(this.routes,r),o)||this._match(e,[this.routes.get(RegExp)],t)},s.prototype._match=function(e,t,n){if(0===t.length)return null;for(var r=0;r0)return s[0].makeHandler(n)}}return null},s.prototype.match=function(e){return this.matchMethod(e.method,e.url)||this.matchMethod("any",e.url)},t.exports=new s},{"./helpers":1,"./route":5}],7:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: cache first ["+e.url+"]",n),o.openCache(n).then(function(t){return t.match(e).then(function(t){return t?t:o.fetchAndCache(e,n)})})}var o=e("../helpers");t.exports=r},{"../helpers":1}],8:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: cache only ["+e.url+"]",n),o.openCache(n).then(function(t){return t.match(e)})}var o=e("../helpers");t.exports=r},{"../helpers":1}],9:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: fastest ["+e.url+"]",n),new Promise(function(r,c){var s=!1,a=[],u=function(e){a.push(e.toString()),s?c(new Error('Both cache and network failed: "'+a.join('", "')+'"')):s=!0},f=function(e){e instanceof Response?r(e):u("No result returned")};o.fetchAndCache(e.clone(),n).then(f,u),i(e,t,n).then(f,u)})}var o=e("../helpers"),i=e("./cacheOnly");t.exports=r},{"../helpers":1,"./cacheOnly":8}],10:[function(e,t,n){t.exports={networkOnly:e("./networkOnly"),networkFirst:e("./networkFirst"),cacheOnly:e("./cacheOnly"),cacheFirst:e("./cacheFirst"),fastest:e("./fastest")}},{"./cacheFirst":7,"./cacheOnly":8,"./fastest":9,"./networkFirst":11,"./networkOnly":12}],11:[function(e,t,n){"use strict";function r(e,t,n){n=n||{};var r=n.successResponses||o.successResponses,c=n.networkTimeoutSeconds||o.networkTimeoutSeconds;return i.debug("Strategy: network first ["+e.url+"]",n),i.openCache(n).then(function(t){var o,s,a=[];if(c){var u=new Promise(function(n){o=setTimeout(function(){t.match(e).then(function(e){e&&n(e)})},1e3*c)});a.push(u)}var f=i.fetchAndCache(e,n).then(function(e){if(o&&clearTimeout(o),r.test(e.status))return e;throw i.debug("Response was an HTTP error: "+e.statusText,n),s=e,new Error("Bad response")}).catch(function(r){return i.debug("Network or response error, fallback to cache ["+e.url+"]",n),t.match(e).then(function(e){if(e)return e;if(s)return s;throw r})});return a.push(f),Promise.race(a)})}var o=e("../options"),i=e("../helpers");t.exports=r},{"../helpers":1,"../options":4}],12:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: network only ["+e.url+"]",n),fetch(e)}var o=e("../helpers");t.exports=r},{"../helpers":1}],13:[function(e,t,n){"use strict";var r=e("./options"),o=e("./router"),i=e("./helpers"),c=e("./strategies"),s=e("./listeners");i.debug("Service Worker Toolbox is loading"),self.addEventListener("install",s.installListener),self.addEventListener("activate",s.activateListener),self.addEventListener("fetch",s.fetchListener),t.exports={networkOnly:c.networkOnly,networkFirst:c.networkFirst,cacheOnly:c.cacheOnly,cacheFirst:c.cacheFirst,fastest:c.fastest,router:o,options:r,cache:i.cache,uncache:i.uncache,precache:i.precache}},{"./helpers":1,"./listeners":3,"./options":4,"./router":6,"./strategies":10}],14:[function(e,t,n){t.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},{}],15:[function(e,t,n){function r(e){for(var t,n=[],r=0,o=0,i="";null!=(t=x.exec(e));){var c=t[0],s=t[1],a=t.index;if(i+=e.slice(o,a),o=a+c.length,s)i+=s[1];else{var f=e[o],h=t[2],p=t[3],l=t[4],d=t[5],g=t[6],m=t[7];i&&(n.push(i),i="");var v=null!=h&&null!=f&&f!==h,w="+"===g||"*"===g,y="?"===g||"*"===g,b=t[2]||"/",E=l||d||(m?".*":"[^"+b+"]+?");n.push({name:p||r++,prefix:h||"",delimiter:b,optional:y,repeat:w,partial:v,asterisk:!!m,pattern:u(E)})}}return o=46||"Chrome"===n&&r>=50)||(Cache.prototype.addAll=function(e){function t(e){this.name="NetworkError",this.code=19,this.message=e}var n=this;return t.prototype=Object.create(Error.prototype),Promise.resolve().then(function(){if(arguments.length<1)throw new TypeError;return e=e.map(function(e){return e instanceof Request?e:String(e)}),Promise.all(e.map(function(e){"string"==typeof e&&(e=new Request(e));var n=new URL(e.url).protocol;if("http:"!==n&&"https:"!==n)throw new t("Invalid scheme");return fetch(e.clone())}))}).then(function(r){if(r.some(function(e){return!e.ok}))throw new t("Incorrect response status");return Promise.all(r.map(function(t,r){return n.put(e[r],t)}))}).then(function(){})},Cache.prototype.add=function(e){return this.addAll([e])})}()},{}]},{},[13])(13)}); +//# sourceMappingURL=sw-toolbox.js.map diff --git a/service-worker.js b/service-worker.js index bbcfd18a6b..aa66e94cdb 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,98 +1,19 @@ --- --- -var version = 'v1'; +importScripts('/scripts/sw-toolbox.js'); -self.addEventListener("install", function(event) { - console.log('WORKER: install event in progress.'); - event.waitUntil( +var swOptions = { + debug: true +} - caches - .open(version) - .then(function(cache) { +var preCachedRessources = [ + '{{ "/css/main.css" | prepend: site.baseurl }}?t={{ site.time | date_to_xmlschema }}"', + {% for page in site.pages %} + '{{ page.url }}', + {% endfor %} +]; - return cache.addAll([ - '/', - '{{ "/css/main.css" | prepend: site.baseurl }}?t={{ site.time | date_to_xmlschema }}"', - {% for page in site.pages %} - '{{ page.url }}', - {% endfor %} +toolbox.precache(preCachedRessources); - ]); - }) - .then(function() { - console.log('WORKER: install completed'); - }) - ); -}); - -self.addEventListener("fetch", function(event) { - console.log('WORKER: fetch event in progress.'); - - event.respondWith( - caches - .match(event.request) - .then(function(cached) { - - var networked = fetch(event.request) - .then(fetchedFromNetwork, unableToResolve) - .catch(unableToResolve); - - console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url); - return cached || networked; - - function fetchedFromNetwork(response) { - var cacheCopy = response.clone(); - - console.log('WORKER: fetch response from network.', event.request.url); - - caches - .open(version + 'pages') - .then(function add(cache) { - cache.put(event.request, cacheCopy); - }) - .then(function() { - console.log('WORKER: fetch response stored in cache.', event.request.url); - }); - - return response; - } - - function unableToResolve () { - console.log('WORKER: fetch request failed in both cache and network.'); - - return new Response('

Service Unavailable

', { - status: 503, - statusText: 'Service Unavailable', - headers: new Headers({ - 'Content-Type': 'text/html' - }) - }); - } - }) - ); -}); - -self.addEventListener("activate", function(event) { - console.log('WORKER: activate event in progress.'); - - event.waitUntil( - caches - .keys() - .then(function (keys) { - - return Promise.all( - keys - .filter(function (key) { - return !key.startsWith(version); - }) - .map(function (key) { - return caches.delete(key); - }) - ); - }) - .then(function() { - console.log('WORKER: activate completed.'); - }) - ); -}); +toolbox.router.get('/(.*)', toolbox.cacheFirst, swOptions); From ef6d78bdcf16f7ab5af4a063d30730dcf78f8f92 Mon Sep 17 00:00:00 2001 From: Sven SAULEAU Date: Mon, 21 Nov 2016 21:31:15 +0100 Subject: [PATCH 03/19] fix(sw): removed pre-cached CSS --- service-worker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/service-worker.js b/service-worker.js index aa66e94cdb..2383fa39ec 100644 --- a/service-worker.js +++ b/service-worker.js @@ -8,7 +8,6 @@ var swOptions = { } var preCachedRessources = [ - '{{ "/css/main.css" | prepend: site.baseurl }}?t={{ site.time | date_to_xmlschema }}"', {% for page in site.pages %} '{{ page.url }}', {% endfor %} From 2d6207db6fdd6586ddf2a1246be110611b0436f6 Mon Sep 17 00:00:00 2001 From: Sven SAULEAU Date: Mon, 21 Nov 2016 21:32:02 +0100 Subject: [PATCH 04/19] feat(sw): add external cache origins --- service-worker.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service-worker.js b/service-worker.js index 2383fa39ec..4bd6ecb3b0 100644 --- a/service-worker.js +++ b/service-worker.js @@ -15,4 +15,8 @@ var preCachedRessources = [ toolbox.precache(preCachedRessources); -toolbox.router.get('/(.*)', toolbox.cacheFirst, swOptions); +toolbox.router.get('/*', toolbox.cacheFirst, swOptions); +toolbox.router.get('/*', toolbox.cacheFirst, { origin: "cdnjs.cloudflare.com" }); +toolbox.router.get('/*', toolbox.cacheFirst, { origin: "cdn.jsdelivr.net" }); +toolbox.router.get('/*', toolbox.cacheFirst, { origin: "unpkg.com" }); // for repl +toolbox.router.post('/*', toolbox.cacheFirst, { origin: "algolia.net" }); // Cache Algolia search response From 2936fad5dbd5f566233040974669dae784f836e9 Mon Sep 17 00:00:00 2001 From: Sven SAULEAU Date: Mon, 21 Nov 2016 21:37:52 +0100 Subject: [PATCH 05/19] feat(sw): add cache version --- service-worker.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/service-worker.js b/service-worker.js index 4bd6ecb3b0..23f0702c1c 100644 --- a/service-worker.js +++ b/service-worker.js @@ -3,9 +3,9 @@ importScripts('/scripts/sw-toolbox.js'); -var swOptions = { - debug: true -} +const VERSION = 1; + +toolbox.cache.name = "Babel-Cache-" + VERSION; var preCachedRessources = [ {% for page in site.pages %} @@ -15,7 +15,7 @@ var preCachedRessources = [ toolbox.precache(preCachedRessources); -toolbox.router.get('/*', toolbox.cacheFirst, swOptions); +toolbox.router.get('/*', toolbox.cacheFirst); toolbox.router.get('/*', toolbox.cacheFirst, { origin: "cdnjs.cloudflare.com" }); toolbox.router.get('/*', toolbox.cacheFirst, { origin: "cdn.jsdelivr.net" }); toolbox.router.get('/*', toolbox.cacheFirst, { origin: "unpkg.com" }); // for repl From 53458badb636ea81b1f2cf6ae4e914d963bfd7e3 Mon Sep 17 00:00:00 2001 From: Sven SAULEAU Date: Mon, 21 Nov 2016 22:38:50 +0100 Subject: [PATCH 06/19] feat(sw): offline indicator --- _includes/header.html | 5 +++++ _sass/_main.scss | 1 + _sass/components/_offline-indicator.scss | 4 ++++ scripts/index.js | 15 ++++++++++++++- 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 _sass/components/_offline-indicator.scss diff --git a/_includes/header.html b/_includes/header.html index 391bbeeb1b..f3cd451134 100644 --- a/_includes/header.html +++ b/_includes/header.html @@ -1,5 +1,10 @@