├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Procfile ├── README.md ├── _recipe_template ├── README.md ├── index.html ├── index.js └── service-worker.js ├── api-analytics ├── README.md ├── index.html ├── index.js ├── report.html ├── server.js └── service-worker.js ├── cache-from-zip ├── README.md ├── imgs │ ├── a.jpeg │ ├── b.jpeg │ └── c.jpeg ├── index.html ├── index.js ├── lib │ ├── ArrayBufferReader.js │ ├── deflate.js │ ├── inflate.js │ └── zip.js ├── package.zip └── worker.js ├── cache-then-network ├── README.md ├── index.html └── index.js ├── dependency-injector ├── README.md ├── actual-controller.js ├── actual-dialogs.js ├── bootstrap.js ├── default-mapping.js ├── index.html ├── injector.js ├── mock-dialogs.js ├── production-sw.js └── testing-sw.js ├── favicon.ico ├── fetching ├── README.md ├── index.html ├── index.js └── service-worker.js ├── gulpfile.js ├── imgs └── random │ ├── picture-1.png │ ├── picture-10.png │ ├── picture-11.png │ ├── picture-12.png │ ├── picture-13.png │ ├── picture-14.png │ ├── picture-15.png │ ├── picture-16.png │ ├── picture-17.png │ ├── picture-18.png │ ├── picture-19.png │ ├── picture-2.png │ ├── picture-20.png │ ├── picture-21.png │ ├── picture-22.png │ ├── picture-23.png │ ├── picture-24.png │ ├── picture-25.png │ ├── picture-26.png │ ├── picture-27.png │ ├── picture-28.png │ ├── picture-29.png │ ├── picture-3.png │ ├── picture-30.png │ ├── picture-31.png │ ├── picture-32.png │ ├── picture-33.png │ ├── picture-34.png │ ├── picture-35.png │ ├── picture-36.png │ ├── picture-37.png │ ├── picture-38.png │ ├── picture-39.png │ ├── picture-4.png │ ├── picture-40.png │ ├── picture-41.png │ ├── picture-42.png │ ├── picture-43.png │ ├── picture-44.png │ ├── picture-45.png │ ├── picture-46.png │ ├── picture-47.png │ ├── picture-48.png │ ├── picture-49.png │ ├── picture-5.png │ ├── picture-50.png │ ├── picture-6.png │ ├── picture-7.png │ ├── picture-8.png │ └── picture-9.png ├── immediate-claim ├── README.md ├── default.jpg ├── index.html ├── index.js ├── server.js └── service-worker.js ├── json-cache ├── README.md ├── files-to-cache.json ├── index.html ├── index.js ├── random-1.png ├── random-2.png ├── random-3.png ├── random-4.png ├── random-5.png ├── random-6.png └── service-worker.js ├── live-flowchart ├── README.md ├── active-service-worker-unregister.png ├── active-service-worker.png ├── app.js ├── index.html ├── logger.js ├── no-active-service-worker.png ├── register-unregister.png ├── register.png ├── security-error.png ├── service-worker-util.js ├── service-worker.js ├── style.css ├── sw-flowchart.png └── wrong-scriptURL.png ├── load-balancer ├── README.md ├── index.html ├── index.js ├── server-1 │ └── imgs │ │ ├── a.jpeg │ │ ├── b.jpeg │ │ └── c.jpeg ├── server-2 │ └── imgs │ │ ├── a.jpeg │ │ ├── b.jpeg │ │ └── c.jpeg ├── server-3 │ └── imgs │ │ ├── a.jpeg │ │ ├── b.jpeg │ │ └── c.jpeg ├── server.js └── service-worker.js ├── local-download ├── README.md ├── index.html ├── index.js └── service-worker.js ├── message-relay ├── README.md ├── index.html ├── index.js └── service-worker.js ├── offline-fallback ├── README.md ├── index.html ├── index.js ├── offline.html └── service-worker.js ├── offline-status ├── README.md ├── app.js ├── index.html ├── index.js ├── random-1.png ├── random-2.png ├── random-3.png ├── random-4.png ├── random-5.png ├── random-6.png ├── service-worker.js └── style.css ├── package.json ├── parseRecipes.js ├── push-clients ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-get-payload ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-payload ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-quota ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-replace ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-rich ├── README.md ├── caesar.jpg ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-simple ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-subscription-management ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── render-store ├── README.md ├── index.html ├── index.js ├── pokemon.html ├── pokemon.js └── service-worker.js ├── request-deferrer ├── README.md ├── index.html ├── index.js ├── lib │ ├── ServiceWorkerWare.js │ └── localforage.js ├── server.js └── service-worker.js ├── server.js ├── src ├── css │ ├── docco.css │ ├── foundation-icons.css │ ├── foundation.css │ ├── foundation.normalize.css │ └── style.css ├── js │ └── layout.js └── tpl │ ├── category.html │ ├── demo.html │ ├── docco │ └── docco.jst │ ├── index.html │ ├── intro.html │ └── layout.html ├── strategy-cache-and-update ├── README.md ├── controlled.html ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-cache-only ├── README.md ├── controlled.html ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-cache-update-and-refresh ├── README.md ├── controlled.html ├── controlled.js ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-embedded-fallback ├── README.md ├── controlled.html ├── controlled.js ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-network-or-cache ├── README.md ├── controlled.html ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── tools.js └── virtual-server ├── README.md ├── index.html ├── index.js ├── lib └── ServiceWorkerWare.js └── service-worker.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/legacy", 3 | "rules": { 4 | "no-console": 0, 5 | "comma-dangle": 0, 6 | "func-names": 0, 7 | "vars-on-top": 0, 8 | "no-use-before-define": 0, 9 | "space-before-function-paren": 0, 10 | "max-len": [1, 80, 4, {"ignoreComments": true, "ignoreUrls": true}], 11 | "no-param-reassign": 0, 12 | "quote-props": 0, 13 | "wrap-iife": [2, "inside"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 10 3 | language: node_js 4 | node_js: 5 | - '6' 6 | deploy: 7 | provider: heroku 8 | api_key: 9 | secure: Vw45m2w7wedVrQdoMqdy3MQc5YpTguqX6ZtRo+4miJ9AqdWAcMO/S5pCosv0AfjvGzvpv55FQ1kWWorzX1GHEjm86NoatygtGJF3BH26WZx6fldgVwnyG+pC3+PcJ/PYs/gfIl2rOAfYTjlnWbWieGHIlztDEtEoV7xsOYHwp5/HhpWsvdACXcIr3trCuNkBo10VmjpjFv8Anms4ULqguMN12pTA7asD9Y+JIQCLUzQ5WqfTYDC+IMoYPDkciSQmN5KfBPFfEVeUCWXGI5aTeCrs/M9LPOlsT8UaWaoC1FN1Ebl9OMsXeqGAbJvQvme4HbhyekcfmHGlfdPA/lpaJ3LbRhBiidsA0Pw5FzlEwTRmGDeIbEB6eM5jH30tKWKiRfwyozbOI3ga/ozLYR29Kw6ER2sUGAho6npHt1LAB5n1+BNbyuNT4M/dkZp3n/X7wlCU+jzxrDAOnpEaQbi6XnxST7KYLjvs4NgTFq8xf8vGM1Bd4yfs9h4UfAM0k9Ez/XoK2Za5jYU9nZyfeSw76VERphUgxLK+kwdoifqMz/avQ4pl/hIVCvKAYY/9jLqQcJTuUiei4h28lEqKRMBgTItVJLvQu8xsdKfAdSxFfu4J95/DtjiOfHDz15MJALNCfyLFcFOCzUMN9Fi7X4ZZBlAkksYiUDXEEmoKsPfC37I= 10 | app: serviceworker-cookbook 11 | skip_cleanup: true 12 | on: 13 | repo: mozilla/serviceworker-cookbook 14 | branch: master 15 | node: '6' 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Harald Kirschner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /_recipe_template/README.md: -------------------------------------------------------------------------------- 1 | # (Title) 2 | (Summary) 3 | 4 | ## Difficulty 5 | (Beginner|Intermediate|Advanced) 6 | 7 | ## Use Case 8 | (Summary) 9 | 10 | ## Features and Usage 11 | 12 | - 13 | - 14 | - 15 | 16 | ## Compatibility 17 | Tests have been run in: 18 | 19 | - 20 | - 21 | - 22 | 23 | ## Solution 24 | (Add details here) 25 | 26 | ## Category 27 | (General Usage|Beyond Offline|Performance|Offline|Web Push|Caching strategies) 28 | -------------------------------------------------------------------------------- /_recipe_template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |This is a report about how many times certain operations have been performed on specific URLs
17 |url | 20 |GET | 21 |POST | 22 |DELETE | 23 |
---|---|---|---|
{{ entry.url }} | 27 |{{ entry.GET }} | 28 |{{ entry.POST }} | 29 |{{ entry.DELETE }} | 30 |
The demo shows Service Workers as dependency injectors. Depending on your selection, 13 | a production or testing Service Worker will control this page and will serve proper 14 | versions of the scripts needed to run the interaction section below.
15 |Select an environment:
16 |17 | Switch to production (dialogs will appear using the browser UI) 18 | Switch to testing (dialogs will leave a trace in the console log) 19 |
20 |What is cool about this demo is that you can see different content for the same resources 21 | if you explore the source files (Chrome only, Nightly fails to load the resource because it 22 | actually does not exists) with the developer tools.
23 |25 | 26 | 27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /dependency-injector/injector.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | /* global mapping */ 3 | 4 | // This script must to be added via `importScripts()` to the 5 | // service workers. See `testing-sw.js` and `production-sw.js` 6 | // first. 7 | 8 | // Make the SW control the client ASAP. 9 | function onInstall(event) { 10 | event.waitUntil(self.skipWaiting()); 11 | } 12 | 13 | function onActivate(event) { 14 | event.waitUntil(self.clients.claim()); 15 | } 16 | 17 | // Easy, simply try to find an actual resource URL for an abstract request. 18 | // If not found, let's fetch the abstract resource. In this demo, this never 19 | // fails. 20 | function onFetch(event) { 21 | var abstractResource = event.request.url; 22 | var actualResource = findActualResource(abstractResource); 23 | event.respondWith(fetch(actualResource || abstractResource)); 24 | } 25 | 26 | // Look inside mapping to get the actual resource URL for the abstract resource URL 27 | // passed as parameter. This act like the dependency factory in charge of creating 28 | // the objects to be injected. 29 | function findActualResource(abstractResource) { 30 | var actualResource; 31 | var patterns = Object.keys(mapping); 32 | for (var index = 0; index < patterns.length; index++) { 33 | var pattern = patterns[index]; 34 | // A really silly matcher just for learning purposes. 35 | if (abstractResource.endsWith(pattern)) { 36 | actualResource = mapping[pattern]; 37 | break; 38 | } 39 | } 40 | // Can return undefined if there is no actual resource. 41 | return actualResource; 42 | } 43 | -------------------------------------------------------------------------------- /dependency-injector/mock-dialogs.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | // The module mock dialogs exports a non blocking fake implementation 3 | // that simply logs the calls. 4 | window.dialogs = { 5 | alert: function(msg) { 6 | console.log('alert:', msg); 7 | }, 8 | 9 | confirm: function(msg) { 10 | console.log('confirm:', msg); 11 | return true; 12 | }, 13 | 14 | prompt: function(msg) { 15 | console.log('prompt:', msg); 16 | return 'test'; 17 | } 18 | }; 19 | })(window); 20 | -------------------------------------------------------------------------------- /dependency-injector/production-sw.js: -------------------------------------------------------------------------------- 1 | /* global importScripts, onFetch, onInstall, onActivate, mapping */ 2 | 3 | importScripts('injector.js'); 4 | 5 | // The default mapping contains the map for abstract resources to their 6 | // concrete counterparts. 7 | importScripts('default-mapping.js'); 8 | 9 | // All the functionality is in `injector.js` here we only wire the listeners. 10 | self.onfetch = onFetch; 11 | self.oninstall = onInstall; 12 | self.onactivate = onActivate; 13 | -------------------------------------------------------------------------------- /dependency-injector/testing-sw.js: -------------------------------------------------------------------------------- 1 | /* global importScripts, onFetch, onInstall, onActivate, mapping */ 2 | 3 | importScripts('injector.js'); 4 | 5 | // Load the default mapping between abstract and concrete resources. 6 | importScripts('default-mapping.js'); 7 | 8 | // But override ``utils/dialogs`` to serve the mockup instead. 9 | mapping['utils/dialogs'] = 'mock-dialogs.js'; 10 | 11 | self.onfetch = onFetch; 12 | self.oninstall = onInstall; 13 | self.onactivate = onActivate; 14 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/favicon.ico -------------------------------------------------------------------------------- /fetching/README.md: -------------------------------------------------------------------------------- 1 | # Fetching Remote Resources 2 | 3 | This recipe shows 2 standard ways of loading a remote resource and one way to use service worker as a proxy middleware. 4 | 5 | There are used 3 types of remote resources - unsecure (http), secured with `allow-origin` header, and secured without the header. 6 | 7 | ## DOM Element 8 | DOM elements are loading the resources 9 | 10 | ## Fetch 11 | Fetch issues a cors or no-cors request to each resource 12 | 13 | ## Fetch with Service Worker Proxy 14 | Fetch on the client loads a local resource `./cookbook-proxy/{full URL}` which is then translated to real URL in the service worker and forwarded to the client. 15 | 16 | ## Difficulty 17 | Intermediate 18 | 19 | ## Category 20 | General Usage 21 | -------------------------------------------------------------------------------- /fetching/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This demo uses a service worker which loads a JSON file representing files to be cached by the Service Worker. Once the JSON file is loaded and parsed, files are placed into the cache via the Service Worker.
23 | 24 |debug
style
11 | debug: function debug(message) {
12 | this.writeLog(message, { debug: true });
13 | },
14 |
15 | // info
style
17 | info: function info(message) {
18 | this.writeLog(message, { info: true });
19 | },
20 |
21 | // log
style
23 | log: function log(message) {
24 | this.writeLog(message);
25 | },
26 |
27 | // warning
style
29 | warn: function warn(message) {
30 | this.writeLog(message, { warn: true });
31 | },
32 |
33 | // error
style
35 | error: function error(message) {
36 | this.writeLog(message, { error: true });
37 | },
38 |
39 | // 11 | Choose from one of the images to be loaded: 12 |
13 |14 | Notice the white label in the image which is telling you the server it comes from. 15 |
16 |17 | 23 |
24 |Configure the load of the content providers.
27 |Servers set to:
28 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /load-balancer/index.js: -------------------------------------------------------------------------------- 1 | // A convenient shortcut for `document.querySelector()` 2 | var $ = document.querySelector.bind(document); // eslint-disable-line id-length 3 | 4 | var serverLoadInputs = [ 5 | $('#load-1'), 6 | $('#load-2'), 7 | $('#load-3') 8 | ]; 9 | 10 | // Register the worker and enable image selection. 11 | navigator.serviceWorker.register('service-worker.js'); 12 | navigator.serviceWorker.ready.then(enableUI); 13 | 14 | function enableUI() { 15 | getServerLoads().then(function(loads) { 16 | serverLoadInputs.forEach(function(input, index) { 17 | input.value = loads[index]; 18 | input.disabled = false; 19 | }); 20 | $('#image-selector').disabled = false; 21 | }); 22 | } 23 | 24 | function getServerLoads() { 25 | return fetch(addSession('./server-loads/')).then(function(response) { 26 | return response.json(); 27 | }); 28 | } 29 | 30 | // When _clicking_ configure button, send the load values to the back-end to 31 | // simulate server loads. 32 | $('#load-configuration').onsubmit = function(event) { 33 | // Avoid navigation 34 | event.preventDefault(); 35 | 36 | // Get fake levels from inputs. 37 | var loads = serverLoadInputs.map(function(input) { 38 | return parseInt(input.value, 10); 39 | }); 40 | 41 | // Send the request to configure the load levels serializing the body 42 | // and setting the content type header properly. 43 | fetch(addSession('./server-loads'), { 44 | method: 'PUT', 45 | headers: { 'Content-Type': 'application/json' }, 46 | body: JSON.stringify(loads) 47 | }).then(function(response) { 48 | return response.json(); 49 | }).then(function(result) { 50 | $('#loads-label').textContent = result; 51 | }); 52 | }; 53 | 54 | // Simply change the source for the image. 55 | $('#image-selector').onchange = function() { 56 | var imgUrl = $('select').value; 57 | if (imgUrl) { 58 | // The bumping parameter `_b` is just to avoid HTTP cache. 59 | $('img').src = addSession(imgUrl) + '&_b=' + Date.now(); 60 | 61 | // Specifically for the cookbook :( 62 | $('img').onload = function() { 63 | if (window.parent !== window) { 64 | window.parent 65 | .document.body.dispatchEvent(new CustomEvent('iframeresize')); 66 | } 67 | }; 68 | } 69 | }; 70 | 71 | // Add the session parameter to an URL. 72 | function addSession(url) { 73 | return url + '?session=' + getSession(); 74 | } 75 | 76 | // A simple session manager based on a random string stored in the localStorage 77 | function getSession() { 78 | var session = localStorage.getItem('session'); 79 | if (!session) { 80 | session = '' + Date.now() + '-' + Math.random(); 81 | localStorage.setItem('session', session); 82 | } 83 | return session; 84 | } 85 | -------------------------------------------------------------------------------- /load-balancer/server-1/imgs/a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-1/imgs/a.jpeg -------------------------------------------------------------------------------- /load-balancer/server-1/imgs/b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-1/imgs/b.jpeg -------------------------------------------------------------------------------- /load-balancer/server-1/imgs/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-1/imgs/c.jpeg -------------------------------------------------------------------------------- /load-balancer/server-2/imgs/a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-2/imgs/a.jpeg -------------------------------------------------------------------------------- /load-balancer/server-2/imgs/b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-2/imgs/b.jpeg -------------------------------------------------------------------------------- /load-balancer/server-2/imgs/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-2/imgs/c.jpeg -------------------------------------------------------------------------------- /load-balancer/server-3/imgs/a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-3/imgs/a.jpeg -------------------------------------------------------------------------------- /load-balancer/server-3/imgs/b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-3/imgs/b.jpeg -------------------------------------------------------------------------------- /load-balancer/server-3/imgs/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-3/imgs/c.jpeg -------------------------------------------------------------------------------- /load-balancer/server.js: -------------------------------------------------------------------------------- 1 | 2 | var bodyParser = require('body-parser'); 3 | 4 | // Simple session handling with a hash of sessions. 5 | var sessions = {}; 6 | 7 | // A simple API to simulate load in simulated content provided servers. 8 | module.exports = function(app, route) { 9 | // Allow express to parse the body of the requests. 10 | app.use(bodyParser.json()); 11 | 12 | // Configure servers loads. 13 | app.put(route + 'server-loads', function(req, res) { 14 | var loads = req.body; 15 | sessions[req.query.session] = loads; 16 | res.status(201).json(loads); 17 | }); 18 | 19 | // Query servers loads. 20 | app.get(route + 'server-loads', function(req, res) { 21 | var loads = sessions[req.query.session] || [50, 75, 25]; 22 | res.json(loads); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /load-balancer/service-worker.js: -------------------------------------------------------------------------------- 1 | // The code in `oninstall` and `onactivate` force the service worker to 2 | // control the clients ASAP. 3 | self.oninstall = function(event) { 4 | event.waitUntil(self.skipWaiting()); 5 | }; 6 | 7 | self.onactivate = function(event) { 8 | event.waitUntil(self.clients.claim()); 9 | }; 10 | 11 | // When fetching, distinguish if this is a resource fetch. If so, 12 | // apply the server selection algorithm. Else, let the request reach the 13 | // network. Could should be autoexplanatory. 14 | self.onfetch = function(event) { 15 | var request = event.request; 16 | if (isResource(request)) { 17 | event.respondWith(fetchFromBestServer(request)); 18 | } else { 19 | event.respondWith(fetch(request)); 20 | } 21 | }; 22 | 23 | // A request is a resource request if it is a `GET` for something inside `imgs`. 24 | function isResource(request) { 25 | return request.url.match(/\/imgs\/.*$/) && request.method === 'GET'; 26 | } 27 | 28 | // Fetching from the best server consists of getting the server loads, 29 | // selecting the server with lowest load, and compose a new request to 30 | // find the resource in the selected server. 31 | function fetchFromBestServer(request) { 32 | var session = request.url.match(/\?session=([^&]*)/)[1]; 33 | return getServerLoads(session) 34 | .then(selectServer) 35 | .then(function(serverUrl) { 36 | // Get the resource path and combine with `serverUrl` to get 37 | // the resource URL but **in the selected server**. 38 | var resourcePath = request.url.match(/\/imgs\/[^?]*/)[0]; 39 | var serverRequest = new Request(serverUrl + resourcePath); 40 | return fetch(serverRequest); 41 | }); 42 | } 43 | 44 | // Query the back-end for servers loads. 45 | function getServerLoads(session) { 46 | return fetch('./server-loads?session=' + session).then(function(response) { 47 | return response.json(); 48 | }); 49 | } 50 | 51 | // Get the server with minimum load and return its URL. In a real 52 | // scenario this could return servers in other domains, just remember 53 | // to set the CORS headers properly. 54 | function selectServer(serverLoads) { 55 | // Not very efficient but super-clear way of finding the index of the server 56 | // with minimum load. 57 | var min = Math.min.apply(Math, serverLoads); 58 | var serverIndex = serverLoads.indexOf(min); 59 | 60 | // Servers are 1, 2, 3... 61 | return './server-' + (serverIndex + 1); 62 | } 63 | -------------------------------------------------------------------------------- /local-download/README.md: -------------------------------------------------------------------------------- 1 | # Local Download 2 | 3 | Allow a user to "download" a file that's been generated on the client side. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | Often it will be necessary to include a download feature in a single page application - for example, a drawing program might want the ability to export as SVG, or a bitmap format generated client side. 10 | 11 | ## Solution 12 | Using the serviceworker to intercept a form POST operation, pull the data from the body of the request. The data can then be put in a request that behaves as a downloadable attachment, which is fed back to the client as a file. The file will appear to have been downloaded, without any round trips to the server necessary. 13 | 14 | ## Category 15 | Beyond Offline 16 | -------------------------------------------------------------------------------- /local-download/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Check your internet connection and refresh.
17 | 18 | 19 | -------------------------------------------------------------------------------- /offline-fallback/service-worker.js: -------------------------------------------------------------------------------- 1 | // [Working example](/serviceworker-cookbook/offline-fallback/). 2 | 3 | self.addEventListener('install', function(event) { 4 | // Put `offline.html` page into cache 5 | var offlineRequest = new Request('offline.html'); 6 | event.waitUntil( 7 | fetch(offlineRequest).then(function(response) { 8 | return caches.open('offline').then(function(cache) { 9 | console.log('[oninstall] Cached offline page', response.url); 10 | return cache.put(offlineRequest, response); 11 | }); 12 | }) 13 | ); 14 | }); 15 | 16 | self.addEventListener('fetch', function(event) { 17 | // Only fall back for HTML documents. 18 | var request = event.request; 19 | // && request.headers.get('accept').includes('text/html') 20 | if (request.method === 'GET') { 21 | // `fetch()` will use the cache when possible, to this examples 22 | // depends on cache-busting URL parameter to avoid the cache. 23 | event.respondWith( 24 | fetch(request).catch(function(error) { 25 | // `fetch()` throws an exception when the server is unreachable but not 26 | // for valid HTTP responses, even `4xx` or `5xx` range. 27 | console.error( 28 | '[onfetch] Failed. Serving cached offline fallback ' + 29 | error 30 | ); 31 | return caches.open('offline').then(function(cache) { 32 | return cache.match('offline.html'); 33 | }); 34 | }) 35 | ); 36 | } 37 | // Any other handlers come here. Without calls to `event.respondWith()` the 38 | // request will be handled without the ServiceWorker. 39 | }); 40 | -------------------------------------------------------------------------------- /offline-status/README.md: -------------------------------------------------------------------------------- 1 | # Offline Status 2 | 3 | This basic recipe illustrates caching critical resources for offline use and then notifying the user that they may go offline and enjoy the same experience. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | The most basic of service worker use cases: caching a set of files so that the user may go offline. The added value in this demo is showing a notification to the user that they can safely go offline. 10 | 11 | ## Features and Usage 12 | 13 | - Register a service worker 14 | - Monitor the cached status of required resources 15 | - Notification to user when resources have been cached 16 | 17 | The only action required is loading the page initially. After initial load, the service worker has installed and the assets have been cached. 18 | 19 | ## Compatibility 20 | 21 | Tests have been run in: 22 | 23 | - Firefox Nightly 44.0a1 24 | - Chrome Canary 48.0.2533.0 25 | - Opera 32.0 26 | 27 | ## Category 28 | Offline 29 | -------------------------------------------------------------------------------- /offline-status/app.js: -------------------------------------------------------------------------------- 1 | // This file is required to make the "app" work offline 2 | 3 | document.getElementById('randomButton').addEventListener('click', function() { 4 | var image = document.getElementById('logoImage'); 5 | var currentIndex = Number(image.src.match('random-([0-9])')[1]); 6 | var newIndex = getRandomNumber(); 7 | 8 | // Ensure that we receive a different image than the current 9 | while (newIndex === currentIndex) { 10 | newIndex = getRandomNumber(); 11 | } 12 | 13 | image.src = 'random-' + newIndex + '.png'; 14 | 15 | function getRandomNumber() { 16 | return Math.floor(Math.random() * 6) + 1; 17 | } 18 | }); 19 | 20 | document.getElementById('clearAndReRegister').addEventListener('click', 21 | function() { 22 | navigator.serviceWorker.getRegistration().then(function(registration) { 23 | registration.unregister(); 24 | window.location.reload(); 25 | }); 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /offline-status/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |The goal of this recipe is to create an app which can be used both online and offline with the help of a ServiceWorker. Once the ServiceWorker has cached assets and becomes activated, the user will receive a notification that they can then go offline and use the app!
19 | 20 |This demo shows how to control the clients of a service worker when the user clicks on a push notification.
17 | 18 |Press on 'Send notification' and try one of:
19 |This demo shows how to send push notifications and retrieve a payload when the notification is received.
17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /push-get-payload/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const payload = document.getElementById('notification-payload').value; 42 | const delay = document.getElementById('notification-delay').value; 43 | const ttl = document.getElementById('notification-ttl').value; 44 | 45 | // Ask the server to send the client a notification (for testing purposes, in actual 46 | // applications the push notification is likely going to be generated by some event 47 | // in the server). 48 | fetch('./sendNotification', { 49 | method: 'post', 50 | headers: { 51 | 'Content-type': 'application/json' 52 | }, 53 | body: JSON.stringify({ 54 | subscription: subscription, 55 | payload: payload, 56 | delay: delay, 57 | ttl: ttl, 58 | }), 59 | }); 60 | }; 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /push-get-payload/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | const payloads = {}; 23 | 24 | module.exports = function (app, route) { 25 | app.get(route + "vapidPublicKey", function (req, res) { 26 | res.send(process.env.VAPID_PUBLIC_KEY); 27 | }); 28 | 29 | app.post(route + "register", function (req, res) { 30 | // A real world application would store the subscription info. 31 | res.sendStatus(201); 32 | }); 33 | 34 | app.post(route + "sendNotification", function (req, res) { 35 | const subscription = req.body.subscription; 36 | const payload = req.body.payload; 37 | const options = { 38 | TTL: req.body.ttl, 39 | }; 40 | 41 | setTimeout(function () { 42 | payloads[req.body.subscription.endpoint] = payload; 43 | webPush 44 | .sendNotification(subscription, null, options) 45 | .then(function () { 46 | res.sendStatus(201); 47 | }) 48 | .catch(function (error) { 49 | res.sendStatus(500); 50 | console.log(error); 51 | }); 52 | }, req.body.delay * 1000); 53 | }); 54 | 55 | app.get(route + "getPayload", function (req, res) { 56 | res.send(payloads[req.query.endpoint]); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /push-get-payload/service-worker.js: -------------------------------------------------------------------------------- 1 | function getEndpoint() { 2 | return self.registration.pushManager.getSubscription() 3 | .then(function(subscription) { 4 | if (subscription) { 5 | return subscription.endpoint; 6 | } 7 | 8 | throw new Error('User not subscribed'); 9 | }); 10 | } 11 | 12 | // Register event listener for the 'push' event. 13 | self.addEventListener('push', function(event) { 14 | // Keep the service worker alive until the notification is created. 15 | event.waitUntil( 16 | getEndpoint() 17 | .then(function(endpoint) { 18 | // Retrieve the textual payload from the server using a GET request. 19 | // We are using the endpoint as an unique ID of the user for simplicity. 20 | return fetch('./getPayload?endpoint=' + endpoint); 21 | }) 22 | .then(function(response) { 23 | return response.text(); 24 | }) 25 | .then(function(payload) { 26 | // Show a notification with title 'ServiceWorker Cookbook' and use the payload 27 | // as the body. 28 | self.registration.showNotification('ServiceWorker Cookbook', { 29 | body: payload, 30 | }); 31 | }) 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /push-payload/README.md: -------------------------------------------------------------------------------- 1 | # Push Payload 2 | 3 | Send push notifications with a payload. This recipe shows how to send and receive a string, but data can be extracted from a Push message in a variety of formats (string, ArrayBuffer, Blob, JSON). 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | A message does not have to deliver just text, but can deliver various kinds of payloads of data to an application. This demonstrates how you can deliver a rich payload to your app. 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-payload/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This demo shows how to send push notifications with a payload.
17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /push-payload/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const payload = document.getElementById('notification-payload').value; 42 | const delay = document.getElementById('notification-delay').value; 43 | const ttl = document.getElementById('notification-ttl').value; 44 | 45 | // Ask the server to send the client a notification (for testing purposes, in actual 46 | // applications the push notification is likely going to be generated by some event 47 | // in the server). 48 | fetch('./sendNotification', { 49 | method: 'post', 50 | headers: { 51 | 'Content-type': 'application/json' 52 | }, 53 | body: JSON.stringify({ 54 | subscription: subscription, 55 | payload: payload, 56 | delay: delay, 57 | ttl: ttl, 58 | }), 59 | }); 60 | }; 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /push-payload/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = req.body.payload; 35 | const options = { 36 | TTL: req.body.ttl, 37 | }; 38 | 39 | setTimeout(function () { 40 | webPush 41 | .sendNotification(subscription, payload, options) 42 | .then(function () { 43 | res.sendStatus(201); 44 | }) 45 | .catch(function (error) { 46 | console.log(error); 47 | res.sendStatus(500); 48 | }); 49 | }, req.body.delay * 1000); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /push-payload/service-worker.js: -------------------------------------------------------------------------------- 1 | // Register event listener for the 'push' event. 2 | self.addEventListener('push', function(event) { 3 | // Retrieve the textual payload from event.data (a PushMessageData object). 4 | // Other formats are supported (ArrayBuffer, Blob, JSON), check out the documentation 5 | // on https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData. 6 | const payload = event.data ? event.data.text() : 'no payload'; 7 | 8 | // Keep the service worker alive until the notification is created. 9 | event.waitUntil( 10 | // Show a notification with title 'ServiceWorker Cookbook' and use the payload 11 | // as the body. 12 | self.registration.showNotification('ServiceWorker Cookbook', { 13 | body: payload, 14 | }) 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /push-quota/README.md: -------------------------------------------------------------------------------- 1 | # Push Quota 2 | 3 | Experiment with the quota management policies of different browsers. Try sending many notifications (visible or invisible) and see what happens if you keep the tab open vs close it, or if you click on some notifications vs you click on none of them. 4 | 5 | ## Difficulty 6 | Advanced 7 | 8 | ## Use Cases 9 | This code allows you to experiment with different push message quota policies with different browsers so you can see how they behave for different actions by the users. 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-quota/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This demo allows you to experiment with the quota management rules enforced by browsers. 21 | 22 | The browser will likely reduce the quota assigned to you if you send too many notifications not visible to the user, or if the user dismisses your visible notifications. 23 | Try closing this page after selecting one of the two options ("visible" or "invisible") with a high number of notifications.
24 | 25 |Received 0 visible notifications 37 | Received 0 invisible notifications
38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /push-quota/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | // Send N notifications, specifying whether the service worker will need to show 33 | // a visible notification or not using the push payload: 34 | // - 'true': show a notification; 35 | // - 'false': don't show a notification. 36 | app.post(route + "sendNotification", function (req, res) { 37 | const subscription = req.body.subscription; 38 | const payload = JSON.stringify(req.body.visible); 39 | const options = { 40 | TTL: 200, 41 | }; 42 | 43 | let num = 1; 44 | 45 | let promises = []; 46 | 47 | let intervalID = setInterval(function () { 48 | promises.push(webPush.sendNotification(subscription, payload, options)); 49 | 50 | if (num++ === Number(req.body.num)) { 51 | clearInterval(intervalID); 52 | 53 | Promise.all(promises) 54 | .then(function () { 55 | res.sendStatus(201); 56 | }) 57 | .catch(function (error) { 58 | res.sendStatus(500); 59 | console.log(error); 60 | }); 61 | } 62 | }, 1000); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /push-quota/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE_NAME = 'notifications'; 2 | 3 | // On install, create the 'notifications' cache that will be used to store the Number 4 | // of notifications received. 5 | self.addEventListener('install', function(event) { 6 | event.waitUntil( 7 | caches.open(CACHE_NAME).then(function(cache) { 8 | return Promise.all([ 9 | // We create a fake request and response to store the info. 10 | cache.put(new Request('invisible'), new Response('0', { 11 | headers: { 12 | 'content-type': 'application/json' 13 | } 14 | })), 15 | cache.put(new Request('visible'), new Response('0', { 16 | headers: { 17 | 'content-type': 'application/json' 18 | } 19 | })), 20 | ]); 21 | }) 22 | ); 23 | }); 24 | 25 | self.addEventListener('activate', function(event) { 26 | event.waitUntil(self.clients.claim); 27 | }); 28 | 29 | function updateNumber(type) { 30 | // Update the number of notifications received of type 'type' (visible or invisible). 31 | return caches.open(CACHE_NAME).then(function(cache) { 32 | return cache.match(type).then(function(response) { 33 | return response.json().then(function(notificationNum) { 34 | var newNotificationNum = notificationNum + 1; 35 | 36 | return cache.put( 37 | new Request(type), 38 | new Response(JSON.stringify(newNotificationNum), { 39 | headers: { 40 | 'content-type': 'application/json', 41 | }, 42 | }) 43 | ).then(function() { 44 | return newNotificationNum; 45 | }); 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | // Register event listener for the 'push' event. 52 | self.addEventListener('push', function(event) { 53 | // Retrieve the payload from event.data (a PushMessageData object) as a JSON object. 54 | var visible = event.data ? event.data.json() : false; 55 | 56 | // Keep the service worker alive until the 'notifications' cache is updated and, 57 | // if visible is true, the notification is created. 58 | 59 | if (visible) { 60 | event.waitUntil(updateNumber('visible').then(function(num) { 61 | return self.registration.showNotification('ServiceWorker Cookbook', { 62 | body: 'Received ' + num + ' visible notifications', 63 | }); 64 | })); 65 | } else { 66 | event.waitUntil(updateNumber('invisible')); 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /push-replace/README.md: -------------------------------------------------------------------------------- 1 | # Push Tag 2 | 3 | Use the notification tag to replace old notifications with new ones. Allows you to show only up-to-date information to your users or collapse multiple notifications into a single one. 4 | 5 | ## Difficulty 6 | Intermediate 7 | 8 | ## Use Cases 9 | This code shows how to manage a queue of notifications so that previous notifications can be discarded or merged into a single notification. 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-replace/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This demo shows how to replace an old notification with a new one.
17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /push-replace/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const delay = document.getElementById('notification-delay').value; 42 | 43 | // Ask the server to send the client a notification (for testing purposes, in actual 44 | // applications the push notification is likely going to be generated by some event 45 | // in the server). 46 | fetch('./sendNotification', { 47 | method: 'post', 48 | headers: { 49 | 'Content-type': 'application/json' 50 | }, 51 | body: JSON.stringify({ 52 | subscription: subscription, 53 | delay: delay, 54 | }), 55 | }); 56 | }; 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /push-replace/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = null; 35 | const options = { 36 | TTL: 200, 37 | }; 38 | 39 | webPush.sendNotification(subscription, payload, options).catch(logError); 40 | 41 | setTimeout(function () { 42 | webPush 43 | .sendNotification(subscription, payload, options) 44 | .then(function () { 45 | res.sendStatus(201); 46 | }) 47 | .catch(logError); 48 | }, req.body.delay * 1000); 49 | 50 | function logError(error) { 51 | res.sendStatus(500); 52 | console.log(error); 53 | } 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /push-replace/service-worker.js: -------------------------------------------------------------------------------- 1 | let num = 1; 2 | 3 | // Register event listener for the 'push' event. 4 | self.addEventListener('push', function(event) { 5 | // Keep the service worker alive until the notification is created. 6 | event.waitUntil( 7 | // Show a notification with title 'ServiceWorker Cookbook' and body containing 8 | // a number that keeps increasing for each received notification. 9 | // The tag field allows replacing an old notification with a new one (a notification 10 | // with the same tag of another one will replace it). 11 | self.registration.showNotification('ServiceWorker Cookbook', { 12 | body: 'Notification ' + num++, 13 | tag: 'swc', 14 | }) 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /push-rich/README.md: -------------------------------------------------------------------------------- 1 | # Push Rich 2 | 3 | Show rich push notifications, defining the language of the notification, a vibration pattern, an image to associate to the notification. See https://notifications.spec.whatwg.org/#api for the other parameters you can set (e.g. a set of actions that can be activated from the notification). 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Category 9 | Web Push 10 | -------------------------------------------------------------------------------- /push-rich/caesar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/push-rich/caesar.jpg -------------------------------------------------------------------------------- /push-rich/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This demo shows how to show rich push notifications.
17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /push-rich/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const delay = document.getElementById('notification-delay').value; 42 | const ttl = document.getElementById('notification-ttl').value; 43 | 44 | // Ask the server to send the client a notification (for testing purposes, in actual 45 | // applications the push notification is likely going to be generated by some event 46 | // in the server). 47 | fetch('./sendNotification', { 48 | method: 'post', 49 | headers: { 50 | 'Content-type': 'application/json' 51 | }, 52 | body: JSON.stringify({ 53 | subscription: subscription, 54 | delay: delay, 55 | ttl: ttl, 56 | }), 57 | }); 58 | }; 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /push-rich/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = null; 35 | const options = { 36 | TTL: req.body.ttl, 37 | }; 38 | 39 | setTimeout(function () { 40 | webPush 41 | .sendNotification(subscription, payload, options) 42 | .then(function () { 43 | res.sendStatus(201); 44 | }) 45 | .catch(function (error) { 46 | console.log(error); 47 | res.sendStatus(500); 48 | }); 49 | }, req.body.delay * 1000); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /push-rich/service-worker.js: -------------------------------------------------------------------------------- 1 | // Register event listener for the 'push' event. 2 | self.addEventListener('push', function(event) { 3 | // Keep the service worker alive until the notification is created. 4 | event.waitUntil( 5 | // Show a notification with title 'ServiceWorker Cookbook' and body 'Alea iacta est'. 6 | // Set other parameters such as the notification language, a vibration pattern associated 7 | // to the notification, an image to show near the body. 8 | // There are many other possible options, for an exhaustive list see the specs: 9 | // https://notifications.spec.whatwg.org/ 10 | self.registration.showNotification('ServiceWorker Cookbook', { 11 | lang: 'la', 12 | body: 'Alea iacta est', 13 | icon: 'caesar.jpg', 14 | vibrate: [500, 100, 500], 15 | }) 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /push-simple/README.md: -------------------------------------------------------------------------------- 1 | # Push Simple 2 | 3 | Simplest example of Web Push API usage. Send notifications to users even when your page is not open. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Category 9 | Web Push 10 | -------------------------------------------------------------------------------- /push-simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This demo shows how to register for push notifications and how to send them.
17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /push-simple/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const delay = document.getElementById('notification-delay').value; 42 | const ttl = document.getElementById('notification-ttl').value; 43 | 44 | // Ask the server to send the client a notification (for testing purposes, in actual 45 | // applications the push notification is likely going to be generated by some event 46 | // in the server). 47 | fetch('./sendNotification', { 48 | method: 'post', 49 | headers: { 50 | 'Content-type': 'application/json' 51 | }, 52 | body: JSON.stringify({ 53 | subscription: subscription, 54 | delay: delay, 55 | ttl: ttl, 56 | }), 57 | }); 58 | }; 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /push-simple/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = null; 35 | const options = { 36 | TTL: req.body.ttl, 37 | }; 38 | 39 | setTimeout(function () { 40 | webPush 41 | .sendNotification(subscription, payload, options) 42 | .then(function () { 43 | res.sendStatus(201); 44 | }) 45 | .catch(function (error) { 46 | res.sendStatus(500); 47 | console.log(error); 48 | }); 49 | }, req.body.delay * 1000); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /push-simple/service-worker.js: -------------------------------------------------------------------------------- 1 | // Register event listener for the 'push' event. 2 | self.addEventListener('push', function(event) { 3 | // Keep the service worker alive until the notification is created. 4 | event.waitUntil( 5 | // Show a notification with title 'ServiceWorker Cookbook' and body 'Alea iacta est'. 6 | self.registration.showNotification('ServiceWorker Cookbook', { 7 | body: 'Alea iacta est', 8 | }) 9 | ); 10 | }); 11 | -------------------------------------------------------------------------------- /push-subscription-management/README.md: -------------------------------------------------------------------------------- 1 | # Push Subscription 2 | 3 | This recipe shows how to use push notifications with subscription management. 4 | 5 | ## Difficulty 6 | Advanced 7 | 8 | ## Use Case 9 | Allowing users to subscribe to features of your app allows you to keep in touch with and convert visitors! 10 | 11 | 12 | Init State 13 | ---------- 14 | After service worker is registered, client is checking if it is already subscribed to the notificiation service. Button's contents is set depending on this. 15 | 16 | Subscribe 17 | --------- 18 | After successful subscription (index.js::pushManager.subscribe) client sends a post request to application server to register the subscription 19 | 20 | Notifications 21 | ------------- 22 | Server periodically sends a notification using web-push library to all registered endpoints. 23 | If an endpoint is not registered anymore (expired or cancelled) it is removed from subscription list. 24 | 25 | Unsubscribe 26 | ----------- 27 | After successful unsubscription (index.js::pushSubscription.unsubscribe) client sends a post request to application server to unregister the subscription. Server is no longer sending notification. 28 | 29 | Subscription Expired 30 | -------------------- 31 | Service worker is watching for the *pushsubscriptionchange* event and resubscribes to the push service. 32 | 33 | Not in Recipe 34 | ------------- 35 | Subscription might be cancelled by the user outside of this page (from browser settings or notification UI). In this recipe server will stop to send the notifications, but the front-end doesn't know about it. One could periodically check if registration is still active. 36 | 37 | ## Category 38 | Web Push 39 | -------------------------------------------------------------------------------- /push-subscription-management/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This demo shows how to subscribe/unsubscribe to the push notifications.
17 | 18 |To simulate the subscription expiration under Firefox please set the dom.push.userAgentId
in about:config to an empty string. (Note: bug 1222428)
Choose a character once, see the times and reload. Check the difference in times.
12 |Fetching model time:
14 |Interpolation time:
15 |Loading time:
16 | 17 |Try to add and delete some quotations.
19 |Then go offline by disconnecting from Internet
20 |And try to continue adding some quotes.
21 |You can see they don't have a delete button because they are in queue
22 |Now reconnect to the Internet and you'll see how they automatically synchronize.
23 |Show console logs for more information. 24 |
28 |The Service Worker Cookbook is a collection of working, practical examples of using service workers in modern web sites.
4 |Tip: Open your Developer Tools console to view fetch
events and informative messages about what each recipe's service worker is doing!
The Service Worker Cookbook was created by Mozilla with contributions from developers like you. All source code is available on GitHub. Contributions and requests welcome.
8 |Attribution of pictures in Caching strategies category can be found at lorempixel.com.
9 | 10 |This image request originates from a controlled page so the image will 17 | be served by the service worker. Even if the content in the server changes, 18 | the page will show out of date content since it is served by the cache but 19 | thanks to the update process, next visit will show up to date content.
20 | 21 | 22 | -------------------------------------------------------------------------------- /strategy-cache-and-update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.
37 |In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.
38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /strategy-cache-and-update/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe reload. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-cache-and-update/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image originates from a non controlled page so, if you reload, it will be always synced with the version in the server.
17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-cache-and-update/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = 0; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-cache-and-update/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-and-update'; 2 | 3 | // On install, cache some resources. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | 7 | // Ask the service worker to keep installing until the returning promise 8 | // resolves. 9 | evt.waitUntil(precache()); 10 | }); 11 | 12 | // On fetch, use cache but update the entry with the latest contents 13 | // from the server. 14 | self.addEventListener('fetch', function(evt) { 15 | console.log('The service worker is serving the asset.'); 16 | // You can use `respondWith()` to answer immediately, without waiting for the 17 | // network response to reach the service worker... 18 | evt.respondWith(fromCache(evt.request)); 19 | // ...and `waitUntil()` to prevent the worker from being killed until the 20 | // cache is updated. 21 | evt.waitUntil(update(evt.request)); 22 | }); 23 | 24 | // Open a cache and use `addAll()` with an array of assets to add all of them 25 | // to the cache. Return a promise resolving when all the assets are added. 26 | function precache() { 27 | return caches.open(CACHE).then(function (cache) { 28 | return cache.addAll([ 29 | './controlled.html', 30 | './asset' 31 | ]); 32 | }); 33 | } 34 | 35 | // Open the cache where the assets were stored and search for the requested 36 | // resource. Notice that in case of no matching, the promise still resolves 37 | // but it does with `undefined` as value. 38 | function fromCache(request) { 39 | return caches.open(CACHE).then(function (cache) { 40 | return cache.match(request).then(function (matching) { 41 | return matching || Promise.reject('no-match'); 42 | }); 43 | }); 44 | } 45 | 46 | // Update consists in opening the cache, performing a network request and 47 | // storing the new response data. 48 | function update(request) { 49 | return caches.open(CACHE).then(function (cache) { 50 | return fetch(request).then(function (response) { 51 | return cache.put(request, response); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /strategy-cache-only/README.md: -------------------------------------------------------------------------------- 1 | # Cache only 2 | The recipe provides a service worker always answering from cache on `fetch` events. 3 | 4 | ## Difficulty 5 | Beginner 6 | 7 | ## Use Case 8 | For a given version of your site, you have static content that never changes 9 | such as the shell around the content. 10 | 11 | ## Solution 12 | Add static content during the installation of the service worker and use the 13 | cache to retrieve it whether the network is available or not. 14 | 15 | ## Category 16 | Caching strategies 17 | -------------------------------------------------------------------------------- /strategy-cache-only/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image request originates from a page under the service worker scope so 17 | the image will be served by the service worker. Due to cache only, it will 18 | be always served from the cache even if the server version changes and you 19 | reload.
20 | 21 | 22 | -------------------------------------------------------------------------------- /strategy-cache-only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.
37 |In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.
38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /strategy-cache-only/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its scope of action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe is reloaded. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-cache-only/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image originates from a non controlled page so, if you reload, it will be always synced with the version in the server.
17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-cache-only/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-cache-only/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-only'; 2 | 3 | // On install, cache some resources. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | 7 | // Ask the service worker to keep installing until the returning promise 8 | // resolves. 9 | evt.waitUntil(precache()); 10 | }); 11 | 12 | // On fetch, use cache only strategy. 13 | self.addEventListener('fetch', function(evt) { 14 | console.log('The service worker is serving the asset.'); 15 | evt.respondWith(fromCache(evt.request)); 16 | }); 17 | 18 | // Open a cache and use `addAll()` with an array of assets to add all of them 19 | // to the cache. Return a promise resolving when all the assets are added. 20 | function precache() { 21 | return caches.open(CACHE).then(function (cache) { 22 | return cache.addAll([ 23 | './controlled.html', 24 | './asset' 25 | ]); 26 | }); 27 | } 28 | 29 | // Open the cache where the assets were stored and search for the requested 30 | // resource. Notice that in case of no matching, the promise still resolves 31 | // but it does with `undefined` as value. 32 | function fromCache(request) { 33 | return caches.open(CACHE).then(function (cache) { 34 | return cache.match(request).then(function (matching) { 35 | return matching || Promise.reject('no-match'); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/README.md: -------------------------------------------------------------------------------- 1 | # Cache, update and refresh 2 | The recipe provides a service worker responding from cache to deliver fast 3 | responses and also updating the cache entry from the network. When the network 4 | response is ready, the UI updates automatically. 5 | 6 | ## Difficulty 7 | Intermediate 8 | 9 | ## Use Case 10 | You want to instantly show content while retrieving new content in background. 11 | Once the new content is available you want to show it somehow. 12 | 13 | ## Solution 14 | Serve the content from the cache but at the same time, perform a network request 15 | to update the cache entry and inform the UI about new up to date content. 16 | 17 | ## Category 18 | Caching strategies 19 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |New content available. Update
27 |This image request originates from a controlled page so the image will 28 | be served by the service worker. Even if the content in the server changes, 29 | the page will show out of date content since it is served by the cache but 30 | the service worker will inform the UI when the new content is 31 | available.
32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/controlled.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-update-and-refresh'; 2 | 3 | if ('serviceWorker' in navigator) { 4 | navigator.serviceWorker.onmessage = function (evt) { 5 | var message = JSON.parse(evt.data); 6 | 7 | var isRefresh = message.type === 'refresh'; 8 | var isAsset = message.url.includes('asset'); 9 | var lastETag = localStorage.currentETag; 10 | 11 | // [ETag](https://en.wikipedia.org/wiki/HTTP_ETag) header usually contains 12 | // the hash of the resource so it is a very effective way of check for fresh 13 | // content. 14 | var isNew = lastETag !== message.eTag; 15 | 16 | if (isRefresh && isAsset && isNew) { 17 | // Escape the first time (when there is no ETag yet) 18 | if (lastETag) { 19 | // Inform the user about the update 20 | notice.hidden = false; 21 | } 22 | // For teaching purposes, although this information is in the offline 23 | // cache and it could be retrieved from the service worker, keeping track 24 | // of the header in the `localStorage` keeps the implementation simple. 25 | localStorage.currentETag = message.eTag; 26 | } 27 | }; 28 | 29 | var notice = document.querySelector('#update-notice'); 30 | 31 | var update = document.querySelector('#update'); 32 | update.onclick = function (evt) { 33 | var img = document.querySelector('img'); 34 | // Avoid navigation. 35 | evt.preventDefault(); 36 | // Open the proper cache. 37 | caches.open(CACHE) 38 | // Get the updated response. 39 | .then(function (cache) { 40 | return cache.match(img.src); 41 | }) 42 | // Extract the body as a blob. 43 | .then(function (response) { 44 | return response.blob(); 45 | }) 46 | // Update the image content. 47 | .then(function (bodyBlob) { 48 | var url = URL.createObjectURL(bodyBlob); 49 | img.src = url; 50 | notice.hidden = true; 51 | }); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.
37 |In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.
38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URLs starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe reload. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image originates from a non controlled page, so if you reload, it will be always synced with the version in the server.
17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-update-and-refresh'; 2 | 3 | // On install, cache some resource. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | // Open a cache and use `addAll()` with an array of assets to add all of them 7 | // to the cache. Ask the service worker to keep installing until the 8 | // returning promise resolves. 9 | evt.waitUntil(caches.open(CACHE).then(function (cache) { 10 | cache.addAll([ 11 | './controlled.html', 12 | './asset' 13 | ]); 14 | })); 15 | }); 16 | 17 | // On fetch, use cache but update the entry with the latest contents 18 | // from the server. 19 | self.addEventListener('fetch', function(evt) { 20 | console.log('The service worker is serving the asset.'); 21 | // You can use `respondWith()` to answer ASAP... 22 | evt.respondWith(fromCache(evt.request)); 23 | // ...and `waitUntil()` to prevent the worker to be killed until 24 | // the cache is updated. 25 | evt.waitUntil( 26 | update(evt.request) 27 | // Finally, send a message to the client to inform it about the 28 | // resource is up to date. 29 | .then(refresh) 30 | ); 31 | }); 32 | 33 | // Open the cache where the assets were stored and search for the requested 34 | // resource. Notice that in case of no matching, the promise still resolves 35 | // but it does with `undefined` as value. 36 | function fromCache(request) { 37 | return caches.open(CACHE).then(function (cache) { 38 | return cache.match(request); 39 | }); 40 | } 41 | 42 | 43 | // Update consists in opening the cache, performing a network request and 44 | // storing the new response data. 45 | function update(request) { 46 | return caches.open(CACHE).then(function (cache) { 47 | return fetch(request).then(function (response) { 48 | return cache.put(request, response.clone()).then(function () { 49 | return response; 50 | }); 51 | }); 52 | }); 53 | } 54 | 55 | // Sends a message to the clients. 56 | function refresh(response) { 57 | return self.clients.matchAll().then(function (clients) { 58 | clients.forEach(function (client) { 59 | // Encode which resource has been updated. By including the 60 | // [ETag](https://en.wikipedia.org/wiki/HTTP_ETag) the client can 61 | // check if the content has changed. 62 | var message = { 63 | type: 'refresh', 64 | url: response.url, 65 | // Notice not all servers return the ETag header. If this is not 66 | // provided you should use other cache headers or rely on your own 67 | // means to check if the content has changed. 68 | eTag: response.headers.get('ETag') 69 | }; 70 | // Tell the client about the update. 71 | client.postMessage(JSON.stringify(message)); 72 | }); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/README.md: -------------------------------------------------------------------------------- 1 | # Embedded fallback 2 | The recipe provides a service worker serving an embedded content fallback in 3 | case of missing resources. 4 | 5 | ## Difficulty 6 | Intermediate 7 | 8 | ## Use Case 9 | You want to make sure the users always receive some content, even if the network 10 | is not available. 11 | 12 | ## Solution 13 | Embed fallback content and serve it in case of failure while requesting 14 | resources. 15 | 16 | ## Category 17 | Caching strategies 18 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image request points to an unavailable resource but since the page 17 | is controlled by the SW providing an embedded fallback it won't fail.
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/controlled.js: -------------------------------------------------------------------------------- 1 | // To be able to provide fallbacks since the beginning, we can only load the 2 | // image once we know the SW is ready. In addition, the service worker should 3 | // start intercepting without waiting for current clients to be closed. 4 | 5 | // Checking for controller is an effective way to see if there is an active 6 | // service worker controlling the page. 7 | if (navigator.serviceWorker.controller) { 8 | loadImage(); 9 | 10 | // If there is not, wait until there is one... 11 | } else { 12 | navigator.serviceWorker.oncontrollerchange = function() { 13 | // ...and monitor it until it's ready to intercept requests. 14 | this.controller.onstatechange = function() { 15 | if (this.state === 'activated') { 16 | loadImage(); 17 | } 18 | }; 19 | }; 20 | } 21 | 22 | function loadImage() { 23 | document.querySelector('img').src = './missing'; 24 | } 25 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |The images in these iframe points to the same asset in the server. But the first is controlled by the service worker and the second is not.
38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | var referenceIframe = document.getElementById('reference'); 10 | var sampleIframe = document.getElementById('sample'); 11 | 12 | // Fix heights every time the iframe reload. 13 | referenceIframe.onload = fixHeight; 14 | sampleIframe.onload = fixHeight; 15 | 16 | // Compute the correct height for the content of an iframe and adjust it 17 | // to match the content. 18 | function fixHeight(evt) { 19 | var iframe = evt.target; 20 | var document = iframe.contentWindow.document.documentElement; 21 | iframe.style.height = document.getClientRects()[0].height + 'px'; 22 | // Specifically for the cookbook site :( 23 | if (window.parent !== window) { 24 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image request points to an unavailable resource. Since there is no 17 | service worker providing a fallback, it will result in a missing image.
18 | 19 | 20 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-network-or-cache/README.md: -------------------------------------------------------------------------------- 1 | # Network or cache 2 | The service worker in this recipe tries to retrieve the most up to date content 3 | from the network but if the network is taking too long, it will serve cached 4 | content instead. 5 | 6 | ## Difficulty 7 | Beginner 8 | 9 | ## Use Case 10 | You want to show the most up to date content but it's preferable to load fast. 11 | 12 | ## Solution 13 | Serve content from network but include a timeout to fall back to cached data if 14 | the answer from the network doesn't arrive on time. 15 | 16 | ## Category 17 | Caching strategies 18 | -------------------------------------------------------------------------------- /strategy-network-or-cache/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image request originates from a controlled page so the image will 17 | be served by the service worker. The service worker will try to retrieve 18 | the most updated content from network but if the answer does not arrive 19 | before a timeout, it will fall back to the cached content. Try to 20 | 21 | adjust throttling to GPRS to see the effects of network latency.
22 | 23 | 24 | -------------------------------------------------------------------------------- /strategy-network-or-cache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Try to adjust network throttling to GPRS to see the image falling back to the cached content.
33 |The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.
38 |In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /strategy-network-or-cache/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe reload. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-network-or-cache/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |This image originates from a non controlled page so, if you reload, it will be always synced with the version in the server.
17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-network-or-cache/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-network-or-cache/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'network-or-cache'; 2 | 3 | // On install, cache some resource. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | 7 | // Ask the service worker to keep installing until the returning promise 8 | // resolves. 9 | evt.waitUntil(precache()); 10 | }); 11 | 12 | // On fetch, use cache but update the entry with the latest contents 13 | // from the server. 14 | self.addEventListener('fetch', function(evt) { 15 | console.log('The service worker is serving the asset.'); 16 | // Try network and if it fails, go for the cached copy. 17 | evt.respondWith(fromNetwork(evt.request, 400).catch(function () { 18 | return fromCache(evt.request); 19 | })); 20 | }); 21 | 22 | // Open a cache and use `addAll()` with an array of assets to add all of them 23 | // to the cache. Return a promise resolving when all the assets are added. 24 | function precache() { 25 | return caches.open(CACHE).then(function (cache) { 26 | return cache.addAll([ 27 | './controlled.html', 28 | './asset' 29 | ]); 30 | }); 31 | } 32 | 33 | // Time limited network request. If the network fails or the response is not 34 | // served before timeout, the promise is rejected. 35 | function fromNetwork(request, timeout) { 36 | return new Promise(function (fulfill, reject) { 37 | // Reject in case of timeout. 38 | var timeoutId = setTimeout(reject, timeout); 39 | // Fulfill in case of success. 40 | fetch(request).then(function (response) { 41 | clearTimeout(timeoutId); 42 | fulfill(response); 43 | // Reject also if network fetch rejects. 44 | }, reject); 45 | }); 46 | } 47 | 48 | // Open the cache where the assets were stored and search for the requested 49 | // resource. Notice that in case of no matching, the promise still resolves 50 | // but it does with `undefined` as value. 51 | function fromCache(request) { 52 | return caches.open(CACHE).then(function (cache) { 53 | return cache.match(request).then(function (matching) { 54 | return matching || Promise.reject('no-match'); 55 | }); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /tools.js: -------------------------------------------------------------------------------- 1 | // This function is needed because Chrome doesn't accept a base64 encoded string 2 | // as value for applicationServerKey in pushManager.subscribe yet 3 | // https://bugs.chromium.org/p/chromium/issues/detail?id=802280 4 | function urlBase64ToUint8Array(base64String) { 5 | var padding = '='.repeat((4 - base64String.length % 4) % 4); 6 | var base64 = (base64String + padding) 7 | .replace(/\-/g, '+') 8 | .replace(/_/g, '/'); 9 | 10 | var rawData = window.atob(base64); 11 | var outputArray = new Uint8Array(rawData.length); 12 | 13 | for (var i = 0; i < rawData.length; ++i) { 14 | outputArray[i] = rawData.charCodeAt(i); 15 | } 16 | return outputArray; 17 | } 18 | -------------------------------------------------------------------------------- /virtual-server/README.md: -------------------------------------------------------------------------------- 1 | # Virtual Server 2 | This recipe shows a service worker acting like a remote server. 3 | 4 | ## Difficulty 5 | Intermediate 6 | 7 | ## Use Case 8 | As an application developer, I want to to fully decouple UI from business logic. 9 | 10 | ## Solution 11 | With REST APIs you can decouple client from business logic. The business logic is actually a separated component placed on a remote server. With Service Workers you can do the same. Simply move your business logic to a Service Worker responding on fetch events. 12 | 13 | Instead of implementing your own logic to distinguish between routes and request methods, use [ServiceWorkerWare](https://github.com/gaia-components/serviceworkerware) or [sw-toolbox](https://github.com/GoogleChrome/sw-toolbox#defining-routes)' router feature to write your worker in a declarative way. 14 | 15 | The client code is virtually identical to [that in the API analytics recipe](/api-analytics_index_doc.html) (the report link has been removed). On the contrary, the [remote _Express server_](/api-analytics_server_doc.html) has been completely replaced by the _[ServiceWorkerWare](https://github.com/gaia-components/serviceworkerware) worker_. 16 | 17 | ## Category 18 | Beyond Offline 19 | -------------------------------------------------------------------------------- /virtual-server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Try to add and delete some quotations. The session is not intended to survive refreshing the page but you could see it pretending to survive. This means the worker holding the state has not been killed yet.
11 | 15 |