├── .gitignore ├── .jakecache.gif ├── .travis.yml ├── LICENSE.md ├── README.md ├── dist ├── jakecache-sw.js └── jakecache.js ├── example ├── appcache.manifest ├── test.html └── test.manifest ├── jakecache-sw.js ├── jakecache.js ├── lib └── md5.js ├── package.json ├── scripts └── build.js └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jakecache.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenchris/jakecache/43452fbb85e07bdd2b71a1c5eb54c02fc876b67a/.jakecache.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | - 6 5 | - '0.12' 6 | - '0.10' 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Everything in this repo is BSD style license unless otherwise specified. 4 | 5 | Copyright (c) 2016 JakeCache authors. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following disclaimer 13 | in the documentation and/or other materials provided with the 14 | distribution. 15 | * Neither the name of JakeCache authors nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JakeCache 2 | 3 | Declarative manifest-driven app cache built on top of ServiceWorker. 4 | 5 | [](https://travis-ci.org/kenchris/jakecache) 6 | [](https://github.com/feross/standard) 7 | 8 |  9 | 10 | ## Why? 11 | 12 | Building offline-first applications has been the ubiquius dream since the early days of the web. Google started by introducing Google Gears, and later followed the web community by building [Application Cache](https://www.w3.org/TR/2011/WD-html5-20110525/offline.html). 13 | 14 | Application Cache was a great step forward, but had several fundamental flaws that were made famous by [Jake Archibald](https://twitter.com/jaffathecake)'s epic [Application Cache is a Douchebag](http://alistapart.com/article/application-cache-is-a-douchebag) article and [talk](https://www.youtube.com/watch?v=cR-TP6jOSQM). So Jake, [Alex Russell](https://twitter.com/slightlylate) and many others, have been busy working on the next generation of application caching API's which today are know as the [Service Worker Specification](https://github.com/slightlyoff/ServiceWorker). 15 | 16 | Service Worker is great, but if you ever had a look at it's API(s) you realize that they are complicated imperative JavaScript API's. These API's tend to scare many web developers who prefer a nice forgiving declarative approach. 17 | 18 | So in order to **fix** the **too** complicated Service Worker API, we are super excited to introduce **JakeCache**. A declarative manifest-driven application cache for web applications implemented on top of ServiceWorker. 19 | 20 | *Sarcasm may occur in this project* 21 | 22 | 😂 23 | 24 | ### Polyfill 25 | 26 | JakeCache serves the additional purpose of being as compatible with the HTML5 Application Cache (aka AppCache) as we could make it and may serve as a polyfill in browsers removing such support. 27 | 28 | Patches are welcome! 29 | 30 | ## Installation 31 | 32 | ```bash 33 | npm install jakecache --save 34 | ``` 35 | 36 | ## Get started 37 | 38 | 1. Create a new JakeCache Manifest, `app.manifest` and save it in your root together with the `jakecache.js` file: 39 | ``` 40 | CACHE MANIFEST 41 | # 2010-06-18:v2 42 | 43 | # Explicitly cached 'master entries'. 44 | CACHE: 45 | /test.html 46 | 47 | # Resources that require the user to be online. 48 | NETWORK: 49 | * 50 | ``` 51 | 52 | 1. Include ``jakecache.js`` on your page, maybe via `````` 53 | 2. Add `````` to your HTML. 54 | 3. That's it! Your website is now Jake-enabled! 55 | 56 | ## License 57 | 58 | See [LICENSE.md](https://github.com/kenchris/jakecache/blob/master/LICENSE.md) 59 | 60 | ### About this project 61 | This is a project by [Kenneth Christiansen](https://twitter.com/kennethrohde) & [Kenneth Auchenberg](https://twitter.com/auchenberg) and a result of too much 🍺 and ☕. 62 | -------------------------------------------------------------------------------- /dist/jakecache-sw.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * es6-md5 5 | * Port of https://github.com/blueimp/JavaScript-MD5 to ES2015 6 | * 7 | * Copyright 2011, Sebastian Tschan 8 | * https://blueimp.net 9 | * 10 | * Licensed under the MIT license: 11 | * http://www.opensource.org/licenses/MIT 12 | * 13 | * Based on 14 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 15 | * Digest Algorithm, as defined in RFC 1321. 16 | * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 17 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 18 | * Distributed under the BSD License 19 | * See http://pajhome.org.uk/crypt/md5 for more info. 20 | */ 21 | 22 | /* 23 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 24 | * to work around bugs in some JS interpreters. 25 | */ 26 | function safe_add (x, y) { 27 | const lsw = (x & 0xFFFF) + (y & 0xFFFF); 28 | const msw = (x >> 16) + (y >> 16) + (lsw >> 16); 29 | return (msw << 16) | (lsw & 0xFFFF) 30 | } 31 | 32 | /* 33 | * Bitwise rotate a 32-bit number to the left. 34 | */ 35 | function bit_rol (num, cnt) { 36 | return (num << cnt) | (num >>> (32 - cnt)) 37 | } 38 | 39 | /* 40 | * These functions implement the four basic operations the algorithm uses. 41 | */ 42 | function md5_cmn (q, a, b, x, s, t) { 43 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) 44 | } 45 | function md5_ff (a, b, c, d, x, s, t) { 46 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t) 47 | } 48 | function md5_gg (a, b, c, d, x, s, t) { 49 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t) 50 | } 51 | function md5_hh (a, b, c, d, x, s, t) { 52 | return md5_cmn(b ^ c ^ d, a, b, x, s, t) 53 | } 54 | function md5_ii (a, b, c, d, x, s, t) { 55 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t) 56 | } 57 | 58 | /* 59 | * Calculate the MD5 of an array of little-endian words, and a bit length. 60 | */ 61 | function binl_md5 (x, len) { 62 | /* append padding */ 63 | x[len >> 5] |= 0x80 << (len % 32) 64 | x[(((len + 64) >>> 9) << 4) + 14] = len 65 | 66 | let i; 67 | let olda; 68 | let oldb; 69 | let oldc; 70 | let oldd; 71 | let a = 1732584193; 72 | let b = -271733879; 73 | let c = -1732584194; 74 | let d = 271733878; 75 | 76 | for (i = 0; i < x.length; i += 16) { 77 | olda = a 78 | oldb = b 79 | oldc = c 80 | oldd = d 81 | 82 | a = md5_ff(a, b, c, d, x[i], 7, -680876936) 83 | d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) 84 | c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) 85 | b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) 86 | a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) 87 | d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) 88 | c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) 89 | b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) 90 | a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) 91 | d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) 92 | c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) 93 | b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) 94 | a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) 95 | d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) 96 | c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) 97 | b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) 98 | 99 | a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) 100 | d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) 101 | c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) 102 | b = md5_gg(b, c, d, a, x[i], 20, -373897302) 103 | a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) 104 | d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) 105 | c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) 106 | b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) 107 | a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) 108 | d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) 109 | c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) 110 | b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) 111 | a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) 112 | d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) 113 | c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) 114 | b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) 115 | 116 | a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) 117 | d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) 118 | c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) 119 | b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) 120 | a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) 121 | d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) 122 | c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) 123 | b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) 124 | a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) 125 | d = md5_hh(d, a, b, c, x[i], 11, -358537222) 126 | c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) 127 | b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) 128 | a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) 129 | d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) 130 | c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) 131 | b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) 132 | 133 | a = md5_ii(a, b, c, d, x[i], 6, -198630844) 134 | d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) 135 | c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) 136 | b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) 137 | a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) 138 | d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) 139 | c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) 140 | b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) 141 | a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) 142 | d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) 143 | c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) 144 | b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) 145 | a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) 146 | d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) 147 | c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) 148 | b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) 149 | 150 | a = safe_add(a, olda) 151 | b = safe_add(b, oldb) 152 | c = safe_add(c, oldc) 153 | d = safe_add(d, oldd) 154 | } 155 | return [a, b, c, d] 156 | } 157 | 158 | /* 159 | * Convert an array of little-endian words to a string 160 | */ 161 | function binl2rstr (input) { 162 | let i; 163 | let output = ''; 164 | for (i = 0; i < input.length * 32; i += 8) { 165 | output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF) 166 | } 167 | return output 168 | } 169 | 170 | /* 171 | * Convert a raw string to an array of little-endian words 172 | * Characters >255 have their high-byte silently ignored. 173 | */ 174 | function rstr2binl (input) { 175 | let i; 176 | const output = []; 177 | output[(input.length >> 2) - 1] = undefined 178 | for (i = 0; i < output.length; i += 1) { 179 | output[i] = 0 180 | } 181 | for (i = 0; i < input.length * 8; i += 8) { 182 | output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32) 183 | } 184 | return output 185 | } 186 | 187 | /* 188 | * Calculate the MD5 of a raw string 189 | */ 190 | function rstr_md5 (s) { 191 | return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) 192 | } 193 | 194 | /* 195 | * Calculate the HMAC-MD5, of a key and some data (raw strings) 196 | */ 197 | function rstr_hmac_md5 (key, data) { 198 | let i; 199 | let bkey = rstr2binl(key); 200 | const ipad = []; 201 | const opad = []; 202 | let hash; 203 | ipad[15] = opad[15] = undefined 204 | if (bkey.length > 16) { 205 | bkey = binl_md5(bkey, key.length * 8) 206 | } 207 | for (i = 0; i < 16; i += 1) { 208 | ipad[i] = bkey[i] ^ 0x36363636 209 | opad[i] = bkey[i] ^ 0x5C5C5C5C 210 | } 211 | hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) 212 | return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) 213 | } 214 | 215 | /* 216 | * Convert a raw string to a hex string 217 | */ 218 | function rstr2hex (input) { 219 | const hex_tab = '0123456789abcdef'; 220 | let output = ''; 221 | let x; 222 | let i; 223 | for (i = 0; i < input.length; i += 1) { 224 | x = input.charCodeAt(i) 225 | output += hex_tab.charAt((x >>> 4) & 0x0F) + 226 | hex_tab.charAt(x & 0x0F) 227 | } 228 | return output 229 | } 230 | 231 | /* 232 | * Encode a string as utf-8 233 | */ 234 | function str2rstr_utf8 (input) { 235 | return unescape(encodeURIComponent(input)) 236 | } 237 | 238 | /* 239 | * Take string arguments and return either raw or hex encoded strings 240 | */ 241 | function raw_md5 (s) { 242 | return rstr_md5(str2rstr_utf8(s)) 243 | } 244 | function hex_md5 (s) { 245 | return rstr2hex(raw_md5(s)) 246 | } 247 | function raw_hmac_md5 (k, d) { 248 | return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)) 249 | } 250 | function hex_hmac_md5 (k, d) { 251 | return rstr2hex(raw_hmac_md5(k, d)) 252 | } 253 | 254 | function md5 (string, key, raw) { 255 | if (!key) { 256 | if (!raw) { 257 | return hex_md5(string) 258 | } 259 | return raw_md5(string) 260 | } 261 | if (!raw) { 262 | return hex_hmac_md5(key, string) 263 | } 264 | return raw_hmac_md5(key, string) 265 | } 266 | 267 | class JakeCacheManifest { 268 | 269 | constructor () { 270 | this._path = null 271 | this._hash = null 272 | this._isValid = false 273 | this._fetchOptions = { credentials: "same-origin" } 274 | } 275 | 276 | groupName () { 277 | let filename = this._path.substring(this._path.lastIndexOf('/') + 1) 278 | return filename 279 | } 280 | 281 | fetchData (path, options = {}) { 282 | this._path = path 283 | 284 | if (this._isValid && options.cache !== 'reload') { 285 | return Promise.resolve(false) 286 | } 287 | 288 | // http://html5doctor.com/go-offline-with-application-cache/ 289 | return fetch(new Request(this._path, options), this._fetchOptions).then((response) => { 290 | if (response.type === 'opaque' || response.status === 404 || response.status === 410) { 291 | return Promise.reject() 292 | } 293 | 294 | this._rawData = { 295 | cache: [], 296 | fallback: [], 297 | network: [] 298 | } 299 | 300 | return response.text().then((result) => { 301 | return new Promise((resolve, reject) => { 302 | let hash = md5(result) 303 | if (this._hash && hash.toString() === this._hash.toString()) { 304 | console.log('noupdate: ' + hash) 305 | return resolve(false) 306 | } 307 | this._hash = hash 308 | console.log(`update: ${hash} (was: ${this._hash})`) 309 | 310 | let lines = result.split(/\r|\n/) 311 | let header = 'cache' // default. 312 | 313 | let firstLine = lines.shift() 314 | if (firstLine !== 'CACHE MANIFEST') { 315 | return reject() 316 | } 317 | 318 | for (let line of lines) { 319 | line = line.replace(/#.*$/, '').trim() 320 | 321 | if (line === '') { 322 | continue 323 | } 324 | 325 | let res = line.match(/^([A-Z]*):/) 326 | if (res) { 327 | header = res[1].toLowerCase() 328 | continue 329 | } 330 | 331 | if (!this._rawData[header]) { 332 | this._rawData[header] = [] 333 | } 334 | this._rawData[header].push(line) 335 | } 336 | 337 | this.cache = ['jakecache.js'] 338 | // Ignore different protocol 339 | for (let pathname of this._rawData.cache) { 340 | let path = new URL(pathname, location) 341 | if (path.protocol === location.protocol) { 342 | this.cache.push(path) 343 | } 344 | } 345 | 346 | this.fallback = [] 347 | for (let entry of this._rawData.fallback) { 348 | let [pathname, fallbackPath] = entry.split(' ') 349 | let path = new URL(pathname, location) 350 | let fallback = new URL(fallbackPath, location) 351 | 352 | // Ignore cross-origin fallbacks 353 | if (path.origin === fallback.origin) { 354 | this.fallback.push([path, fallback]) 355 | this.cache.push(fallback) 356 | } 357 | } 358 | 359 | this.allowNetworkFallback = false 360 | this.network = [] 361 | for (let entry of this._rawData.network) { 362 | if (entry === '*') { 363 | this.allowNetworkFallback = true 364 | continue 365 | } 366 | let path = new URL(entry, location) 367 | if (path.protocol === location.protocol) { 368 | this.network.push(path) 369 | } 370 | } 371 | 372 | this._isValid = true 373 | resolve(true) 374 | }) 375 | }) 376 | }) 377 | } 378 | } 379 | 380 | self.addEventListener('message', function (event) { 381 | switch (event.data.command) { 382 | case 'update': 383 | update.call(this, event.data.pathname, event.data.options) 384 | break 385 | case 'abort': 386 | postMessage({ type: 'error', message: 'Not implementable without cancellable promises.' }) 387 | break 388 | case 'swapCache': 389 | swapCache() 390 | break 391 | } 392 | }) 393 | 394 | let manifest = new JakeCacheManifest() 395 | 396 | const CacheStatus = { 397 | UNCACHED: 0, 398 | IDLE: 1, 399 | CHECKING: 2, 400 | DOWNLOADING: 3, 401 | UPDATEREADY: 4, 402 | OBSOLETE: 5 403 | } 404 | 405 | let cacheStatus = CacheStatus.UNCACHED 406 | 407 | function postMessage (msg) { 408 | return self.clients.matchAll().then(clients => { 409 | return Promise.all(clients.map(client => { 410 | return client.postMessage(msg) 411 | })) 412 | }) 413 | } 414 | 415 | function swapCache () { 416 | caches.keys().then(keyList => { 417 | return Promise.all(keyList.map(key => { 418 | return caches.delete(key) 419 | })) 420 | }).then(() => { 421 | // FIXME: Add new keys. 422 | }) 423 | } 424 | 425 | // 7.9.4 426 | function update (pathname, options = {}) { 427 | if (!pathname) { 428 | console.log('No pathname!') 429 | return Promise.reject() 430 | } 431 | 432 | // *.2.2 433 | this.options = options 434 | this.cacheGroup = pathname 435 | 436 | return caches.keys().then(cacheNames => { 437 | this.uncached = !cacheNames.length 438 | console.log('uncached ' + this.uncached) 439 | return Promise.resolve(this.uncached) 440 | }).then((uncached) => { 441 | if (this.options.cache !== 'reload' && !uncached) { 442 | // We have a cache and we are no doing an update check. 443 | return Promise.reject() 444 | } 445 | 446 | // *.2.4 and *.2.6 447 | if (cacheStatus === CacheStatus.CHECKING) { 448 | postMessage({ type: 'checking' }) 449 | postMessage({ type: 'abort' }) 450 | return Promise.reject() 451 | } 452 | // *.2.4, *.2.5, *.2.6 453 | if (cacheStatus === CacheStatus.DOWNLOADING) { 454 | postMessage({ type: 'checking' }) 455 | postMessage({ type: 'downloading' }) 456 | postMessage({ type: 'abort' }) 457 | return Promise.reject() 458 | } 459 | return Promise.resolve() 460 | }).then(() => { 461 | // *.2.7 and *.2.8 462 | cacheStatus = CacheStatus.CHECKING 463 | postMessage({ type: 'checking' }) 464 | 465 | // FIXME: *.6: Fetch manifest, mark obsolete if fails. 466 | return manifest.fetchData(this.cacheGroup, this.options).catch(err => { 467 | cacheStatus = CacheStatus.OBSOLETE 468 | postMessage({ type: 'obsolete' }) 469 | // FIXME: *.7: Error for each existing entry. 470 | cacheStatus = CacheStatus.IDLE 471 | postMessage({ type: 'idle' }) 472 | return Promise.reject(err) 473 | }) 474 | }).then(modified => { 475 | this.modified = modified 476 | // *.2: If cache group already has an application cache in it, then 477 | // this is an upgrade attempt. Otherwise, this is a cache attempt. 478 | return caches.keys().then(cacheNames => { 479 | return Promise.resolve(!!cacheNames.length) 480 | }) 481 | }).then(upgrade => { 482 | this.upgrade = upgrade 483 | if (this.upgrade && !this.modified) { 484 | cacheStatus = CacheStatus.IDLE 485 | postMessage({ type: 'noupdate' }) 486 | return Promise.reject() 487 | } 488 | 489 | // Appcache is no-cors by default. 490 | this.requests = manifest.cache.map(url => { 491 | return new Request(url, { mode: 'no-cors' }) 492 | }) 493 | 494 | cacheStatus = CacheStatus.DOWNLOADING 495 | postMessage({ type: 'downloading' }) 496 | 497 | this.loaded = 0 498 | this.total = this.requests.length 499 | 500 | return Promise.all(this.requests.map(request => { 501 | // Manual fetch to emulate appcache behavior. 502 | return fetch(request, manifest._fetchOptions).then(response => { 503 | cacheStatus = CacheStatus.PROGRESS 504 | postMessage({ 505 | type: 'progress', 506 | lengthComputable: true, 507 | loaded: ++(this.loaded), 508 | total: this.total 509 | }) 510 | 511 | // section 5.6.4 of http://www.w3.org/TR/2011/WD-html5-20110525/offline.html 512 | 513 | // Redirects are fatal. 514 | if (response.url !== request.url) { 515 | throw Error() 516 | } 517 | 518 | // FIXME: should we update this.total below? 519 | 520 | if (response.type !== 'opaque') { 521 | // If the error was a 404 or 410 HTTP response or equivalent 522 | // Skip this resource. It is dropped from the cache. 523 | if (response.status < 200 || response.status >= 300) { 524 | return undefined 525 | } 526 | 527 | // HTTP caching rules, such as Cache-Control: no-store, are ignored. 528 | if ((response.headers.get('cache-control') || '').match(/no-store/i)) { 529 | return undefined 530 | } 531 | } 532 | 533 | return response 534 | }) 535 | })) 536 | }).then(responses => { 537 | this.responses = responses.filter(response => response) 538 | if (this.upgrade) { 539 | cacheStatus = CacheStatus.UPDATEREADY 540 | postMessage({ type: 'updateready' }) 541 | return Promise.reject() 542 | } else { 543 | return Promise.resolve(this.responses) 544 | } 545 | }).then(responses => { 546 | console.log('Adding to cache ' + manifest.groupName()) 547 | return caches.open(manifest.groupName()).then(cache => { 548 | return Promise.all(responses.map((response, index) => { 549 | return cache.put(self.requests[index], response) 550 | })) 551 | }).then(_ => { 552 | cacheStatus = CacheStatus.CACHED 553 | postMessage({ type: 'cached' }) 554 | }) 555 | }).catch(err => { 556 | if (err) { 557 | postMessage({ type: 'error' }, err) 558 | console.log(err) 559 | } 560 | }) 561 | } 562 | 563 | self.addEventListener('install', function (event) { 564 | event.waitUntil(self.skipWaiting()) 565 | }) 566 | 567 | self.addEventListener('activate', function (event) { 568 | event.waitUntil(self.clients.claim()) 569 | }) 570 | 571 | self.addEventListener('fetch', function (event) { 572 | if (cacheStatus === CacheStatus.UNCACHED) { 573 | return fetch(event.request) 574 | } 575 | 576 | let url = new URL(event.request.url) 577 | 578 | // Ignore non-GET and different schemes. 579 | if (event.request.method !== 'GET' || url.scheme !== location.scheme) { 580 | return 581 | } 582 | 583 | // FIXME: Get data from IndexedDB instead. 584 | event.respondWith(manifest.fetchData('test.manifest').then(_ => { 585 | // Process network-only. 586 | if (manifest.network.filter(entry => entry.href === url.href).length) { 587 | return fetch(event.request) 588 | } 589 | 590 | return caches.match(event.request).then(response => { 591 | // Cache always wins. 592 | if (response) { 593 | return response 594 | } 595 | 596 | // Fallbacks consult network, and falls back on failure. 597 | for (let [path, fallback] of manifest.fallback) { 598 | if (url.href.indexOf(path) === 0) { 599 | return fetch(event.request).then(response => { 600 | // Same origin only. 601 | if (new URL(response.url).origin !== location.origin) { 602 | throw Error() 603 | } 604 | 605 | if (response.type !== 'opaque') { 606 | if (response.status < 200 || response.status >= 300) { 607 | throw Error() 608 | } 609 | } 610 | }).catch(_ => { 611 | return cache.match(fallback) 612 | }) 613 | } 614 | } 615 | 616 | if (manifest.allowNetworkFallback) { 617 | return fetch(event.request) 618 | } 619 | 620 | return response // failure. 621 | }) 622 | })) 623 | }) -------------------------------------------------------------------------------- /dist/jakecache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _eventHandlers = Symbol('eventHandlers') 4 | 5 | let CustomEvent = window.CustomEvent 6 | let DOMException = window.DOMException 7 | let ErrorEvent = window.ErrorEvent 8 | let ProgressEvent = window.ProgressEvent 9 | 10 | class PolyfilledEventTarget { 11 | constructor (names) { 12 | this[_eventHandlers] = {} 13 | 14 | names.map(name => { 15 | this[_eventHandlers][name] = { handler: null, listeners: [] } 16 | Object.defineProperty(this, 'on' + name, { 17 | get: function () { 18 | return this[_eventHandlers][name]['handler'] 19 | }, 20 | set: function (fn) { 21 | if (fn === null || fn instanceof Function) { 22 | this[_eventHandlers][name]['handler'] = fn 23 | } 24 | }, 25 | enumerable: false 26 | }) 27 | }) 28 | } 29 | 30 | dispatchEvent (event) { 31 | if (this[_eventHandlers][event.type]) { 32 | let handlers = this[_eventHandlers][event.type] 33 | let mainFn = handlers['handler'] 34 | if (mainFn) { 35 | mainFn(event) 36 | } 37 | for (let fn of handlers['listeners']) { 38 | fn(event) 39 | } 40 | } 41 | } 42 | 43 | addEventListener (name, fn) { 44 | if (this[_eventHandlers][name]) { 45 | let store = this[_eventHandlers][name]['listeners'] 46 | let index = store.indexOf(fn) 47 | if (index === -1) { 48 | store.push(fn) 49 | } 50 | } 51 | } 52 | 53 | removeEventListener (name, fn) { 54 | if (this[_eventHandlers][name]) { 55 | let store = this[_eventHandlers][name]['listeners'] 56 | let index = store.indexOf(fn) 57 | if (index > 0) { 58 | store.splice(index, 1) 59 | } 60 | } 61 | } 62 | } 63 | 64 | const _status = Symbol('status') 65 | 66 | class JakeCache extends PolyfilledEventTarget { 67 | constructor () { 68 | super(['abort', 'cached', 'checking', 69 | 'downloading', 'error', 'obsolete', 70 | 'progress', 'updateready', 'noupdate']) 71 | 72 | if (window.jakeCache) { 73 | return window.jakeCache 74 | } 75 | window.jakeCache = this 76 | 77 | if (('serviceWorker' in navigator) === false) { 78 | return 79 | } 80 | 81 | let onload = () => { 82 | if (document.readyState !== 'complete') { 83 | return 84 | } 85 | 86 | let html = document.querySelector('html') 87 | this.pathname = html.getAttribute('manifest') 88 | 89 | if (this.pathname && 'serviceWorker' in navigator) { 90 | navigator.serviceWorker.register('jakecache-sw.js').then(registration => { 91 | console.log(`JakeCache installed for ${registration.scope}`) 92 | 93 | if (registration.active) { 94 | // Check whether we have a cache, or cache it (no reload enforced). 95 | console.log('cache check') 96 | registration.active.postMessage({ 97 | command: 'update', 98 | pathname: this.pathname 99 | }) 100 | } 101 | }).catch(err => { 102 | console.log(`JakeCache installation failed: ${err}`) 103 | }) 104 | } 105 | } 106 | 107 | if (document.readyState === 'complete') { 108 | onload() 109 | } else { 110 | document.onreadystatechange = onload 111 | } 112 | 113 | this[_status] = this.UNCACHED 114 | 115 | navigator.serviceWorker.addEventListener('message', event => { 116 | switch (event.data.type) { 117 | case 'abort': 118 | this.dispatchEvent(new CustomEvent('abort')) 119 | break 120 | case 'idle': 121 | this[_status] = this.IDLE 122 | break 123 | case 'checking': 124 | this[_status] = this.CHECKING 125 | this.dispatchEvent(new CustomEvent('checking')) 126 | break 127 | case 'cached': 128 | this[_status] = this.IDLE 129 | this.dispatchEvent(new CustomEvent('cached')) 130 | break 131 | case 'downloading': 132 | this[_status] = this.DOWNLOADING 133 | this.dispatchEvent(new CustomEvent('downloading')) 134 | break 135 | case 'updateready': 136 | this[_status] = this.UPDATEREADY 137 | this.dispatchEvent(new CustomEvent('updateready')) 138 | break 139 | case 'noupdate': 140 | this[_status] = this.IDLE 141 | this.dispatchEvent(new CustomEvent('noupdate')) 142 | break 143 | case 'progress': 144 | this.dispatchEvent(new ProgressEvent('progress', event.data)) 145 | break 146 | case 'obsolete': 147 | this[_status] = this.OBSOLETE 148 | this.dispatchEvent(new CustomEvent('obsolete')) 149 | break 150 | case 'error': 151 | this.dispatchEvent(new ErrorEvent('error', event.data)) 152 | break 153 | } 154 | }) 155 | } 156 | 157 | get UNCACHED () { return 0 } 158 | get IDLE () { return 1 } 159 | get CHECKING () { return 2 } 160 | get DOWNLOADING () { return 3 } 161 | get UPDATEREADY () { return 4 } 162 | get OBSOLETE () { return 5 } 163 | 164 | get status () { 165 | return this[_status] 166 | } 167 | 168 | update () { 169 | if (false) {} 170 | 171 | navigator.serviceWorker.controller.postMessage({ 172 | command: 'update', 173 | pathname: this.pathname, 174 | options: { 175 | cache: 'reload' 176 | } 177 | }) 178 | } 179 | 180 | abort () { 181 | if (this.status === this.DOWNLOADING) { 182 | navigator.serviceWorker.controller.postMessage({ 183 | command: 'abort' 184 | }) 185 | } 186 | } 187 | 188 | swapCache () { 189 | if (this.status !== this.UPDATEREADY) { 190 | throw new DOMException(DOMException.INVALID_STATE_ERR, 191 | 'there is no newer application cache to swap to.') 192 | } 193 | navigator.serviceWorker.controller.postMessage({ 194 | command: 'swapCache' 195 | }) 196 | } 197 | } 198 | 199 | new JakeCache() -------------------------------------------------------------------------------- /example/appcache.manifest: -------------------------------------------------------------------------------- 1 | CACHE MANIFEST 2 | # 2010-06-18:v2 3 | 4 | # Explicitly cached 'master entries'. 5 | CACHE: 6 | /favicon.ico 7 | index.html 8 | stylesheet.css 9 | images/logo.png 10 | scripts/main.js 11 | 12 | # Resources that require the user to be online. 13 | NETWORK: 14 | * 15 | 16 | # static.html will be served if main.py is inaccessible 17 | # offline.jpg will be served in place of all images in images/large/ 18 | # offline.html will be served in place of all other .html files 19 | FALLBACK: 20 | /main.py /static.html 21 | images/large/ images/offline.jpg -------------------------------------------------------------------------------- /example/test.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |