├── .gitignore ├── .npmignore ├── bower.json ├── .jshintrc ├── package.json ├── companion.js ├── lib ├── strategies │ ├── index.js │ ├── networkOnly.js │ ├── cacheOnly.js │ ├── cacheFirst.js │ ├── fastest.js │ └── networkFirst.js ├── options.js ├── route.js ├── helpers.js ├── router.js └── sw-toolbox.js ├── CONTRIBUTING.md ├── gulpfile.js ├── README.md ├── sw-toolbox.js ├── LICENSE └── sw-toolbox.map.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | lib/ 3 | tests/ 4 | node_modules/ 5 | npm-debug.log 6 | gulpfile.js 7 | bower.json 8 | .jshintrc 9 | .npmignore -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sw-toolbox", 3 | "main": "sw-toolbox.js", 4 | "moduleType": [ 5 | "globals" 6 | ], 7 | "license": "Apache-2", 8 | "ignore": [ 9 | "**/.*", 10 | "lib", 11 | "tests", 12 | "gulpfile.js" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "noempty": true, 13 | "nonew": true, 14 | "plusplus": false, 15 | "quotmark": "single", 16 | "undef": true, 17 | "unused": "vars", 18 | "strict": true, 19 | 20 | "worker": true, 21 | "devel": true, 22 | "node": true, 23 | "predef": ["fetch", "Request", "Response", "caches", "self", "URL"] 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sw-toolbox", 3 | "version": "2.0.0", 4 | "scripts": { 5 | "test": "gulp test" 6 | }, 7 | "repository": "https://github.com/GoogleChrome/sw-toolbox", 8 | "devDependencies": { 9 | "browserify": "^6.3.2", 10 | "gulp": "^3.8.10", 11 | "gulp-jshint": "^1.9.0", 12 | "jshint-stylish": "^1.0.0", 13 | "path-to-regexp": "^1.0.1", 14 | "serviceworker-cache-polyfill": "coonsta/cache-polyfill", 15 | "vinyl-source-stream": "^1.0.0" 16 | }, 17 | "dependencies": { 18 | "browserify-header": "^0.9.2", 19 | "minifyify": "^6.4.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /companion.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | (function() { 17 | 'use strict'; 18 | var workerScript = document.currentScript.dataset.serviceWorker; 19 | 20 | if (workerScript && 'serviceWorker' in navigator) { 21 | navigator.serviceWorker.register(workerScript); 22 | } 23 | })(); 24 | -------------------------------------------------------------------------------- /lib/strategies/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | module.exports = { 17 | networkOnly: require('./networkOnly'), 18 | networkFirst: require('./networkFirst'), 19 | cacheOnly: require('./cacheOnly'), 20 | cacheFirst: require('./cacheFirst'), 21 | fastest: require('./fastest') 22 | }; -------------------------------------------------------------------------------- /lib/strategies/networkOnly.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | var helpers = require('../helpers'); 18 | 19 | function networkOnly(request, values, options) { 20 | helpers.debug('Strategy: network only [' + request.url + ']', options); 21 | return fetch(request); 22 | } 23 | 24 | module.exports = networkOnly; -------------------------------------------------------------------------------- /lib/strategies/cacheOnly.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | var helpers = require('../helpers'); 18 | 19 | function cacheOnly(request, values, options) { 20 | helpers.debug('Strategy: cache only [' + request.url + ']', options); 21 | return helpers.openCache(options).then(function(cache) { 22 | return cache.match(request); 23 | }); 24 | } 25 | 26 | module.exports = cacheOnly; 27 | -------------------------------------------------------------------------------- /lib/strategies/cacheFirst.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | var helpers = require('../helpers'); 18 | 19 | function cacheFirst(request, values, options) { 20 | helpers.debug('Strategy: cache first [' + request.url + ']', options); 21 | return helpers.openCache(options).then(function(cache) { 22 | return cache.match(request).then(function (response) { 23 | if (response) { 24 | return response; 25 | } 26 | 27 | return helpers.fetchAndCache(request, options); 28 | }); 29 | }); 30 | } 31 | 32 | module.exports = cacheFirst; -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | // TODO: This is necessary to handle different implementations in the wild 19 | // The spec defines self.registration, but it was not implemented in Chrome 40. 20 | var scope; 21 | if (self.registration) { 22 | scope = self.registration.scope; 23 | } else { 24 | scope = self.scope || new URL('./', self.location).href; 25 | } 26 | 27 | module.exports = { 28 | cacheName: '$$$toolbox-cache$$$' + scope + '$$$', 29 | debug: false, 30 | preCacheItems: [], 31 | // A regular expression to apply to HTTP response codes. Codes that match 32 | // will be considered successes, while others will not, and will not be 33 | // cached. 34 | successResponses: /^0|([123]\d\d)|(40[14567])|410$/, 35 | }; 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement 9 | (CLA). 10 | 11 | * If you are an individual writing original source code and you're sure you 12 | own the intellectual property, then you'll need to sign an [individual CLA] 13 | (https://developers.google.com/open-source/cla/individual). 14 | * If you work for a company that wants to allow you to contribute your work, 15 | then you'll need to sign a [corporate CLA] 16 | (https://developers.google.com/open-source/cla/corporate). 17 | 18 | Follow either of the two links above to access the appropriate CLA and 19 | instructions for how to sign and return it. Once we receive it, we'll be able to 20 | accept your pull requests. 21 | 22 | ## Contributing A Patch 23 | 24 | 1. Submit an issue describing your proposed change to the repo in question. 25 | 1. The repo owner will respond to your issue promptly. 26 | 1. If your proposed change is accepted, and you haven't already done so, sign a 27 | Contributor License Agreement (see details above). 28 | 1. Fork the desired repo, develop and test your code changes. 29 | 1. Ensure that your code adheres to the existing style in the library. 30 | 1. Submit a pull request. 31 | -------------------------------------------------------------------------------- /lib/strategies/fastest.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | var helpers = require('../helpers'); 18 | var cacheOnly = require('./cacheOnly'); 19 | 20 | function fastest(request, values, options) { 21 | helpers.debug('Strategy: fastest [' + request.url + ']', options); 22 | var rejected = false; 23 | var reasons = []; 24 | 25 | var maybeReject = function(reason) { 26 | reasons.push(reason.toString()); 27 | if (rejected) { 28 | return Promise.reject(new Error('Both cache and network failed: "' + reasons.join('", "') + '"')); 29 | } 30 | rejected = true; 31 | }; 32 | 33 | return new Promise(function(resolve, reject) { 34 | helpers.fetchAndCache(request.clone(), options).then(resolve, maybeReject); 35 | cacheOnly(request, options).then(resolve, maybeReject); 36 | }); 37 | } 38 | 39 | module.exports = fastest; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | var browserify = require('browserify'); 19 | var gulp = require('gulp'); 20 | var source = require('vinyl-source-stream'); 21 | var jshint = require('gulp-jshint'); 22 | 23 | var sources = ['build/**/*.js', 'lib/**/*.js']; 24 | 25 | gulp.task('build', function() { 26 | var bundler = browserify({ 27 | entries: ['./lib/sw-toolbox.js'], 28 | standalone: 'toolbox', 29 | debug: true 30 | }); 31 | 32 | bundler.plugin('browserify-header'); 33 | bundler.plugin('minifyify', { 34 | map: 'sw-toolbox.map.json', 35 | output: 'sw-toolbox.map.json' 36 | }); 37 | 38 | 39 | return bundler 40 | .bundle() 41 | .pipe(source('sw-toolbox.js')) 42 | .pipe(gulp.dest('./')); 43 | }); 44 | 45 | gulp.task('test', function () { 46 | gulp.src(sources.concat('gulpfile.js')) 47 | .pipe(jshint('.jshintrc')) 48 | .pipe(jshint.reporter('jshint-stylish')); 49 | }); 50 | 51 | gulp.task('watch', ['default'], function() { 52 | gulp.watch(sources, ['default']); 53 | }); 54 | 55 | gulp.task('default', ['test', 'build']); -------------------------------------------------------------------------------- /lib/route.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | //TODO: Use self.registration.scope instead of self.location 19 | var url = new URL('./', self.location); 20 | var basePath = url.pathname; 21 | var pathRegexp = require('path-to-regexp'); 22 | 23 | 24 | var Route = function(method, path, handler, options) { 25 | // The URL() constructor can't parse express-style routes as they are not 26 | // valid urls. This means we have to manually manipulate relative urls into 27 | // absolute ones. This check is extremely naive but implementing a tweaked 28 | // version of the full algorithm seems like overkill 29 | // (https://url.spec.whatwg.org/#concept-basic-url-parser) 30 | if (path.indexOf('/') !== 0) { 31 | path = basePath + path; 32 | } 33 | 34 | this.method = method; 35 | this.keys = []; 36 | this.regexp = pathRegexp(path, this.keys); 37 | this.options = options; 38 | this.handler = handler; 39 | }; 40 | 41 | Route.prototype.makeHandler = function(url) { 42 | var match = this.regexp.exec(url); 43 | var values = {}; 44 | this.keys.forEach(function(key, index) { 45 | values[key.name] = match[index + 1]; 46 | }); 47 | return function(request) { 48 | return this.handler(request, values, this.options); 49 | }.bind(this); 50 | }; 51 | 52 | module.exports = Route; 53 | -------------------------------------------------------------------------------- /lib/strategies/networkFirst.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | var globalOptions = require('../options'); 18 | var helpers = require('../helpers'); 19 | 20 | function networkFirst(request, values, options) { 21 | options = options || {}; 22 | var successResponses = options.successResponses || globalOptions.successResponses; 23 | helpers.debug('Strategy: network first [' + request.url + ']', options); 24 | return helpers.openCache(options).then(function(cache) { 25 | return helpers.fetchAndCache(request, options).then(function(response) { 26 | if (successResponses.test(response.status)) { 27 | return response; 28 | } 29 | 30 | return cache.match(request).then(function(cacheResponse) { 31 | helpers.debug('Response was an HTTP error', options); 32 | if (cacheResponse) { 33 | helpers.debug('Resolving with cached response instead', options); 34 | return cacheResponse; 35 | } else { 36 | // If we didn't have anything in the cache, it's better to return the 37 | // error page than to return nothing 38 | helpers.debug('No cached result, resolving with HTTP error response from network', options); 39 | return response; 40 | } 41 | }); 42 | }).catch(function(error) { 43 | helpers.debug('Network error, fallback to cache [' + request.url + ']', options); 44 | return cache.match(request); 45 | }); 46 | }); 47 | } 48 | 49 | module.exports = networkFirst; -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | var globalOptions = require('./options'); 19 | 20 | function debug(message, options) { 21 | options = options || {}; 22 | var flag = options.debug || globalOptions.debug; 23 | if (flag) { 24 | console.log('[sw-toolbox] ' + message); 25 | } 26 | } 27 | 28 | function openCache(options) { 29 | options = options || {}; 30 | var cacheName = options.cacheName || globalOptions.cacheName; 31 | debug('Opening cache "' + cacheName + '"', options); 32 | return caches.open(cacheName); 33 | } 34 | 35 | function fetchAndCache(request, options) { 36 | options = options || {}; 37 | var successResponses = options.successResponses || globalOptions.successResponses; 38 | return fetch(request.clone()).then(function(response) { 39 | 40 | // Only cache successful responses 41 | if (successResponses.test(response.status)) { 42 | openCache(options).then(function(cache) { 43 | cache.put(request, response); 44 | }); 45 | } 46 | 47 | return response.clone(); 48 | }); 49 | } 50 | 51 | function renameCache(source, destination, options) { 52 | debug('Renaming cache: [' + source + '] to [' + destination + ']', options); 53 | return caches.delete(destination).then(function() { 54 | return Promise.all([ 55 | caches.open(source), 56 | caches.open(destination) 57 | ]).then(function(results) { 58 | var sourceCache = results[0]; 59 | var destCache = results[1]; 60 | 61 | return sourceCache.keys().then(function(requests) { 62 | return Promise.all(requests.map(function(request) { 63 | return sourceCache.match(request).then(function(response) { 64 | return destCache.put(request, response); 65 | }); 66 | })); 67 | }).then(function() { 68 | return caches.delete(source); 69 | }); 70 | }); 71 | }); 72 | } 73 | 74 | module.exports = { 75 | debug: debug, 76 | fetchAndCache: fetchAndCache, 77 | openCache: openCache, 78 | renameCache: renameCache 79 | }; -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | var Route = require('./route'); 19 | 20 | function regexEscape(s) { 21 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 22 | } 23 | 24 | var keyMatch = function(object, string) { 25 | var keys = Object.keys(object); 26 | for (var i = 0; i < keys.length; i++) { 27 | var pattern = new RegExp(keys[i]); 28 | if (pattern.test(string)) { 29 | return object[keys[i]]; 30 | } 31 | } 32 | return null; 33 | }; 34 | 35 | var Router = function() { 36 | this.routes = {}; 37 | this.default = null; 38 | }; 39 | 40 | ['get', 'post', 'put', 'delete', 'head', 'any'].forEach(function(method) { 41 | Router.prototype[method] = function(path, handler, options) { 42 | return this.add(method, path, handler, options); 43 | }; 44 | }); 45 | 46 | Router.prototype.add = function(method, path, handler, options) { 47 | options = options || {}; 48 | var origin = options.origin || self.location.origin; 49 | if (origin instanceof RegExp) { 50 | origin = origin.source; 51 | } else { 52 | origin = regexEscape(origin); 53 | } 54 | method = method.toLowerCase(); 55 | var route = new Route(method, path, handler, options); 56 | this.routes[origin] = this.routes[origin] || {}; 57 | this.routes[origin][method] = this.routes[origin][method] || {}; 58 | this.routes[origin][method][route.regexp.source] = route; 59 | }; 60 | 61 | Router.prototype.matchMethod = function(method, url) { 62 | url = new URL(url); 63 | var origin = url.origin; 64 | var path = url.pathname; 65 | method = method.toLowerCase(); 66 | 67 | var methods = keyMatch(this.routes, origin); 68 | if (!methods) { 69 | return null; 70 | } 71 | 72 | var routes = methods[method]; 73 | if (!routes) { 74 | return null; 75 | } 76 | 77 | var route = keyMatch(routes, path); 78 | 79 | if (route) { 80 | return route.makeHandler(path); 81 | } 82 | 83 | return null; 84 | }; 85 | 86 | Router.prototype.match = function(request) { 87 | return this.matchMethod(request.method, request.url) || this.matchMethod('any', request.url); 88 | }; 89 | 90 | module.exports = new Router(); 91 | -------------------------------------------------------------------------------- /lib/sw-toolbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | require('serviceworker-cache-polyfill'); 19 | var options = require('./options'); 20 | var router = require('./router'); 21 | var helpers = require('./helpers'); 22 | var strategies = require('./strategies'); 23 | 24 | helpers.debug('Service Worker Toolbox is loading'); 25 | 26 | // Install 27 | 28 | self.addEventListener('install', function(event) { 29 | var inactiveCache = options.cacheName + '$$$inactive$$$'; 30 | helpers.debug('install event fired'); 31 | helpers.debug('creating cache [' + inactiveCache + ']'); 32 | helpers.debug('preCache list: ' + (options.preCacheItems.join(', ') || '(none)')); 33 | event.waitUntil( 34 | helpers.openCache({cacheName: inactiveCache}).then(function(cache) { 35 | return Promise.all(options.preCacheItems).then(cache.addAll.bind(cache)); 36 | }) 37 | ); 38 | }); 39 | 40 | // Activate 41 | 42 | self.addEventListener('activate', function(event) { 43 | helpers.debug('activate event fired'); 44 | var inactiveCache = options.cacheName + '$$$inactive$$$'; 45 | event.waitUntil(helpers.renameCache(inactiveCache, options.cacheName)); 46 | }); 47 | 48 | // Fetch 49 | 50 | self.addEventListener('fetch', function(event) { 51 | var handler = router.match(event.request); 52 | 53 | if (handler) { 54 | event.respondWith(handler(event.request)); 55 | } else if (router.default) { 56 | event.respondWith(router.default(event.request)); 57 | } 58 | }); 59 | 60 | // Caching 61 | 62 | function cache(url, options) { 63 | return helpers.openCache(options).then(function(cache) { 64 | return cache.add(url); 65 | }); 66 | } 67 | 68 | function uncache(url, options) { 69 | return helpers.openCache(options).then(function(cache) { 70 | return cache.delete(url); 71 | }); 72 | } 73 | 74 | function precache(items) { 75 | if (!Array.isArray(items)) { 76 | items = [items]; 77 | } 78 | options.preCacheItems = options.preCacheItems.concat(items); 79 | } 80 | 81 | module.exports = { 82 | networkOnly: strategies.networkOnly, 83 | networkFirst: strategies.networkFirst, 84 | cacheOnly: strategies.cacheOnly, 85 | cacheFirst: strategies.cacheFirst, 86 | fastest: strategies.fastest, 87 | router: router, 88 | options: options, 89 | cache: cache, 90 | uncache: uncache, 91 | precache: precache 92 | }; 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Worker Toolbox 2 | 3 | > A collection of tools for [service workers](https://slightlyoff.github.io/ServiceWorker/spec/service_worker/) 4 | 5 | ## Service Worker helpers 6 | 7 | Service Worker Toolbox provides some simple helpers for use in creating your own service workers. If you're not sure what service workers are or what they are for, start with [the explainer doc](https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md). 8 | 9 | ### Installing Service Worker Toolbox 10 | 11 | Service Worker Toolbox is available through Bower, npm or direct from github: 12 | 13 | `bower install --save sw-toolbox` 14 | 15 | `npm install --save sw-toolbox` 16 | 17 | `git clone https://github.com/GoogleChrome/sw-toolbox.git` 18 | 19 | ### Registering your service worker 20 | 21 | From your registering page, register your service worker in the normal way. For example: 22 | 23 | ```javascript 24 | navigator.serviceWorker.register('my-service-worker.js', {scope: '/'}); 25 | ``` 26 | 27 | For even lower friction, if you don't intend to doing anything more fancy than just registering with a default scope, you can instead include the Service Worker Toolbox companion script in your HTML: 28 | 29 | ```html 30 | 31 | ``` 32 | 33 | As currently implemented in Chrome 40+, a service worker must exist at the root of the scope that you intend it to control, or higher. So if you want all of the pages under `/myapp/` to be controlled by the worker, the worker script itself must be served from either `/` or `/myapp/`. The default scope is the containing path of the service worker script. 34 | 35 | ### Using Service Worker Toolbox in your worker script 36 | 37 | In your service worker you just need to use `importScripts` to load Service Worker Toolbox 38 | 39 | ```javascript 40 | importScripts('bower_components/sw-toolbox/sw-toolbox.js'); // Update path to match your own setup 41 | ``` 42 | 43 | ## Basic usage 44 | Within your service worker file 45 | ```javascript 46 | // Set up routes from URL patterns to request handlers 47 | toolbox.router.get('/myapp/index.html', someHandler); 48 | 49 | // For some common cases Service Worker Toolbox provides a built-in handler 50 | toolbox.router.get('/', toolbox.networkFirst); 51 | 52 | // URL patterns are the same syntax as ExpressJS routes 53 | // (http://expressjs.com/guide/routing.html) 54 | toolbox.router.get(':foo/index.html', function(request, values) { 55 | return new Response('Handled a request for ' + request.url + 56 | ', where foo is "' + values.foo + '"); 57 | }); 58 | 59 | // For requests to other origins, specify the origin as an option 60 | toolbox.router.post('/(.*)', apiHandler, {origin: 'https://api.example.com'}); 61 | 62 | // Provide a default handler 63 | toolbox.router.default = myDefaultRequestHandler; 64 | 65 | // You can provide a list of resources which will be cached at service worker install time 66 | toolbox.precache(['/index.html', '/site.css', '/images/logo.png']); 67 | ``` 68 | 69 | ## Request handlers 70 | A request handler receives three arguments 71 | 72 | ```javascript 73 | var myHandler = function(request, values, options) { 74 | // ... 75 | } 76 | ``` 77 | 78 | - `request` - [Request](https://fetch.spec.whatwg.org/#request) object that triggered the `fetch` event 79 | - `values` - Object whose keys are the placeholder names in the URL pattern, with the values being the corresponding part of the request URL. For example, with a URL pattern of `'/images/:size/:name.jpg'` and an actual URL of `'/images/large/unicorns.jpg'`, `values` would be `{size: 'large', name: 'unicorns'}` 80 | - `options` - the options object that was used when [creating the route](#api) 81 | 82 | The return value should be a [Response](https://fetch.spec.whatwg.org/#response), or a [Promise](http://www.html5rocks.com/en/tutorials/es6/promises/) that resolves with a Response. If another value is returned, or if the returned Promise is rejected, the Request will fail which will appear to be a [NetworkError](https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-NetworkError) to the page that made the request. 83 | 84 | ### Built-in handlers 85 | 86 | There are 5 built-in handlers to cover the most common network strategies. For more information about offline strategies see the [Offline Cookbook](http://jakearchibald.com/2014/offline-cookbook/). 87 | 88 | #### `toolbox.networkFirst` 89 | Try to handle the request by fetching from the network. If it succeeds, store the response in the cache. Otherwise, try to fulfill the request from the cache. This is the strategy to use for basic read-through caching. Also good for API requests where you always want the freshest data when it is available but would rather have stale data than no data. 90 | 91 | #### `toolbox.cacheFirst` 92 | If the request matches a cache entry, respond with that. Otherwise try to fetch the resource from the network. If the network request succeeds, update the cache. Good for resources that don't change, or for which you have some other update mechanism. 93 | 94 | #### `toolbox.fastest` 95 | Request the resource from both the cache and the network in parallel. Respond with whichever returns first. Usually this will be the cached version, if there is one. On the one hand this strategy will always make a network request, even if the resource is cached. On the other hand, if/when the network request completes the cache is updated, so that future cache reads will be more up-to-date. 96 | 97 | #### `toolbox.cacheOnly` 98 | Resolve the request from the cache, or fail. Good for when you need to guarantee that no network request will be made - to save battery on mobile, for example. 99 | 100 | #### `toolbox.networkOnly` 101 | Handle the request by trying to fetch the URL from the network. If the fetch fails, fail the request. Essentially the same as not creating a route for the URL at all. 102 | 103 | ## API 104 | 105 | ### Global Options 106 | Any method that accepts an `options` object will accept a boolean option of `debug`. When true this causes Service Worker Toolbox to output verbose log messages to the worker's console. 107 | 108 | Most methods that involve a cache (`toolbox.cache`, `toolbox.uncache`, `toolbox.fastest`, `toolbox.cacheFirst`, `toolbox.cacheOnly`, `toolbox.networkFirst`) accept an option called `cache`, which is the **name** of the [Cache](https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache) that should be used. If not specifed Service Worker Toolbox will use a default cache. 109 | 110 | ### `toolbox.router.get(urlPattern, handler, options)` 111 | ### `toolbox.router.post(urlPattern, handler, options)` 112 | ### `toolbox.router.put(urlPattern, handler, options)` 113 | ### `toolbox.router.delete(urlPattern, handler, options)` 114 | ### `toolbox.router.head(urlPattern, handler, options)` 115 | Create a route that causes requests for URLs matching `urlPattern` to be resolved by calling `handler`. Matches requests using the GET, POST, PUT, DELETE or HEAD HTTP methods respectively. 116 | 117 | - `urlPattern` - an Express style route. See the docs for the [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module for the full syntax 118 | - `handler` - a request handler, as [described above](#request-handlers) 119 | - `options` - an object containing options for the route. This options object will be available to the request handler. The `origin` option is specific to the route methods, and is an exact string or a Regexp against which the origin of the Request must match for the route to be used. 120 | 121 | ### `toolbox.router.any(urlPattern, handler, options)` 122 | Like `toolbox.router.get`, etc., but matches any HTTP method. 123 | 124 | ### `toolbox.router.default` 125 | If you set this property to a function it will be used as the request handler for any request that does not match a route. 126 | 127 | ### `toolbox.precache(arrayOfURLs)` 128 | Add each URL in arrayOfURLs to the list of resources that should be cached during the service worker install step. Note that this needs to be called before the install event is triggered, so you should do it on the first run of your script. 129 | 130 | ### `toolbox.cache(url, options)` 131 | Causes the resource at `url` to be added to the cache. Returns a Promise. Supports the `debug` and `cache` [global options](#global-options). 132 | 133 | ### `toolbox.uncache(url, options)` 134 | Causes the resource at `url` to be removed from the cache. Returns a Promise. Supports the `debug` and `cache` [global options](#global-options). 135 | 136 | ## Support 137 | 138 | If you’ve found an error in this library, please file an issue: https://github.com/GoogleChrome/sw-toolbox/issues 139 | 140 | Patches are encouraged, and may be submitted by forking this project and submitting a pull request through GitHub. 141 | 142 | ## License 143 | 144 | Copyright 2014 Google, Inc. 145 | 146 | Licensed under the Apache License, Version 2.0 (the "License"); 147 | you may not use this file except in compliance with the License. 148 | You may obtain a copy of the License at 149 | 150 | http://www.apache.org/licenses/LICENSE-2.0 151 | 152 | Unless required by applicable law or agreed to in writing, software 153 | distributed under the License is distributed on an "AS IS" BASIS, 154 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 155 | See the License for the specific language governing permissions and 156 | limitations under the License. 157 | -------------------------------------------------------------------------------- /sw-toolbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self),o.toolbox=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o binding:\n var sequence = [];\n\n requests = requests.map(function(request) {\n if (request instanceof Request) {\n return request;\n }\n else {\n return String(request); // may throw TypeError\n }\n });\n\n return Promise.all(\n requests.map(function(request) {\n if (typeof request === 'string') {\n request = new Request(request);\n }\n\n var scheme = new URL(request.url).protocol;\n\n if (scheme !== 'http:' && scheme !== 'https:') {\n throw new NetworkError(\"Invalid scheme\");\n }\n\n return fetch(request.clone());\n })\n );\n }).then(function(responses) {\n // TODO: check that requests don't overwrite one another\n // (don't think this is possible to polyfill due to opaque responses)\n return Promise.all(\n responses.map(function(response, i) {\n return cache.put(requests[i], response);\n })\n );\n }).then(function() {\n return undefined;\n });\n };\n}\n","var isArray = require('isarray');\n\n/**\n * Expose `pathToRegexp`.\n */\nmodule.exports = pathToRegexp;\n\n/**\n * The main path matching regexp utility.\n *\n * @type {RegExp}\n */\nvar PATH_REGEXP = new RegExp([\n // Match escaped characters that would otherwise appear in future matches.\n // This allows the user to escape special characters that won't transform.\n '(\\\\\\\\.)',\n // Match Express-style parameters and un-named parameters with a prefix\n // and optional suffixes. Matches appear as:\n //\n // \"/:test(\\\\d+)?\" => [\"/\", \"test\", \"\\d+\", undefined, \"?\"]\n // \"/route(\\\\d+)\" => [undefined, undefined, undefined, \"\\d+\", undefined]\n '([\\\\/.])?(?:\\\\:(\\\\w+)(?:\\\\(((?:\\\\\\\\.|[^)])*)\\\\))?|\\\\(((?:\\\\\\\\.|[^)])*)\\\\))([+*?])?',\n // Match regexp special characters that are always escaped.\n '([.+*?=^!:${}()[\\\\]|\\\\/])'\n].join('|'), 'g');\n\n/**\n * Escape the capturing group by escaping special characters and meaning.\n *\n * @param {String} group\n * @return {String}\n */\nfunction escapeGroup (group) {\n return group.replace(/([=!:$\\/()])/g, '\\\\$1');\n}\n\n/**\n * Attach the keys as a property of the regexp.\n *\n * @param {RegExp} re\n * @param {Array} keys\n * @return {RegExp}\n */\nfunction attachKeys (re, keys) {\n re.keys = keys;\n return re;\n}\n\n/**\n * Get the flags for a regexp from the options.\n *\n * @param {Object} options\n * @return {String}\n */\nfunction flags (options) {\n return options.sensitive ? '' : 'i';\n}\n\n/**\n * Pull out keys from a regexp.\n *\n * @param {RegExp} path\n * @param {Array} keys\n * @return {RegExp}\n */\nfunction regexpToRegexp (path, keys) {\n // Use a negative lookahead to match only capturing groups.\n var groups = path.source.match(/\\((?!\\?)/g);\n\n if (groups) {\n for (var i = 0; i < groups.length; i++) {\n keys.push({\n name: i,\n delimiter: null,\n optional: false,\n repeat: false\n });\n }\n }\n\n return attachKeys(path, keys);\n}\n\n/**\n * Transform an array into a regexp.\n *\n * @param {Array} path\n * @param {Array} keys\n * @param {Object} options\n * @return {RegExp}\n */\nfunction arrayToRegexp (path, keys, options) {\n var parts = [];\n\n for (var i = 0; i < path.length; i++) {\n parts.push(pathToRegexp(path[i], keys, options).source);\n }\n\n var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options));\n return attachKeys(regexp, keys);\n}\n\n/**\n * Replace the specific tags with regexp strings.\n *\n * @param {String} path\n * @param {Array} keys\n * @return {String}\n */\nfunction replacePath (path, keys) {\n var index = 0;\n\n function replace (_, escaped, prefix, key, capture, group, suffix, escape) {\n if (escaped) {\n return escaped;\n }\n\n if (escape) {\n return '\\\\' + escape;\n }\n\n var repeat = suffix === '+' || suffix === '*';\n var optional = suffix === '?' || suffix === '*';\n\n keys.push({\n name: key || index++,\n delimiter: prefix || '/',\n optional: optional,\n repeat: repeat\n });\n\n prefix = prefix ? ('\\\\' + prefix) : '';\n capture = escapeGroup(capture || group || '[^' + (prefix || '\\\\/') + ']+?');\n\n if (repeat) {\n capture = capture + '(?:' + prefix + capture + ')*';\n }\n\n if (optional) {\n return '(?:' + prefix + '(' + capture + '))?';\n }\n\n // Basic parameter support.\n return prefix + '(' + capture + ')';\n }\n\n return path.replace(PATH_REGEXP, replace);\n}\n\n/**\n * Normalize the given path string, returning a regular expression.\n *\n * An empty array can be passed in for the keys, which will hold the\n * placeholder key descriptions. For example, using `/user/:id`, `keys` will\n * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.\n *\n * @param {(String|RegExp|Array)} path\n * @param {Array} [keys]\n * @param {Object} [options]\n * @return {RegExp}\n */\nfunction pathToRegexp (path, keys, options) {\n keys = keys || [];\n\n if (!isArray(keys)) {\n options = keys;\n keys = [];\n } else if (!options) {\n options = {};\n }\n\n if (path instanceof RegExp) {\n return regexpToRegexp(path, keys, options);\n }\n\n if (isArray(path)) {\n return arrayToRegexp(path, keys, options);\n }\n\n var strict = options.strict;\n var end = options.end !== false;\n var route = replacePath(path, keys);\n var endsWithSlash = path.charAt(path.length - 1) === '/';\n\n // In non-strict mode we allow a slash at the end of match. If the path to\n // match already ends with a slash, we remove it for consistency. The slash\n // is valid at the end of a path match, not in the middle. This is important\n // in non-ending mode, where \"/test/\" shouldn't match \"/test//route\".\n if (!strict) {\n route = (endsWithSlash ? route.slice(0, -2) : route) + '(?:\\\\/(?=$))?';\n }\n\n if (end) {\n route += '$';\n } else {\n // In non-ending mode, we need the capturing groups to match as much as\n // possible by using a positive lookahead to the end or next path segment.\n route += strict && endsWithSlash ? '' : '(?=\\\\/|$)';\n }\n\n return attachKeys(new RegExp('^' + route, flags(options)), keys);\n}\n","module.exports = Array.isArray || function (arr) {\n return Object.prototype.toString.call(arr) == '[object Array]';\n};\n"]} --------------------------------------------------------------------------------