├── .gitignore ├── .travis.yml ├── README.md ├── design ├── logo-export.svg ├── logo.ai ├── logo.psd └── logo.svg ├── gulpfile.js ├── package-lock.json ├── package.json └── src ├── base.html ├── css ├── _components.scss ├── _global.scss ├── _layout.scss ├── _page-specific.scss ├── _utils.scss └── all.scss ├── data.json ├── demos ├── claim │ ├── index.html │ └── sw.js ├── clients-count │ ├── index.html │ └── sw.js ├── fetch │ ├── index.html │ ├── json.json │ └── sw.js ├── fetchevent │ ├── index.html │ └── sw.js ├── force-reload-loop │ ├── index.html │ └── sw.js ├── forward-requests │ ├── index.html │ └── sw.js ├── gif-stream │ ├── babel.html │ ├── buffer.html │ ├── cat.mpg │ ├── gif.html │ ├── gif.js │ ├── index.html │ ├── jsmpeg-async.js │ ├── jsmpeg-babel.js │ ├── jsmpeg-stream.js │ ├── jsmpeg.js │ ├── regenerator-runtime.js │ ├── stream.html │ └── sw.js ├── globalapis │ ├── index.html │ └── sw.js ├── headers-log │ ├── index.html │ └── sw.js ├── http-redirect │ ├── blah │ │ └── index.html │ ├── index.html │ └── sw.js ├── img-rewrite │ ├── index.html │ └── sw.js ├── install-fail │ ├── error-thrown-oninstall │ │ ├── index.html │ │ └── sw.js │ ├── execution-error │ │ ├── index.html │ │ └── sw.js │ └── rejected-promise │ │ ├── index.html │ │ └── sw.js ├── installactivate │ ├── index.html │ └── sw.js ├── json-stream │ ├── index.html │ ├── photos.json │ └── photos.sjson ├── manual-response │ ├── index.html │ └── sw.js ├── nav-preload │ ├── imgs.html │ ├── index.html │ ├── styles.css │ └── sw.js ├── navigator.serviceWorker │ └── index.html ├── page-cache-bug │ ├── index.html │ └── sw.js ├── postMessage │ ├── index.html │ └── sw.js ├── redirect │ ├── destination │ │ └── index.html │ ├── index.html │ └── sw.js ├── registerunregister │ ├── index.html │ └── sw.js ├── scope │ ├── app-a │ │ ├── index.html │ │ └── sw.js │ ├── app-b-sw.js │ ├── app-b │ │ └── index.html │ ├── index.html │ └── sw.js ├── simple-stream │ ├── index.html │ └── sw.js ├── slow-update │ ├── index.html │ └── sw.js ├── sync │ ├── index.html │ └── sw.js ├── template-stream │ ├── index.html │ ├── prism.js │ ├── styles.css │ └── sw.js └── transform-stream │ ├── cloud.html │ ├── edge-cases.md │ ├── index.html │ └── sw.js ├── img ├── chrome-canary.png ├── chrome.png ├── edge.png ├── firefox-nightly.png ├── firefox.png ├── opera-developer.png ├── opera.png ├── safari.png ├── samsung-internet.png └── webkit.png ├── index.html ├── masthead.html └── resources.html /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | build 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # For more information about the configurations used 2 | # in this file, please see the Travis CI documentation: 3 | # http://docs.travis-ci.com 4 | 5 | deploy: 6 | provider: pages 7 | skip_cleanup: true 8 | github_token: $GH_TOKEN 9 | local_dir: build 10 | on: 11 | branch: master 12 | 13 | env: 14 | global: 15 | - secure: "aFvIDrSx6T91kHM7JHJvwdU90eVZ/XIo3mztoDPHW2eysHomdwwE17ShEEQahOVTslOqjt88U7R8vcAoVliEie64oP/wK29To38Pe3N3YLvGeTfxYKuDhbUmm5U+16qTmgFTjlVbQXCMMH4HSjcPujIzX3wbd/vuF1mkj/cY8Z0=" 16 | 17 | git: 18 | depth: 10 19 | 20 | language: node_js 21 | 22 | node_js: 23 | - "stable" 24 | 25 | script: npm run build 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Is Service Worker Ready Yet? 2 | 3 | [![Build Status](https://travis-ci.org/jakearchibald/isserviceworkerready.svg)](https://travis-ci.org/jakearchibald/isserviceworkerready) 4 | [![devDependency Status](https://david-dm.org/jakearchibald/isserviceworkerready/dev-status.svg)](https://david-dm.org/jakearchibald/isserviceworkerready#info=devDependencies) 5 | 6 | Tracks the features of service worker supported in browsers. 7 | [View the site](https://jakearchibald.github.io/isserviceworkerready). 8 | 9 | ## Run locally 10 | 11 | To install, run the following in the root of your cloned copy of the repo: 12 | 13 | ```sh 14 | npm install 15 | ``` 16 | 17 | To serve the site on `localhost:8000`: 18 | 19 | ```sh 20 | npm run serve 21 | ``` 22 | 23 | To build the site: 24 | 25 | ```sh 26 | npm run build 27 | ``` 28 | 29 | ## Contribute 30 | 31 | To update data, edit [`data.json`](src/data.json), which is in this format: 32 | 33 | ```js 34 | 35 | //... 36 | 37 | "features": [ 38 | 39 | //... 40 | 41 | { 42 | "name", "Feature name or interface.whatever", 43 | "description", "Brief feature details, html allowed", 44 | "chrome": { 45 | // 1 = supported 46 | // 0.5 = supported with caveats (eg flags, nightlies, special builds) 47 | // 0 = not supported 48 | "supported": 1 49 | // (optional) browser version 50 | "minVersion": 35, 51 | // (optional) alternate icon, currently supports: 52 | // "chrome-canary" 53 | // "firefox-nightly" 54 | // "webkit" 55 | // "opera-developer" 56 | "icon": "canary", 57 | // (optional) details, cavats, links to tickets, flags etc 58 | "details": [ 59 | "Requires Chrome Canary" 60 | ] 61 | }, 62 | "firefox": {}, 63 | "opera": {}, 64 | "safari": {}, 65 | // (optional) details that don't apply to a single browser 66 | "details": [ 67 | "Chrome & Firefox: sitting in a tree K-I-S-S-I-N-G" 68 | ] 69 | }, 70 | 71 | // ... 72 | 73 | ] 74 | ``` 75 | -------------------------------------------------------------------------------- /design/logo-export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /design/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/design/logo.ai -------------------------------------------------------------------------------- /design/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/design/logo.psd -------------------------------------------------------------------------------- /design/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | is 3 | SERVICEWORKER 4 | ready 5 | ? 6 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var del = require('del'); 4 | var browserSync = require('browser-sync'); 5 | var gulp = require('gulp'); 6 | var plugins = require('gulp-load-plugins')(); 7 | var runSequence = require('run-sequence'); // Temporary solution until Gulp 4 8 | // https://github.com/gulpjs/gulp/issues/355 9 | 10 | var reload = browserSync.reload; 11 | 12 | // --------------------------------------------------------------------- 13 | // | Helper tasks | 14 | // --------------------------------------------------------------------- 15 | 16 | gulp.task('clean', function () { 17 | return del(['build']); 18 | }); 19 | 20 | gulp.task('copy', [ 21 | 'copy:css', 22 | 'copy:html', 23 | 'copy:misc' 24 | ]); 25 | 26 | gulp.task('copy:css', function () { 27 | return gulp.src('src/css/all.scss') 28 | .pipe(plugins.sass({ style: 'compressed' })) 29 | .pipe(gulp.dest('build/css')) 30 | .pipe(plugins.filter('**/*.css')) 31 | .pipe(reload({stream: true})); 32 | }); 33 | 34 | gulp.task('copy:html', function () { 35 | return gulp.src([ 36 | 37 | // Copy all `.html` files 38 | 'src/*.html', 39 | 40 | // Exclude the following files since they 41 | // are only used to build the other files 42 | '!src/masthead.html', 43 | '!src/base.html' 44 | 45 | ]).pipe(plugins.swig({ 46 | defaults: { cache: false }, 47 | data: JSON.parse(fs.readFileSync("./src/data.json")) 48 | })).pipe(plugins.htmlmin({ 49 | 50 | // In-depth information about the options: 51 | // https://github.com/kangax/html-minifier#options-quick-reference 52 | 53 | collapseBooleanAttributes: true, 54 | collapseWhitespace: true, 55 | minifyJS: true, 56 | removeAttributeQuotes: true, 57 | removeComments: true, 58 | removeEmptyAttributes: true, 59 | removeOptionalTags: true, 60 | removeRedundantAttributes: true, 61 | 62 | // Prevent html-minifier from breaking the SVGs 63 | // https://github.com/kangax/html-minifier/issues/285 64 | keepClosingSlash: true, 65 | caseSensitive: true 66 | 67 | })).pipe(gulp.dest('build')).pipe(reload({stream: true})); 68 | }); 69 | 70 | gulp.task('copy:misc', function () { 71 | return gulp.src([ 72 | 73 | // Copy all files 74 | 'src/**', 75 | 76 | // Exclude the following files 77 | // (other tasks will handle the copying of these files) 78 | '!src/*.html', 79 | '!src/{css,css/**}', 80 | '!src/data.json' 81 | 82 | ], { 83 | // Include hidden files by default 84 | dot: true 85 | }).pipe(gulp.dest('build')); 86 | 87 | }); 88 | 89 | gulp.task('browser-sync', function() { 90 | browserSync({ 91 | 92 | // In-depth information about the options: 93 | // http://www.browsersync.io/docs/options/ 94 | 95 | notify: false, 96 | port: 8000, 97 | server: "build", 98 | open: false 99 | }); 100 | }); 101 | 102 | gulp.task('watch', function () { 103 | gulp.watch(['src/**/*.scss'], ['copy:css']); 104 | gulp.watch(['src/*.html', 'src/data.json'], ['copy:html', reload]); 105 | gulp.watch(['src/img/**', 'src/demos/**'], ['copy:misc']); 106 | }); 107 | 108 | // --------------------------------------------------------------------- 109 | // | Main tasks | 110 | // --------------------------------------------------------------------- 111 | 112 | gulp.task('build', function (done) { 113 | runSequence('clean', 'copy', done); 114 | }); 115 | 116 | gulp.task('default', ['build']); 117 | 118 | gulp.task('serve', function (done) { 119 | runSequence('build', ['browser-sync', 'watch'], done); 120 | }); 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "browser-sync": "^2.24.1", 4 | "del": "^3.0.0", 5 | "gulp": "^3.8.10", 6 | "gulp-filter": "^5.1.0", 7 | "gulp-htmlmin": "^4.0.0", 8 | "gulp-load-plugins": "^1.5.0", 9 | "gulp-sass": "^4.0.1", 10 | "gulp-swig": "^0.8.0", 11 | "run-sequence": "^2.2.1" 12 | }, 13 | "private": true, 14 | "scripts": { 15 | "build": "gulp build", 16 | "serve": "gulp serve" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %} 5 | 6 | 18 | 19 | 20 | 21 | {% block head %}{% endblock %} 22 | 23 | 24 | {% block body %}{% endblock %} 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/css/_components.scss: -------------------------------------------------------------------------------- 1 | .page-title { 2 | position: absolute; 3 | left: 0; 4 | top: -5000px; 5 | } 6 | 7 | .hero { 8 | height: calc(100vh - 2em - 60px); 9 | display: flex; 10 | justify-content: space-between; 11 | flex-direction: column; 12 | margin-bottom: 1em; 13 | padding-top: 1rem; 14 | @media (min-width: 500px) { 15 | padding-top: 3rem; 16 | height: calc(100vh - 3em - 2em); 17 | } 18 | 19 | @media (min-width: 800px) { 20 | padding-top: 4.5rem; 21 | height: calc(100vh - 4.5em - 2em); 22 | } 23 | 24 | @media (min-width: 720px) { 25 | padding-top: 125px; 26 | height: calc(100vh - 125px - 2em); 27 | } 28 | } 29 | 30 | .logo { 31 | max-width: 947px; 32 | fill: #FFF; 33 | display: block; 34 | margin: 0 auto 0; 35 | position: relative; 36 | padding-top: 18.98%; 37 | 38 | & svg { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 100%; 44 | margin: 0 auto; 45 | } 46 | } 47 | 48 | .answer { 49 | font-family: "Press Start 2P"; 50 | text-align: center; 51 | color: #fff; 52 | margin: 0 auto; 53 | display: block; 54 | font-size: 5em; 55 | text-shadow: 5px 5px 0px rgba(0, 0, 0, 0.5); 56 | } 57 | 58 | .external-links { 59 | font-family: "Press Start 2P"; 60 | margin: 0.8rem 0 1.1rem; 61 | padding: 0; 62 | text-align: center; 63 | color: #fff; 64 | 65 | @media (min-width: 500px) { 66 | margin: 0.8rem 0 3.2rem; 67 | } 68 | 69 | @media (min-width: 800px) { 70 | margin: 0.8rem 0 4.4rem; 71 | } 72 | 73 | & > li { 74 | margin: 0 10px; 75 | display: inline-block; 76 | 77 | @media (min-width: 360px) { 78 | margin: 0; 79 | 80 | &::after { 81 | content: '*'; 82 | margin: 0 1rem; 83 | } 84 | 85 | &:last-child::after { 86 | content: none; 87 | } 88 | } 89 | } 90 | 91 | & a:link, 92 | & a:visited { 93 | color: #71B5FF; 94 | } 95 | & a:hover, 96 | & a:active { 97 | text-decoration: underline; 98 | } 99 | } 100 | 101 | .details-link { 102 | display: block; 103 | width: 100%; 104 | color: #fff; 105 | text-align: center; 106 | svg { 107 | height: 10vh; 108 | width: 10vh; 109 | } 110 | } 111 | 112 | %box-heading { 113 | background: #2A609B; 114 | line-height: 1; 115 | padding: 0.4rem 1rem; 116 | font-weight: normal; 117 | font-size: 1rem; 118 | color: #fff; 119 | margin: 0 -1rem; 120 | overflow: hidden; 121 | text-overflow: ellipsis; 122 | } 123 | 124 | .feature { 125 | margin: 0 auto; 126 | margin-bottom: 1rem; 127 | line-height: 1.2; 128 | max-width: 510px; 129 | 130 | @media (min-width: 800px) { 131 | @include display-flex; 132 | @include flex-flow(wrap); 133 | @include justify-content(flex-end); 134 | max-width: 947px; 135 | } 136 | 137 | &:target > header { 138 | background: #FFECA9; 139 | } 140 | 141 | & > header { 142 | background: #fff; 143 | overflow: hidden; 144 | padding: 0 1rem; 145 | 146 | @media (min-width: 800px) { 147 | width: 35%; 148 | box-sizing: border-box; 149 | } 150 | 151 | & p { 152 | margin: 0.7rem 0; 153 | } 154 | } 155 | 156 | & .feature-name { 157 | @extend %box-heading; 158 | position: relative; 159 | } 160 | 161 | & .perma { 162 | &:link, 163 | &:visited { 164 | color: #fff; 165 | } 166 | &:hover, 167 | &:active { 168 | text-decoration: none; 169 | & .feature-name::after { 170 | content: '¶'; 171 | position: absolute; 172 | top: 0.3rem; 173 | right: 0.3rem; 174 | color: rgba(255, 255, 255, 0.53); 175 | } 176 | } 177 | } 178 | } 179 | 180 | .feature > header.no-background { 181 | background: none; 182 | } 183 | 184 | .results { 185 | @include display-flex; 186 | background: #fff; 187 | 188 | :target & { 189 | background: #FFECA9; 190 | } 191 | 192 | @media (min-width: 800px) { 193 | width: 65%; 194 | } 195 | 196 | & .result { 197 | @include flex(1); 198 | } 199 | } 200 | 201 | .result { 202 | margin: 0 0.4rem 0.4rem; 203 | margin-left: 0; 204 | overflow: hidden; 205 | position: relative; 206 | 207 | @media (min-width: 800px) { 208 | margin-top: 0.4rem; 209 | } 210 | 211 | &:first-child { 212 | margin-left: 0.4rem; 213 | } 214 | 215 | &::after { 216 | display: block; 217 | content: ''; 218 | padding-top: 100%; 219 | } 220 | 221 | &.yes { 222 | background: #376D37; 223 | } 224 | 225 | &.ish { 226 | background: #D5BB00; 227 | } 228 | 229 | &.no { 230 | background: #CCC; 231 | & .icon { 232 | opacity: 0.2; 233 | -webkit-filter: grayscale(100%); 234 | 235 | @supports (-webkit-filter: grayscale(100%)) { 236 | opacity: 0.4; 237 | } 238 | } 239 | } 240 | 241 | & .support { 242 | margin: 0; 243 | text-indent: -5000px; 244 | position: absolute; 245 | top: 0; 246 | left: 0; 247 | bottom: 0; 248 | right: 0; 249 | } 250 | 251 | & .version { 252 | position: absolute; 253 | bottom: 0; 254 | right: 0; 255 | background: rgba(255, 255, 255, 0.8); 256 | line-height: 1; 257 | color: #000; 258 | text-indent: 0; 259 | padding: 0.1rem 0.2rem; 260 | font-size: 0.8rem; 261 | 262 | &::after { 263 | content: '+'; 264 | } 265 | 266 | @media (min-width: 578px) { 267 | border-radius: 4px; 268 | bottom: 5%; 269 | right: 5%; 270 | padding: 0; 271 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.3); 272 | @include display-flex; 273 | @include justify-content(center); 274 | @include align-items(center); 275 | 276 | width: 50%; 277 | height: 30%; 278 | } 279 | } 280 | 281 | & .icon { 282 | text-indent: -5000px; 283 | position: absolute; 284 | top: 0; 285 | right: 0; 286 | bottom: 0; 287 | left: 0; 288 | } 289 | } 290 | 291 | .icon { 292 | margin: 0; 293 | background-size: 78%; 294 | background-repeat: no-repeat; 295 | background-position: center center; 296 | 297 | @each $icon in chrome, chrome-canary, firefox, firefox-nightly, edge, opera, opera-developer, samsung-internet, safari, webkit { 298 | &.#{$icon} { 299 | background-image: url(../img/#{$icon}.png); 300 | } 301 | } 302 | } 303 | 304 | .details { 305 | margin: 0; 306 | padding: 0; 307 | font-size: 0.9rem; 308 | 309 | @media (min-width: 800px) { 310 | width: 65%; 311 | box-sizing: border-box; 312 | } 313 | 314 | & > li { 315 | display: block; 316 | background: #E4E4E4; 317 | margin: 0; 318 | padding: 0.4rem 1rem; 319 | 320 | &:nth-child(even) { 321 | background: #F1F1F1; 322 | } 323 | 324 | &:first-child { 325 | border-top: solid 1px #CFCFCF; 326 | } 327 | 328 | } 329 | } 330 | 331 | .outro { 332 | font-family: "Press Start 2P"; 333 | margin: 2.2rem 0; 334 | text-align: center; 335 | color: #fff; 336 | } 337 | -------------------------------------------------------------------------------- /src/css/_global.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | font-size: 15px; 4 | line-height: 1.4; 5 | min-height: 100%; 6 | background: #102a48; 7 | background: #102a48 radial-gradient(circle closest-corner at 50% 63px, #166ab9, #013668, #102a48) no-repeat; 8 | overflow-y: scroll; 9 | 10 | -webkit-text-size-adjust: 100%; 11 | 12 | @media (min-width: 500px) { 13 | background: #102a48 radial-gradient(circle closest-corner at 50% 126px, #166ab9, #013668, #102a48) no-repeat; 14 | font-size: 17px; 15 | } 16 | 17 | @media (min-width: 720px) { 18 | background: #102a48 radial-gradient(circle closest-corner at 50% 256px, #166ab9, #013668, #102a48) no-repeat; 19 | } 20 | } 21 | 22 | body { 23 | margin: 1rem; 24 | @media (min-width: 500px) { 25 | margin: 1.8rem; 26 | } 27 | } 28 | 29 | a { 30 | &:link, 31 | &:visited { 32 | color: #1F6DC2; 33 | text-decoration: none; 34 | } 35 | &:hover, 36 | &:active { 37 | color: #007AFF; 38 | text-decoration: underline; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/css/_layout.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/css/_layout.scss -------------------------------------------------------------------------------- /src/css/_page-specific.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/css/_page-specific.scss -------------------------------------------------------------------------------- /src/css/_utils.scss: -------------------------------------------------------------------------------- 1 | @mixin placeholder { 2 | &::-webkit-input-placeholder, 3 | &::-moz-placeholder, 4 | &:-ms-input-placeholder { 5 | @content; 6 | } 7 | } 8 | 9 | // transition & animation 10 | $easeInQuad : cubic-bezier(0.550, 0.085, 0.680, 0.530); 11 | $easeInCubic : cubic-bezier(0.550, 0.055, 0.675, 0.190); 12 | $easeInQuart : cubic-bezier(0.895, 0.030, 0.685, 0.220); 13 | $easeInQuint : cubic-bezier(0.755, 0.050, 0.855, 0.060); 14 | $easeInSine : cubic-bezier(0.470, 0.000, 0.745, 0.715); 15 | $easeInExpo : cubic-bezier(0.950, 0.050, 0.795, 0.035); 16 | $easeInCirc : cubic-bezier(0.600, 0.040, 0.980, 0.335); 17 | $easeInBack : cubic-bezier(0.600, -0.280, 0.735, 0.045); 18 | 19 | $easeOutQuad : cubic-bezier(0.250, 0.460, 0.450, 0.940); 20 | $easeOutCubic : cubic-bezier(0.215, 0.610, 0.355, 1.000); 21 | $easeOutQuart : cubic-bezier(0.165, 0.840, 0.440, 1.000); 22 | $easeOutQuint : cubic-bezier(0.230, 1.000, 0.320, 1.000); 23 | $easeOutSine : cubic-bezier(0.390, 0.575, 0.565, 1.000); 24 | $easeOutExpo : cubic-bezier(0.190, 1.000, 0.220, 1.000); 25 | $easeOutCirc : cubic-bezier(0.075, 0.820, 0.165, 1.000); 26 | $easeOutBack : cubic-bezier(0.175, 0.885, 0.320, 1.275); 27 | 28 | $easeInOutQuad : cubic-bezier(0.455, 0.030, 0.515, 0.955); 29 | $easeInOutCubic : cubic-bezier(0.645, 0.045, 0.355, 1.000); 30 | $easeInOutQuart : cubic-bezier(0.770, 0.000, 0.175, 1.000); 31 | $easeInOutQuint : cubic-bezier(0.860, 0.000, 0.070, 1.000); 32 | $easeInOutSine : cubic-bezier(0.445, 0.050, 0.550, 0.950); 33 | $easeInOutExpo : cubic-bezier(1.000, 0.000, 0.000, 1.000); 34 | $easeInOutCirc : cubic-bezier(0.785, 0.135, 0.150, 0.860); 35 | $easeInOutBack : cubic-bezier(0.680, -0.550, 0.265, 1.550); 36 | 37 | @mixin transition($spec...) { 38 | @each $prefix in -webkit-, '' { 39 | #{$prefix}transition: $spec; 40 | } 41 | } 42 | 43 | @mixin transition-property($spec...) { 44 | @each $prefix in -webkit-, '' { 45 | #{$prefix}transition-property: $spec; 46 | } 47 | } 48 | 49 | @mixin animation($type) { 50 | @each $prefix in -webkit-, '' { 51 | #{$prefix}animation: $type; 52 | } 53 | } 54 | 55 | @mixin keyframes($name) { 56 | @-webkit-keyframes $name { @content; } 57 | @keyframes $name { @content; } 58 | } 59 | 60 | @mixin transform($spec...) { 61 | @each $prefix in -webkit-, '' { 62 | #{$prefix}transform: $spec; 63 | } 64 | } 65 | 66 | // flexbox 67 | @mixin display-flex { 68 | @each $prefix in -webkit-, '' { 69 | display: #{$prefix}flex; 70 | } 71 | } 72 | @mixin flex-direction($spec...) { 73 | @each $prefix in -webkit-, '' { 74 | #{$prefix}flex-direction: $spec; 75 | } 76 | } 77 | @mixin justify-content($spec...) { 78 | @each $prefix in -webkit-, '' { 79 | #{$prefix}justify-content: $spec; 80 | } 81 | } 82 | @mixin flex($spec...) { 83 | @each $prefix in -webkit-, '' { 84 | #{$prefix}flex: $spec; 85 | } 86 | } 87 | @mixin flex-flow($spec...) { 88 | @each $prefix in -webkit-, '' { 89 | #{$prefix}flex-flow: $spec; 90 | } 91 | } 92 | @mixin align-items($spec...) { 93 | @each $prefix in -webkit-, '' { 94 | #{$prefix}align-items: $spec; 95 | } 96 | } 97 | @mixin max-height-max-content { 98 | @each $prefix in -webkit-, -moz-, '' { 99 | max-height: #{$prefix}max-content; 100 | } 101 | } 102 | @mixin max-height-min-content { 103 | @each $prefix in -webkit-, -moz-, '' { 104 | max-height: #{$prefix}max-content; 105 | } 106 | } 107 | @mixin min-height-min-content { 108 | @each $prefix in -webkit-, -moz-, '' { 109 | min-height: #{$prefix}min-content; 110 | } 111 | } 112 | @mixin min-width-min-content { 113 | @each $prefix in -webkit-, -moz-, '' { 114 | min-width: #{$prefix}min-content; 115 | } 116 | } -------------------------------------------------------------------------------- /src/css/all.scss: -------------------------------------------------------------------------------- 1 | @import 'utils'; 2 | @import 'global'; 3 | @import 'layout'; 4 | @import 'components'; 5 | @import 'page-specific'; -------------------------------------------------------------------------------- /src/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "browsers": [ 3 | { 4 | "id": "chrome", 5 | "name": "Chrome" 6 | }, 7 | { 8 | "id": "firefox", 9 | "name": "Firefox" 10 | }, 11 | { 12 | "id": "opera", 13 | "name": "Opera" 14 | }, 15 | { 16 | "id": "samsung-internet", 17 | "name": "Samsung Internet" 18 | }, 19 | { 20 | "id": "safari", 21 | "name": "Safari" 22 | }, 23 | { 24 | "id": "edge", 25 | "name": "Edge" 26 | } 27 | ], 28 | "features": [ 29 | { 30 | "name": "Service worker enthusiasm", 31 | "description": "The first thing any implementation needs.", 32 | "chrome": { 33 | "supported": 1, 34 | "details": [ 35 | "Shipped." 36 | ] 37 | }, 38 | "firefox": { 39 | "supported": 1, 40 | "details": [ 41 | "Shipped." 42 | ] 43 | }, 44 | "opera": { 45 | "supported": 1 46 | }, 47 | "samsung-internet": { 48 | "supported": 1, 49 | "details": [ 50 | "Shipped. Based on Chromium 44.2403 with some additions and changes. (See \"Service Worker\" section.)" 51 | ] 52 | }, 53 | "safari": { 54 | "supported": 1, 55 | "details": [ 56 | "Shipped." 57 | ] 58 | }, 59 | "edge": { 60 | "supported": 1, 61 | "details": [ 62 | "Shipped." 63 | ] 64 | }, 65 | "details": [ 66 | "Support does not include iOS versions of third-party browsers on that platform (see Safari support)." 67 | ] 68 | }, 69 | { 70 | "name": "Promises", 71 | "description": "Not service worker-specific, but required by service worker. Spec.", 72 | "chrome": { 73 | "supported": 1, 74 | "minVersion": 36 75 | }, 76 | "firefox": { 77 | "supported": 1, 78 | "minVersion": 29 79 | }, 80 | "opera": { 81 | "supported": 1, 82 | "minVersion": 23 83 | }, 84 | "samsung-internet": { 85 | "supported": 1, 86 | "minVersion": 2.0 87 | }, 88 | "safari": { 89 | "minVersion": 9, 90 | "supported": 1 91 | }, 92 | "edge": { 93 | "icon": "edge", 94 | "minVersion": 13, 95 | "supported": 1 96 | } 97 | }, 98 | { 99 | "name": "Debugging", 100 | "description": "State of debugging tools.", 101 | "chrome": { 102 | "supported": 1, 103 | "minVersion": 40, 104 | "details": [ 105 | "You can debug service worker scripts as any other. \"Application\" panel in devtools has service worker & cache sections." 106 | ] 107 | }, 108 | "firefox": { 109 | "supported": 1, 110 | "minVersion": 47, 111 | "details": [ 112 | "Debuggable from the \"Workers\" page in about:debugging.", 113 | "Web Console can display console messages from service workers.", 114 | "about:serviceworkers has some under-the-hood stuff." 115 | ] 116 | }, 117 | "opera": { 118 | "supported": 1, 119 | "details": [ 120 | "Debuggable from the resources panel in Opera developer if you enable super-experimental devtools.", 121 | "Console messages from the service worker appear in the pages' console in Opera stable." 122 | ] 123 | }, 124 | "samsung-internet": { 125 | "supported": 1 126 | }, 127 | "safari": { 128 | "supported": 1, 129 | "minVersion": 11.1, 130 | "details": [ 131 | "See 'service workers' in the 'develop' menu to open an inspector for a particular service worker." 132 | ] 133 | }, 134 | "edge": { 135 | "minVersion": 17, 136 | "supported": 1, 137 | "details": [ 138 | "See the service worker section in the sources panel." 139 | ] 140 | }, 141 | "details": [ 142 | "Chrome & Opera: Debuggable from the resources panel in Chrome Canary and Opera developer if you enable super-experimental devtools.", 143 | "Chrome & Opera: Console messages from the service worker appear in the pages' console.", 144 | "Chrome & Opera & Samsung Internet: chrome://serviceworker-internals resp. browser://serviceworker-internals (in Opera developer) has some under-the-hood stuff." 145 | ] 146 | }, 147 | { 148 | "name": "navigator.serviceWorker", 149 | "description": "Namespace for page-side service worker API. Spec. Test.", 150 | "chrome": { 151 | "supported": 1, 152 | "minVersion": 40 153 | }, 154 | "firefox": { 155 | "supported": 1, 156 | "minVersion": 44 157 | }, 158 | "opera": { 159 | "supported": 1, 160 | "minVersion": 27 161 | }, 162 | "samsung-internet": { 163 | "supported": 1, 164 | "minVersion": 4.0 165 | }, 166 | "safari": { 167 | "supported": 1, 168 | "minVersion": 11.1 169 | }, 170 | "edge": { 171 | "supported": 1, 172 | "minVersion": 17 173 | }, 174 | "details": [ 175 | ] 176 | }, 177 | { 178 | "name": "Register / unregister", 179 | "description": "Register for a SW and get a registration instance back, unregister undoes. Spec. Test.", 180 | "chrome": { 181 | "supported": 1, 182 | "minVersion": 40 183 | }, 184 | "firefox": { 185 | "supported": 1, 186 | "minVersion": 44 187 | }, 188 | "opera": { 189 | "supported": 1, 190 | "minVersion": 27 191 | }, 192 | "samsung-internet": { 193 | "supported": 1, 194 | "minVersion": 4.0 195 | }, 196 | "safari": { 197 | "supported": 1, 198 | "minVersion": 11.1 199 | }, 200 | "edge": { 201 | "supported": 1, 202 | "minVersion": 17 203 | }, 204 | "details": [ 205 | ] 206 | }, 207 | { 208 | "name": "postMessage to & from worker", 209 | "description": "Spec. Test.", 210 | "chrome": { 211 | "supported": 1, 212 | "minVersion": 45 213 | }, 214 | "firefox": { 215 | "supported": 1, 216 | "minVersion": 44 217 | }, 218 | "opera": { 219 | "supported": 1, 220 | "minVersion": 32 221 | }, 222 | "samsung-internet": { 223 | "supported": 1, 224 | "minVersion": 4.0 225 | }, 226 | "safari": { 227 | "supported": 1, 228 | "minVersion": 11.1 229 | }, 230 | "edge": { 231 | "supported": 1, 232 | "minVersion": 17 233 | }, 234 | "details": [ 235 | ] 236 | }, 237 | { 238 | "name": "Fetch event", 239 | "description": "Fires for pages and all sub-resources. Spec. Test.", 240 | "chrome": { 241 | "supported": 1, 242 | "minVersion": 40 243 | }, 244 | "firefox": { 245 | "supported": 1, 246 | "minVersion": 44 247 | }, 248 | "opera": { 249 | "supported": 1, 250 | "minVersion": 27 251 | }, 252 | "samsung-internet": { 253 | "supported": 1, 254 | "minVersion": 4.0 255 | }, 256 | "safari": { 257 | "supported": 1, 258 | "minVersion": 11.1 259 | }, 260 | "edge": { 261 | "supported": 1, 262 | "minVersion": 17 263 | } 264 | }, 265 | { 266 | "name": "fetchEvent.request", 267 | "description": "Spec. Test.", 268 | "chrome": { 269 | "supported": 1, 270 | "minVersion": 40 271 | }, 272 | "firefox": { 273 | "supported": 1, 274 | "minVersion": 44 275 | }, 276 | "opera": { 277 | "supported": 1, 278 | "minVersion": 27 279 | }, 280 | "samsung-internet": { 281 | "supported": 1, 282 | "minVersion": 4.0 283 | }, 284 | "safari": { 285 | "supported": 1, 286 | "minVersion": 11.1 287 | }, 288 | "edge": { 289 | "supported": 1, 290 | "minVersion": 17 291 | } 292 | }, 293 | { 294 | "name": "fetchEvent.respondWith()", 295 | "description": "Spec. Test.", 296 | "chrome": { 297 | "supported": 1, 298 | "minVersion": 40 299 | }, 300 | "firefox": { 301 | "supported": 1, 302 | "minVersion": 44 303 | }, 304 | "opera": { 305 | "supported": 1, 306 | "minVersion": 27 307 | }, 308 | "samsung-internet": { 309 | "supported": 1, 310 | "minVersion": 4.0 311 | }, 312 | "safari": { 313 | "supported": 1, 314 | "minVersion": 11.1 315 | }, 316 | "edge": { 317 | "supported": 1, 318 | "minVersion": 17 319 | } 320 | }, 321 | { 322 | "name": "Install event", 323 | "description": "Install event fires in a newly discovered SW. Includes InstallEvent.waitUntil(). Spec. Test", 324 | "chrome": { 325 | "supported": 1, 326 | "minVersion": 40 327 | }, 328 | "firefox": { 329 | "supported": 1, 330 | "minVersion": 44 331 | }, 332 | "opera": { 333 | "supported": 1, 334 | "minVersion": 27 335 | }, 336 | "samsung-internet": { 337 | "supported": 1, 338 | "minVersion": 4.0 339 | }, 340 | "safari": { 341 | "supported": 1, 342 | "minVersion": 11.1 343 | }, 344 | "edge": { 345 | "supported": 1, 346 | "minVersion": 17 347 | } 348 | }, 349 | { 350 | "name": "self.skipWaiting()", 351 | "description": "Allow an installing worker to take over from the current active worker once installed. Spec. Test.", 352 | "chrome": { 353 | "supported": 1, 354 | "minVersion": 42 355 | }, 356 | "firefox": { 357 | "supported": 1, 358 | "minVersion": 44 359 | }, 360 | "opera": { 361 | "supported": 1, 362 | "minVersion": 27 363 | }, 364 | "samsung-internet": { 365 | "supported": 1, 366 | "minVersion": 4.0 367 | }, 368 | "safari": { 369 | "supported": 1, 370 | "minVersion": 11.1 371 | }, 372 | "edge": { 373 | "supported": 1, 374 | "minVersion": 17 375 | } 376 | }, 377 | { 378 | "name": "Activate event", 379 | "description": "Activate event fires once this worker becomes the active worker in a registration. Includes event.waitUntil(). Spec. Test.", 380 | "chrome": { 381 | "supported": 1, 382 | "minVersion": 40 383 | }, 384 | "firefox": { 385 | "supported": 1, 386 | "minVersion": 44 387 | }, 388 | "opera": { 389 | "supported": 1, 390 | "minVersion": 27 391 | }, 392 | "samsung-internet": { 393 | "supported": 1, 394 | "minVersion": 4.0 395 | }, 396 | "safari": { 397 | "supported": 1, 398 | "minVersion": 11.1 399 | }, 400 | "edge": { 401 | "supported": 1, 402 | "minVersion": 17 403 | } 404 | }, 405 | { 406 | "name": "clients.claim()", 407 | "description": "Allow an active worker to take control of pages in its scope (eg, documents that were loaded before the SW was registered). Spec. Test.", 408 | "chrome": { 409 | "supported": 1, 410 | "minVersion": 42 411 | }, 412 | "firefox": { 413 | "supported": 1, 414 | "minVersion": 44 415 | }, 416 | "opera": { 417 | "supported": 1, 418 | "minVersion": 33 419 | }, 420 | "samsung-internet": { 421 | "supported": 1, 422 | "minVersion": 4.0 423 | }, 424 | "safari": { 425 | "supported": 1, 426 | "minVersion": 11.1 427 | }, 428 | "edge": { 429 | "supported": 1, 430 | "minVersion": 17 431 | } 432 | }, 433 | { 434 | "name": "Update checks", 435 | "description": "Browser checks for SW updates after navigation. Spec.", 436 | "chrome": { 437 | "supported": 1, 438 | "minVersion": 40 439 | }, 440 | "firefox": { 441 | "supported": 1, 442 | "minVersion": 44 443 | }, 444 | "opera": { 445 | "supported": 1, 446 | "minVersion": 27 447 | }, 448 | "samsung-internet": { 449 | "supported": 1, 450 | "minVersion": 4.0 451 | }, 452 | "safari": { 453 | "supported": 1, 454 | "minVersion": 11.1 455 | }, 456 | "edge": { 457 | "supported": 1, 458 | "minVersion": 17 459 | } 460 | }, 461 | { 462 | "name": "Service worker lifecycle", 463 | "description": "Allow a next version to be in waiting & take over when appropriate.", 464 | "chrome": { 465 | "supported": 1, 466 | "minVersion": 40 467 | }, 468 | "firefox": { 469 | "supported": 1, 470 | "minVersion": 44 471 | }, 472 | "opera": { 473 | "supported": 1, 474 | "minVersion": 27 475 | }, 476 | "samsung-internet": { 477 | "supported": 1, 478 | "minVersion": 4.0 479 | }, 480 | "safari": { 481 | "supported": 1, 482 | "minVersion": 11.1 483 | }, 484 | "edge": { 485 | "supported": 1, 486 | "minVersion": 17 487 | } 488 | }, 489 | { 490 | "name": "Request", 491 | "description": "Spec. Test.", 492 | "chrome": { 493 | "supported": 1, 494 | "minVersion": 40 495 | }, 496 | "firefox": { 497 | "supported": 1, 498 | "minVersion": 39 499 | }, 500 | "opera": { 501 | "supported": 1, 502 | "minVersion": 27 503 | }, 504 | "samsung-internet": { 505 | "supported": 1, 506 | "minVersion": 4.0 507 | }, 508 | "safari": { 509 | "supported": 1, 510 | "minVersion": 10.1 511 | }, 512 | "edge": { 513 | "supported": 1, 514 | "minVersion": 14 515 | } 516 | }, 517 | { 518 | "name": "Response", 519 | "description": "Spec. Test.", 520 | "chrome": { 521 | "supported": 1, 522 | "minVersion": 40 523 | }, 524 | "firefox": { 525 | "supported": 1, 526 | "minVersion": 39 527 | }, 528 | "opera": { 529 | "supported": 1, 530 | "minVersion": 33 531 | }, 532 | "samsung-internet": { 533 | "supported": 1, 534 | "minVersion": 4.0 535 | }, 536 | "safari": { 537 | "supported": 1, 538 | "minVersion": 10.1 539 | }, 540 | "edge": { 541 | "supported": 1, 542 | "minVersion": 14 543 | }, 544 | "details": [ 545 | "Chrome & Samsung Internet: URLSearchParams not supported yet" 546 | ] 547 | }, 548 | { 549 | "name": "fetch(request)", 550 | "description": "Spec. Test.", 551 | "chrome": { 552 | "supported": 1, 553 | "minVersion": 40 554 | }, 555 | "firefox": { 556 | "supported": 1, 557 | "minVersion": 39 558 | }, 559 | "opera": { 560 | "supported": 1, 561 | "minVersion": 27 562 | }, 563 | "samsung-internet": { 564 | "supported": 1, 565 | "minVersion": 4.0 566 | }, 567 | "safari": { 568 | "supported": 1, 569 | "minVersion": 10.1 570 | }, 571 | "edge": { 572 | "supported": 1, 573 | "minVersion": 14 574 | }, 575 | "details": [ 576 | "Polyfill available" 577 | ] 578 | }, 579 | { 580 | "name": "caches", 581 | "description": "Spec. Test.", 582 | "chrome": { 583 | "supported": 1, 584 | "minVersion": 46 585 | }, 586 | "firefox": { 587 | "supported": 1, 588 | "minVersion": 44 589 | }, 590 | "opera": { 591 | "supported": 1, 592 | "minVersion": 33 593 | }, 594 | "samsung-internet": { 595 | "supported": 1, 596 | "minVersion": 4.0 597 | }, 598 | "safari": { 599 | "supported": 1, 600 | "minVersion": 11.1 601 | }, 602 | "edge": { 603 | "supported": 1, 604 | "minVersion": 16 605 | } 606 | }, 607 | { 608 | "name": "serviceWorker.ready", 609 | "description": "Spec. Test.", 610 | "chrome": { 611 | "supported": 1 612 | }, 613 | "firefox": { 614 | "supported": 1, 615 | "minVersion": 44 616 | }, 617 | "opera": { 618 | "supported": 1, 619 | "minVersion": 33 620 | }, 621 | "samsung-internet": { 622 | "supported": 1, 623 | "minVersion": 4.0 624 | }, 625 | "safari": { 626 | "supported": 1, 627 | "minVersion": 11.1 628 | }, 629 | "edge": { 630 | "supported": 1, 631 | "minVersion": 17 632 | } 633 | }, 634 | { 635 | "name": "Background sync", 636 | "description": "Deferring tasks until the user has connectivity. Spec. Test.", 637 | "chrome": { 638 | "supported": 1, 639 | "minVersion": 49 640 | }, 641 | "firefox": { 642 | "supported": 0, 643 | "details": [ 644 | "Bug 1217544" 645 | ] 646 | }, 647 | "opera": { 648 | "supported": 0 649 | }, 650 | "samsung-internet": { 651 | "supported": 0 652 | }, 653 | "safari": { 654 | "supported": 0 655 | }, 656 | "edge": { 657 | "supported": 0, 658 | "details": [ 659 | "In development" 660 | ] 661 | } 662 | } 663 | ] 664 | } 665 | -------------------------------------------------------------------------------- /src/demos/claim/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 59 | 60 | -------------------------------------------------------------------------------- /src/demos/claim/sw.js: -------------------------------------------------------------------------------- 1 | self.onactivate = function() { 2 | clients.claim(); 3 | }; 4 | 5 | self.onmessage = function(event) { 6 | if (event.data == 'claim') { 7 | clients.claim(); 8 | } 9 | }; 10 | 11 | 12 | self.onfetch = function(event) { 13 | var url = new URL(event.request.url); 14 | if (url.pathname.endsWith('/404.json')) { 15 | event.respondWith( 16 | new Response('{"This came from": "The ServiceWorker"}', { 17 | headers: { 18 | "Content-Type": "application/json" 19 | } 20 | }) 21 | ); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/demos/clients-count/index.html: -------------------------------------------------------------------------------- 1 | 2 |

No controller, refresh to load via SW

3 | -------------------------------------------------------------------------------- /src/demos/clients-count/sw.js: -------------------------------------------------------------------------------- 1 | self.skipWaiting(); 2 | 3 | self.addEventListener('fetch', event => { 4 | event.respondWith( 5 | clients.matchAll().then(clients => new Response(`Number of controlled clients = ${clients.length}`)) 6 | ); 7 | }); -------------------------------------------------------------------------------- /src/demos/fetch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 32 | 33 | -------------------------------------------------------------------------------- /src/demos/fetch/json.json: -------------------------------------------------------------------------------- 1 | {"hello": "world"} 2 | -------------------------------------------------------------------------------- /src/demos/fetch/sw.js: -------------------------------------------------------------------------------- 1 | console.log("fetch", this.fetch); 2 | 3 | if (this.fetch) { 4 | console.log("Attempting fetch"); 5 | fetch('./').then(function(res) { 6 | console.log("Response", res); 7 | return res.text(); 8 | }).then(function(text) { 9 | console.log("body", text); 10 | }).catch(function(err) { 11 | console.error(err); 12 | }).then(function() { 13 | console.log("Attempting JSON fetch"); 14 | return fetch('./json.json'); 15 | }).then(function(res) { 16 | console.log("Response", res); 17 | return res.json(); 18 | }).then(function(data) { 19 | console.log("body", data); 20 | }).catch(function(err) { 21 | console.error(err); 22 | }).then(function() { 23 | console.log("Attempting fetch outside of scope"); 24 | return fetch('/'); 25 | }).then(function(res) { 26 | console.log("Response", res); 27 | return res.text(); 28 | }).then(function(text) { 29 | console.log("body", text); 30 | }).catch(function(err) { 31 | console.error(err); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/demos/fetchevent/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /src/demos/fetchevent/sw.js: -------------------------------------------------------------------------------- 1 | console.log("SW startup"); 2 | 3 | this.onfetch = function(event) { 4 | console.log("Fetch event", event); 5 | console.log(".request", event.request); 6 | console.log(".respondWith", event.respondWith); 7 | console.log(".default", event.default); 8 | 9 | if (event.respondWith) { 10 | event.respondWith(new Response(new Blob(["Hello world"], {type : 'text/html'}), { 11 | headers: {"Content-Type": "text/html"} 12 | })); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/demos/force-reload-loop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

13 | This page refreshes once the controller changes 14 |

15 | 16 |
17 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/demos/force-reload-loop/sw.js: -------------------------------------------------------------------------------- 1 | // Blah -------------------------------------------------------------------------------- /src/demos/forward-requests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

13 | \o/ 14 |

15 | 16 |
17 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/demos/forward-requests/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('activate', _ => { 2 | clients.claim(); 3 | }); 4 | 5 | self.addEventListener('fetch', event => { 6 | console.log(event.request); 7 | event.respondWith(fetch(event.request)); 8 | }); -------------------------------------------------------------------------------- /src/demos/gif-stream/babel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 |

Decoding MPEG

16 | 17 | 18 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/demos/gif-stream/buffer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 |

Decoding MPEG

16 | 17 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/demos/gif-stream/cat.mpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/demos/gif-stream/cat.mpg -------------------------------------------------------------------------------- /src/demos/gif-stream/gif.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Streaming GIF demo 5 | 6 | 14 | 15 | 16 |

Streaming gif

17 |

The following gif is dynamically generated from an MPEG1 video.

18 | Cat hates strawberry 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/demos/gif-stream/gif.js: -------------------------------------------------------------------------------- 1 | (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 { 70 | this.readControllers.push(controller); 71 | } 72 | }); 73 | } 74 | 75 | /* 76 | GIFEncoder.prototype.createWriteStream = function (options) { 77 | var self = this; 78 | if (options) { 79 | Object.keys(options).forEach(function (option) { 80 | var fn = 'set' + option[0].toUpperCase() + option.substr(1); 81 | if (~['setDelay', 'setFrameRate', 'setDispose', 'setRepeat', 82 | 'setTransparent', 'setQuality'].indexOf(fn)) { 83 | self[fn].call(self, options[option]); 84 | } 85 | }); 86 | } 87 | 88 | var ws = new stream.Duplex({ objectMode: true }); 89 | ws._read = function () {}; 90 | this.createReadStream(ws); 91 | 92 | var self = this; 93 | ws._write = function (data, enc, next) { 94 | if (!self.started) self.start(); 95 | self.addFrame(data); 96 | next(); 97 | }; 98 | var end = ws.end; 99 | ws.end = function () { 100 | end.apply(ws, [].slice.call(arguments)); 101 | self.finish(); 102 | }; 103 | return ws; 104 | }; 105 | */ 106 | 107 | GIFEncoder.prototype.emit = function() { 108 | if (this.readControllers.length === 0) return; 109 | if (this.out.data.length) { 110 | this.readControllers.forEach(c => c.enqueue(Uint8Array.from(this.out.data))); 111 | this.out.data = []; 112 | } 113 | }; 114 | 115 | GIFEncoder.prototype.end = function() { 116 | if (this.readControllers.length === null) return; 117 | this.emit(); 118 | this.readControllers.forEach(c => c.close()); 119 | this.readControllers = []; 120 | }; 121 | 122 | /* 123 | Sets the delay time between each frame, or changes it for subsequent frames 124 | (applies to the next frame added) 125 | */ 126 | GIFEncoder.prototype.setDelay = function(milliseconds) { 127 | this.delay = Math.round(milliseconds / 10); 128 | }; 129 | 130 | /* 131 | Sets frame rate in frames per second. 132 | */ 133 | GIFEncoder.prototype.setFrameRate = function(fps) { 134 | this.delay = Math.round(100 / fps); 135 | }; 136 | 137 | /* 138 | Sets the GIF frame disposal code for the last added frame and any 139 | subsequent frames. 140 | 141 | Default is 0 if no transparent color has been set, otherwise 2. 142 | */ 143 | GIFEncoder.prototype.setDispose = function(disposalCode) { 144 | if (disposalCode >= 0) this.dispose = disposalCode; 145 | }; 146 | 147 | /* 148 | Sets the number of times the set of GIF frames should be played. 149 | 150 | -1 = play once 151 | 0 = repeat indefinitely 152 | 153 | Default is -1 154 | 155 | Must be invoked before the first image is added 156 | */ 157 | 158 | GIFEncoder.prototype.setRepeat = function(repeat) { 159 | this.repeat = repeat; 160 | }; 161 | 162 | /* 163 | Sets the transparent color for the last added frame and any subsequent 164 | frames. Since all colors are subject to modification in the quantization 165 | process, the color in the final palette for each frame closest to the given 166 | color becomes the transparent color for that frame. May be set to null to 167 | indicate no transparent color. 168 | */ 169 | GIFEncoder.prototype.setTransparent = function(color) { 170 | this.transparent = color; 171 | }; 172 | 173 | /* 174 | Adds next GIF frame. The frame is not written immediately, but is 175 | actually deferred until the next frame is received so that timing 176 | data can be inserted. Invoking finish() flushes all frames. 177 | */ 178 | GIFEncoder.prototype.addFrame = function(imageData) { 179 | // HTML Canvas 2D Context Passed In 180 | if (imageData && imageData.getImageData) { 181 | this.image = imageData.getImageData(0, 0, this.width, this.height).data; 182 | } else { 183 | this.image = imageData; 184 | } 185 | 186 | this.getImagePixels(); // convert to correct format if necessary 187 | this.analyzePixels(); // build color table & map pixels 188 | 189 | if (this.firstFrame) { 190 | this.writeLSD(); // logical screen descriptior 191 | this.writePalette(); // global color table 192 | if (this.repeat >= 0) { 193 | // use NS app extension to indicate reps 194 | this.writeNetscapeExt(); 195 | } 196 | } 197 | 198 | this.writeGraphicCtrlExt(); // write graphic control extension 199 | this.writeImageDesc(); // image descriptor 200 | if (!this.firstFrame) this.writePalette(); // local color table 201 | this.writePixels(); // encode and write pixel data 202 | 203 | this.firstFrame = false; 204 | this.emit(); 205 | }; 206 | 207 | /* 208 | Adds final trailer to the GIF stream, if you don't call the finish method 209 | the GIF stream will not be valid. 210 | */ 211 | GIFEncoder.prototype.finish = function() { 212 | this.out.writeByte(0x3b); // gif trailer 213 | this.end(); 214 | }; 215 | 216 | /* 217 | Sets quality of color quantization (conversion of images to the maximum 256 218 | colors allowed by the GIF specification). Lower values (minimum = 1) 219 | produce better colors, but slow processing significantly. 10 is the 220 | default, and produces good color mapping at reasonable speeds. Values 221 | greater than 20 do not yield significant improvements in speed. 222 | */ 223 | GIFEncoder.prototype.setQuality = function(quality) { 224 | if (quality < 1) quality = 1; 225 | this.sample = quality; 226 | }; 227 | 228 | /* 229 | Writes GIF file header 230 | */ 231 | GIFEncoder.prototype.start = function() { 232 | this.out.writeUTFBytes("GIF89a"); 233 | this.started = true; 234 | this.emit(); 235 | }; 236 | 237 | /* 238 | Analyzes current frame colors and creates color map. 239 | */ 240 | GIFEncoder.prototype.analyzePixels = function() { 241 | var len = this.pixels.length; 242 | var nPix = len / 3; 243 | 244 | this.indexedPixels = new Uint8Array(nPix); 245 | 246 | var imgq = new NeuQuant(this.pixels, this.sample); 247 | imgq.buildColormap(); // create reduced palette 248 | this.colorTab = imgq.getColormap(); 249 | 250 | // map image pixels to new palette 251 | var k = 0; 252 | for (var j = 0; j < nPix; j++) { 253 | var index = imgq.lookupRGB( 254 | this.pixels[k++] & 0xff, 255 | this.pixels[k++] & 0xff, 256 | this.pixels[k++] & 0xff 257 | ); 258 | this.usedEntry[index] = true; 259 | this.indexedPixels[j] = index; 260 | } 261 | 262 | this.pixels = null; 263 | this.colorDepth = 8; 264 | this.palSize = 7; 265 | 266 | // get closest match to transparent color if specified 267 | if (this.transparent !== null) { 268 | this.transIndex = this.findClosest(this.transparent); 269 | } 270 | }; 271 | 272 | /* 273 | Returns index of palette color closest to c 274 | */ 275 | GIFEncoder.prototype.findClosest = function(c) { 276 | if (this.colorTab === null) return -1; 277 | 278 | var r = (c & 0xFF0000) >> 16; 279 | var g = (c & 0x00FF00) >> 8; 280 | var b = (c & 0x0000FF); 281 | var minpos = 0; 282 | var dmin = 256 * 256 * 256; 283 | var len = this.colorTab.length; 284 | 285 | for (var i = 0; i < len;) { 286 | var dr = r - (this.colorTab[i++] & 0xff); 287 | var dg = g - (this.colorTab[i++] & 0xff); 288 | var db = b - (this.colorTab[i] & 0xff); 289 | var d = dr * dr + dg * dg + db * db; 290 | var index = i / 3; 291 | if (this.usedEntry[index] && (d < dmin)) { 292 | dmin = d; 293 | minpos = index; 294 | } 295 | i++; 296 | } 297 | 298 | return minpos; 299 | }; 300 | 301 | /* 302 | Extracts image pixels into byte array pixels 303 | (removes alphachannel from canvas imagedata) 304 | */ 305 | GIFEncoder.prototype.getImagePixels = function() { 306 | var w = this.width; 307 | var h = this.height; 308 | this.pixels = new Uint8Array(w * h * 3); 309 | 310 | var data = this.image; 311 | var count = 0; 312 | 313 | for (var i = 0; i < h; i++) { 314 | for (var j = 0; j < w; j++) { 315 | var b = (i * w * 4) + j * 4; 316 | this.pixels[count++] = data[b]; 317 | this.pixels[count++] = data[b+1]; 318 | this.pixels[count++] = data[b+2]; 319 | } 320 | } 321 | }; 322 | 323 | /* 324 | Writes Graphic Control Extension 325 | */ 326 | GIFEncoder.prototype.writeGraphicCtrlExt = function() { 327 | this.out.writeByte(0x21); // extension introducer 328 | this.out.writeByte(0xf9); // GCE label 329 | this.out.writeByte(4); // data block size 330 | 331 | var transp, disp; 332 | if (this.transparent === null) { 333 | transp = 0; 334 | disp = 0; // dispose = no action 335 | } else { 336 | transp = 1; 337 | disp = 2; // force clear if using transparent color 338 | } 339 | 340 | if (this.dispose >= 0) { 341 | disp = this.dispose & 7; // user override 342 | } 343 | disp <<= 2; 344 | 345 | // packed fields 346 | this.out.writeByte( 347 | 0 | // 1:3 reserved 348 | disp | // 4:6 disposal 349 | 0 | // 7 user input - 0 = none 350 | transp // 8 transparency flag 351 | ); 352 | 353 | this.writeShort(this.delay); // delay x 1/100 sec 354 | this.out.writeByte(this.transIndex); // transparent color index 355 | this.out.writeByte(0); // block terminator 356 | }; 357 | 358 | /* 359 | Writes Image Descriptor 360 | */ 361 | GIFEncoder.prototype.writeImageDesc = function() { 362 | this.out.writeByte(0x2c); // image separator 363 | this.writeShort(0); // image position x,y = 0,0 364 | this.writeShort(0); 365 | this.writeShort(this.width); // image size 366 | this.writeShort(this.height); 367 | 368 | // packed fields 369 | if (this.firstFrame) { 370 | // no LCT - GCT is used for first (or only) frame 371 | this.out.writeByte(0); 372 | } else { 373 | // specify normal LCT 374 | this.out.writeByte( 375 | 0x80 | // 1 local color table 1=yes 376 | 0 | // 2 interlace - 0=no 377 | 0 | // 3 sorted - 0=no 378 | 0 | // 4-5 reserved 379 | this.palSize // 6-8 size of color table 380 | ); 381 | } 382 | }; 383 | 384 | /* 385 | Writes Logical Screen Descriptor 386 | */ 387 | GIFEncoder.prototype.writeLSD = function() { 388 | // logical screen size 389 | this.writeShort(this.width); 390 | this.writeShort(this.height); 391 | 392 | // packed fields 393 | this.out.writeByte( 394 | 0x80 | // 1 : global color table flag = 1 (gct used) 395 | 0x70 | // 2-4 : color resolution = 7 396 | 0x00 | // 5 : gct sort flag = 0 397 | this.palSize // 6-8 : gct size 398 | ); 399 | 400 | this.out.writeByte(0); // background color index 401 | this.out.writeByte(0); // pixel aspect ratio - assume 1:1 402 | }; 403 | 404 | /* 405 | Writes Netscape application extension to define repeat count. 406 | */ 407 | GIFEncoder.prototype.writeNetscapeExt = function() { 408 | this.out.writeByte(0x21); // extension introducer 409 | this.out.writeByte(0xff); // app extension label 410 | this.out.writeByte(11); // block size 411 | this.out.writeUTFBytes('NETSCAPE2.0'); // app id + auth code 412 | this.out.writeByte(3); // sub-block size 413 | this.out.writeByte(1); // loop sub-block id 414 | this.writeShort(this.repeat); // loop count (extra iterations, 0=repeat forever) 415 | this.out.writeByte(0); // block terminator 416 | }; 417 | 418 | /* 419 | Writes color table 420 | */ 421 | GIFEncoder.prototype.writePalette = function() { 422 | this.out.writeBytes(this.colorTab); 423 | var n = (3 * 256) - this.colorTab.length; 424 | for (var i = 0; i < n; i++) 425 | this.out.writeByte(0); 426 | }; 427 | 428 | GIFEncoder.prototype.writeShort = function(pValue) { 429 | this.out.writeByte(pValue & 0xFF); 430 | this.out.writeByte((pValue >> 8) & 0xFF); 431 | }; 432 | 433 | /* 434 | Encodes and writes pixel data 435 | */ 436 | GIFEncoder.prototype.writePixels = function() { 437 | var enc = new LZWEncoder(this.width, this.height, this.indexedPixels, this.colorDepth); 438 | enc.encode(this.out); 439 | }; 440 | 441 | self.GIFEncoder = GIFEncoder; 442 | module.exports = GIFEncoder; 443 | },{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){ 444 | /* 445 | LZWEncoder.js 446 | 447 | Authors 448 | Kevin Weiner (original Java version - kweiner@fmsware.com) 449 | Thibault Imbert (AS3 version - bytearray.org) 450 | Johan Nordberg (JS version - code@johan-nordberg.com) 451 | 452 | Acknowledgements 453 | GIFCOMPR.C - GIF Image compression routines 454 | Lempel-Ziv compression based on 'compress'. GIF modifications by 455 | David Rowley (mgardi@watdcsu.waterloo.edu) 456 | GIF Image compression - modified 'compress' 457 | Based on: compress.c - File compression ala IEEE Computer, June 1984. 458 | By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) 459 | Jim McKie (decvax!mcvax!jim) 460 | Steve Davies (decvax!vax135!petsd!peora!srd) 461 | Ken Turkowski (decvax!decwrl!turtlevax!ken) 462 | James A. Woods (decvax!ihnp4!ames!jaw) 463 | Joe Orost (decvax!vax135!petsd!joe) 464 | */ 465 | 466 | var EOF = -1; 467 | var BITS = 12; 468 | var HSIZE = 5003; // 80% occupancy 469 | var masks = [0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 470 | 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 471 | 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF]; 472 | 473 | function LZWEncoder(width, height, pixels, colorDepth) { 474 | var initCodeSize = Math.max(2, colorDepth); 475 | 476 | var accum = new Uint8Array(256); 477 | var htab = new Int32Array(HSIZE); 478 | var codetab = new Int32Array(HSIZE); 479 | 480 | var cur_accum, cur_bits = 0; 481 | var a_count; 482 | var free_ent = 0; // first unused entry 483 | var maxcode; 484 | 485 | // block compression parameters -- after all codes are used up, 486 | // and compression rate changes, start over. 487 | var clear_flg = false; 488 | 489 | // Algorithm: use open addressing double hashing (no chaining) on the 490 | // prefix code / next character combination. We do a variant of Knuth's 491 | // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime 492 | // secondary probe. Here, the modular division first probe is gives way 493 | // to a faster exclusive-or manipulation. Also do block compression with 494 | // an adaptive reset, whereby the code table is cleared when the compression 495 | // ratio decreases, but after the table fills. The variable-length output 496 | // codes are re-sized at this point, and a special CLEAR code is generated 497 | // for the decompressor. Late addition: construct the table according to 498 | // file size for noticeable speed improvement on small files. Please direct 499 | // questions about this implementation to ames!jaw. 500 | var g_init_bits, ClearCode, EOFCode; 501 | 502 | // Add a character to the end of the current packet, and if it is 254 503 | // characters, flush the packet to disk. 504 | function char_out(c, outs) { 505 | accum[a_count++] = c; 506 | if (a_count >= 254) flush_char(outs); 507 | } 508 | 509 | // Clear out the hash table 510 | // table clear for block compress 511 | function cl_block(outs) { 512 | cl_hash(HSIZE); 513 | free_ent = ClearCode + 2; 514 | clear_flg = true; 515 | output(ClearCode, outs); 516 | } 517 | 518 | // Reset code table 519 | function cl_hash(hsize) { 520 | for (var i = 0; i < hsize; ++i) htab[i] = -1; 521 | } 522 | 523 | function compress(init_bits, outs) { 524 | var fcode, c, i, ent, disp, hsize_reg, hshift; 525 | 526 | // Set up the globals: g_init_bits - initial number of bits 527 | g_init_bits = init_bits; 528 | 529 | // Set up the necessary values 530 | clear_flg = false; 531 | n_bits = g_init_bits; 532 | maxcode = MAXCODE(n_bits); 533 | 534 | ClearCode = 1 << (init_bits - 1); 535 | EOFCode = ClearCode + 1; 536 | free_ent = ClearCode + 2; 537 | 538 | a_count = 0; // clear packet 539 | 540 | ent = nextPixel(); 541 | 542 | hshift = 0; 543 | for (fcode = HSIZE; fcode < 65536; fcode *= 2) ++hshift; 544 | hshift = 8 - hshift; // set hash code range bound 545 | hsize_reg = HSIZE; 546 | cl_hash(hsize_reg); // clear hash table 547 | 548 | output(ClearCode, outs); 549 | 550 | outer_loop: while ((c = nextPixel()) != EOF) { 551 | fcode = (c << BITS) + ent; 552 | i = (c << hshift) ^ ent; // xor hashing 553 | if (htab[i] === fcode) { 554 | ent = codetab[i]; 555 | continue; 556 | } else if (htab[i] >= 0) { // non-empty slot 557 | disp = hsize_reg - i; // secondary hash (after G. Knott) 558 | if (i === 0) disp = 1; 559 | do { 560 | if ((i -= disp) < 0) i += hsize_reg; 561 | if (htab[i] === fcode) { 562 | ent = codetab[i]; 563 | continue outer_loop; 564 | } 565 | } while (htab[i] >= 0); 566 | } 567 | output(ent, outs); 568 | ent = c; 569 | if (free_ent < 1 << BITS) { 570 | codetab[i] = free_ent++; // code -> hashtable 571 | htab[i] = fcode; 572 | } else { 573 | cl_block(outs); 574 | } 575 | } 576 | 577 | // Put out the final code. 578 | output(ent, outs); 579 | output(EOFCode, outs); 580 | } 581 | 582 | function encode(outs) { 583 | outs.writeByte(initCodeSize); // write "initial code size" byte 584 | remaining = width * height; // reset navigation variables 585 | curPixel = 0; 586 | compress(initCodeSize + 1, outs); // compress and write the pixel data 587 | outs.writeByte(0); // write block terminator 588 | } 589 | 590 | // Flush the packet to disk, and reset the accumulator 591 | function flush_char(outs) { 592 | if (a_count > 0) { 593 | outs.writeByte(a_count); 594 | outs.writeBytes(accum, 0, a_count); 595 | a_count = 0; 596 | } 597 | } 598 | 599 | function MAXCODE(n_bits) { 600 | return (1 << n_bits) - 1; 601 | } 602 | 603 | // Return the next pixel from the image 604 | function nextPixel() { 605 | if (remaining === 0) return EOF; 606 | --remaining; 607 | var pix = pixels[curPixel++]; 608 | return pix & 0xff; 609 | } 610 | 611 | function output(code, outs) { 612 | cur_accum &= masks[cur_bits]; 613 | 614 | if (cur_bits > 0) cur_accum |= (code << cur_bits); 615 | else cur_accum = code; 616 | 617 | cur_bits += n_bits; 618 | 619 | while (cur_bits >= 8) { 620 | char_out((cur_accum & 0xff), outs); 621 | cur_accum >>= 8; 622 | cur_bits -= 8; 623 | } 624 | 625 | // If the next entry is going to be too big for the code size, 626 | // then increase it, if possible. 627 | if (free_ent > maxcode || clear_flg) { 628 | if (clear_flg) { 629 | maxcode = MAXCODE(n_bits = g_init_bits); 630 | clear_flg = false; 631 | } else { 632 | ++n_bits; 633 | if (n_bits == BITS) maxcode = 1 << BITS; 634 | else maxcode = MAXCODE(n_bits); 635 | } 636 | } 637 | 638 | if (code == EOFCode) { 639 | // At EOF, write the rest of the buffer. 640 | while (cur_bits > 0) { 641 | char_out((cur_accum & 0xff), outs); 642 | cur_accum >>= 8; 643 | cur_bits -= 8; 644 | } 645 | flush_char(outs); 646 | } 647 | } 648 | 649 | this.encode = encode; 650 | } 651 | 652 | module.exports = LZWEncoder; 653 | },{}],3:[function(require,module,exports){ 654 | /* NeuQuant Neural-Net Quantization Algorithm 655 | * ------------------------------------------ 656 | * 657 | * Copyright (c) 1994 Anthony Dekker 658 | * 659 | * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. 660 | * See "Kohonen neural networks for optimal colour quantization" 661 | * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 662 | * for a discussion of the algorithm. 663 | * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML 664 | * 665 | * Any party obtaining a copy of these files from the author, directly or 666 | * indirectly, is granted, free of charge, a full and unrestricted irrevocable, 667 | * world-wide, paid up, royalty-free, nonexclusive right and license to deal 668 | * in this software and documentation files (the "Software"), including without 669 | * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 670 | * and/or sell copies of the Software, and to permit persons who receive 671 | * copies from any such party to do so, with the only requirement being 672 | * that this copyright notice remain intact. 673 | * 674 | * (JavaScript port 2012 by Johan Nordberg) 675 | */ 676 | 677 | var ncycles = 100; // number of learning cycles 678 | var netsize = 256; // number of colors used 679 | var maxnetpos = netsize - 1; 680 | 681 | // defs for freq and bias 682 | var netbiasshift = 4; // bias for colour values 683 | var intbiasshift = 16; // bias for fractions 684 | var intbias = (1 << intbiasshift); 685 | var gammashift = 10; 686 | var gamma = (1 << gammashift); 687 | var betashift = 10; 688 | var beta = (intbias >> betashift); /* beta = 1/1024 */ 689 | var betagamma = (intbias << (gammashift - betashift)); 690 | 691 | // defs for decreasing radius factor 692 | var initrad = (netsize >> 3); // for 256 cols, radius starts 693 | var radiusbiasshift = 6; // at 32.0 biased by 6 bits 694 | var radiusbias = (1 << radiusbiasshift); 695 | var initradius = (initrad * radiusbias); //and decreases by a 696 | var radiusdec = 30; // factor of 1/30 each cycle 697 | 698 | // defs for decreasing alpha factor 699 | var alphabiasshift = 10; // alpha starts at 1.0 700 | var initalpha = (1 << alphabiasshift); 701 | var alphadec; // biased by 10 bits 702 | 703 | /* radbias and alpharadbias used for radpower calculation */ 704 | var radbiasshift = 8; 705 | var radbias = (1 << radbiasshift); 706 | var alpharadbshift = (alphabiasshift + radbiasshift); 707 | var alpharadbias = (1 << alpharadbshift); 708 | 709 | // four primes near 500 - assume no image has a length so large that it is 710 | // divisible by all four primes 711 | var prime1 = 499; 712 | var prime2 = 491; 713 | var prime3 = 487; 714 | var prime4 = 503; 715 | var minpicturebytes = (3 * prime4); 716 | 717 | /* 718 | Constructor: NeuQuant 719 | 720 | Arguments: 721 | 722 | pixels - array of pixels in RGB format 723 | samplefac - sampling factor 1 to 30 where lower is better quality 724 | 725 | > 726 | > pixels = [r, g, b, r, g, b, r, g, b, ..] 727 | > 728 | */ 729 | function NeuQuant(pixels, samplefac) { 730 | var network; // int[netsize][4] 731 | var netindex; // for network lookup - really 256 732 | 733 | // bias and freq arrays for learning 734 | var bias; 735 | var freq; 736 | var radpower; 737 | 738 | /* 739 | Private Method: init 740 | 741 | sets up arrays 742 | */ 743 | function init() { 744 | network = []; 745 | netindex = new Int32Array(256); 746 | bias = new Int32Array(netsize); 747 | freq = new Int32Array(netsize); 748 | radpower = new Int32Array(netsize >> 3); 749 | 750 | var i, v; 751 | for (i = 0; i < netsize; i++) { 752 | v = (i << (netbiasshift + 8)) / netsize; 753 | network[i] = new Float64Array([v, v, v, 0]); 754 | //network[i] = [v, v, v, 0] 755 | freq[i] = intbias / netsize; 756 | bias[i] = 0; 757 | } 758 | } 759 | 760 | /* 761 | Private Method: unbiasnet 762 | 763 | unbiases network to give byte values 0..255 and record position i to prepare for sort 764 | */ 765 | function unbiasnet() { 766 | for (var i = 0; i < netsize; i++) { 767 | network[i][0] >>= netbiasshift; 768 | network[i][1] >>= netbiasshift; 769 | network[i][2] >>= netbiasshift; 770 | network[i][3] = i; // record color number 771 | } 772 | } 773 | 774 | /* 775 | Private Method: altersingle 776 | 777 | moves neuron *i* towards biased (b,g,r) by factor *alpha* 778 | */ 779 | function altersingle(alpha, i, b, g, r) { 780 | network[i][0] -= (alpha * (network[i][0] - b)) / initalpha; 781 | network[i][1] -= (alpha * (network[i][1] - g)) / initalpha; 782 | network[i][2] -= (alpha * (network[i][2] - r)) / initalpha; 783 | } 784 | 785 | /* 786 | Private Method: alterneigh 787 | 788 | moves neurons in *radius* around index *i* towards biased (b,g,r) by factor *alpha* 789 | */ 790 | function alterneigh(radius, i, b, g, r) { 791 | var lo = Math.abs(i - radius); 792 | var hi = Math.min(i + radius, netsize); 793 | 794 | var j = i + 1; 795 | var k = i - 1; 796 | var m = 1; 797 | 798 | var p, a; 799 | while ((j < hi) || (k > lo)) { 800 | a = radpower[m++]; 801 | 802 | if (j < hi) { 803 | p = network[j++]; 804 | p[0] -= (a * (p[0] - b)) / alpharadbias; 805 | p[1] -= (a * (p[1] - g)) / alpharadbias; 806 | p[2] -= (a * (p[2] - r)) / alpharadbias; 807 | } 808 | 809 | if (k > lo) { 810 | p = network[k--]; 811 | p[0] -= (a * (p[0] - b)) / alpharadbias; 812 | p[1] -= (a * (p[1] - g)) / alpharadbias; 813 | p[2] -= (a * (p[2] - r)) / alpharadbias; 814 | } 815 | } 816 | } 817 | 818 | /* 819 | Private Method: contest 820 | 821 | searches for biased BGR values 822 | */ 823 | function contest(b, g, r) { 824 | /* 825 | finds closest neuron (min dist) and updates freq 826 | finds best neuron (min dist-bias) and returns position 827 | for frequently chosen neurons, freq[i] is high and bias[i] is negative 828 | bias[i] = gamma * ((1 / netsize) - freq[i]) 829 | */ 830 | 831 | var bestd = ~(1 << 31); 832 | var bestbiasd = bestd; 833 | var bestpos = -1; 834 | var bestbiaspos = bestpos; 835 | 836 | var i, n, dist, biasdist, betafreq; 837 | for (i = 0; i < netsize; i++) { 838 | n = network[i]; 839 | 840 | dist = Math.abs(n[0] - b) + Math.abs(n[1] - g) + Math.abs(n[2] - r); 841 | if (dist < bestd) { 842 | bestd = dist; 843 | bestpos = i; 844 | } 845 | 846 | biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); 847 | if (biasdist < bestbiasd) { 848 | bestbiasd = biasdist; 849 | bestbiaspos = i; 850 | } 851 | 852 | betafreq = (freq[i] >> betashift); 853 | freq[i] -= betafreq; 854 | bias[i] += (betafreq << gammashift); 855 | } 856 | 857 | freq[bestpos] += beta; 858 | bias[bestpos] -= betagamma; 859 | 860 | return bestbiaspos; 861 | } 862 | 863 | /* 864 | Private Method: inxbuild 865 | 866 | sorts network and builds netindex[0..255] 867 | */ 868 | function inxbuild() { 869 | var i, j, p, q, smallpos, smallval, previouscol = 0, startpos = 0; 870 | for (i = 0; i < netsize; i++) { 871 | p = network[i]; 872 | smallpos = i; 873 | smallval = p[1]; // index on g 874 | // find smallest in i..netsize-1 875 | for (j = i + 1; j < netsize; j++) { 876 | q = network[j]; 877 | if (q[1] < smallval) { // index on g 878 | smallpos = j; 879 | smallval = q[1]; // index on g 880 | } 881 | } 882 | q = network[smallpos]; 883 | // swap p (i) and q (smallpos) entries 884 | if (i != smallpos) { 885 | j = q[0]; q[0] = p[0]; p[0] = j; 886 | j = q[1]; q[1] = p[1]; p[1] = j; 887 | j = q[2]; q[2] = p[2]; p[2] = j; 888 | j = q[3]; q[3] = p[3]; p[3] = j; 889 | } 890 | // smallval entry is now in position i 891 | 892 | if (smallval != previouscol) { 893 | netindex[previouscol] = (startpos + i) >> 1; 894 | for (j = previouscol + 1; j < smallval; j++) 895 | netindex[j] = i; 896 | previouscol = smallval; 897 | startpos = i; 898 | } 899 | } 900 | netindex[previouscol] = (startpos + maxnetpos) >> 1; 901 | for (j = previouscol + 1; j < 256; j++) 902 | netindex[j] = maxnetpos; // really 256 903 | } 904 | 905 | /* 906 | Private Method: inxsearch 907 | 908 | searches for BGR values 0..255 and returns a color index 909 | */ 910 | function inxsearch(b, g, r) { 911 | var a, p, dist; 912 | 913 | var bestd = 1000; // biggest possible dist is 256*3 914 | var best = -1; 915 | 916 | var i = netindex[g]; // index on g 917 | var j = i - 1; // start at netindex[g] and work outwards 918 | 919 | while ((i < netsize) || (j >= 0)) { 920 | if (i < netsize) { 921 | p = network[i]; 922 | dist = p[1] - g; // inx key 923 | if (dist >= bestd) i = netsize; // stop iter 924 | else { 925 | i++; 926 | if (dist < 0) dist = -dist; 927 | a = p[0] - b; if (a < 0) a = -a; 928 | dist += a; 929 | if (dist < bestd) { 930 | a = p[2] - r; if (a < 0) a = -a; 931 | dist += a; 932 | if (dist < bestd) { 933 | bestd = dist; 934 | best = p[3]; 935 | } 936 | } 937 | } 938 | } 939 | if (j >= 0) { 940 | p = network[j]; 941 | dist = g - p[1]; // inx key - reverse dif 942 | if (dist >= bestd) j = -1; // stop iter 943 | else { 944 | j--; 945 | if (dist < 0) dist = -dist; 946 | a = p[0] - b; if (a < 0) a = -a; 947 | dist += a; 948 | if (dist < bestd) { 949 | a = p[2] - r; if (a < 0) a = -a; 950 | dist += a; 951 | if (dist < bestd) { 952 | bestd = dist; 953 | best = p[3]; 954 | } 955 | } 956 | } 957 | } 958 | } 959 | 960 | return best; 961 | } 962 | 963 | /* 964 | Private Method: learn 965 | 966 | "Main Learning Loop" 967 | */ 968 | function learn() { 969 | var i; 970 | 971 | var lengthcount = pixels.length; 972 | var alphadec = 30 + ((samplefac - 1) / 3); 973 | var samplepixels = lengthcount / (3 * samplefac); 974 | var delta = ~~(samplepixels / ncycles); 975 | var alpha = initalpha; 976 | var radius = initradius; 977 | 978 | var rad = radius >> radiusbiasshift; 979 | 980 | if (rad <= 1) rad = 0; 981 | for (i = 0; i < rad; i++) 982 | radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); 983 | 984 | var step; 985 | if (lengthcount < minpicturebytes) { 986 | samplefac = 1; 987 | step = 3; 988 | } else if ((lengthcount % prime1) !== 0) { 989 | step = 3 * prime1; 990 | } else if ((lengthcount % prime2) !== 0) { 991 | step = 3 * prime2; 992 | } else if ((lengthcount % prime3) !== 0) { 993 | step = 3 * prime3; 994 | } else { 995 | step = 3 * prime4; 996 | } 997 | 998 | var b, g, r, j; 999 | var pix = 0; // current pixel 1000 | 1001 | i = 0; 1002 | while (i < samplepixels) { 1003 | b = (pixels[pix] & 0xff) << netbiasshift; 1004 | g = (pixels[pix + 1] & 0xff) << netbiasshift; 1005 | r = (pixels[pix + 2] & 0xff) << netbiasshift; 1006 | 1007 | j = contest(b, g, r); 1008 | 1009 | altersingle(alpha, j, b, g, r); 1010 | if (rad !== 0) alterneigh(rad, j, b, g, r); // alter neighbours 1011 | 1012 | pix += step; 1013 | if (pix >= lengthcount) pix -= lengthcount; 1014 | 1015 | i++; 1016 | 1017 | if (delta === 0) delta = 1; 1018 | if (i % delta === 0) { 1019 | alpha -= alpha / alphadec; 1020 | radius -= radius / radiusdec; 1021 | rad = radius >> radiusbiasshift; 1022 | 1023 | if (rad <= 1) rad = 0; 1024 | for (j = 0; j < rad; j++) 1025 | radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); 1026 | } 1027 | } 1028 | } 1029 | 1030 | /* 1031 | Method: buildColormap 1032 | 1033 | 1. initializes network 1034 | 2. trains it 1035 | 3. removes misconceptions 1036 | 4. builds colorindex 1037 | */ 1038 | function buildColormap() { 1039 | init(); 1040 | learn(); 1041 | unbiasnet(); 1042 | inxbuild(); 1043 | } 1044 | this.buildColormap = buildColormap; 1045 | 1046 | /* 1047 | Method: getColormap 1048 | 1049 | builds colormap from the index 1050 | 1051 | returns array in the format: 1052 | 1053 | > 1054 | > [r, g, b, r, g, b, r, g, b, ..] 1055 | > 1056 | */ 1057 | function getColormap() { 1058 | var map = []; 1059 | var index = []; 1060 | 1061 | for (var i = 0; i < netsize; i++) 1062 | index[network[i][3]] = i; 1063 | 1064 | var k = 0; 1065 | for (var l = 0; l < netsize; l++) { 1066 | var j = index[l]; 1067 | map[k++] = (network[j][0]); 1068 | map[k++] = (network[j][1]); 1069 | map[k++] = (network[j][2]); 1070 | } 1071 | return map; 1072 | } 1073 | this.getColormap = getColormap; 1074 | 1075 | /* 1076 | Method: lookupRGB 1077 | 1078 | looks for the closest *r*, *g*, *b* color in the map and 1079 | returns its index 1080 | */ 1081 | this.lookupRGB = inxsearch; 1082 | } 1083 | 1084 | module.exports = NeuQuant; 1085 | },{}]},{},[1]); 1086 | -------------------------------------------------------------------------------- /src/demos/gif-stream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

Loading service worker…

13 |
14 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/demos/gif-stream/regenerator-runtime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * https://raw.github.com/facebook/regenerator/master/LICENSE file. An 7 | * additional grant of patent rights can be found in the PATENTS file in 8 | * the same directory. 9 | */ 10 | 11 | !(function(global) { 12 | "use strict"; 13 | 14 | var hasOwn = Object.prototype.hasOwnProperty; 15 | var undefined; // More compressible than void 0. 16 | var $Symbol = typeof Symbol === "function" ? Symbol : {}; 17 | var iteratorSymbol = $Symbol.iterator || "@@iterator"; 18 | var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; 19 | 20 | var inModule = typeof module === "object"; 21 | var runtime = global.regeneratorRuntime; 22 | if (runtime) { 23 | if (inModule) { 24 | // If regeneratorRuntime is defined globally and we're in a module, 25 | // make the exports object identical to regeneratorRuntime. 26 | module.exports = runtime; 27 | } 28 | // Don't bother evaluating the rest of this file if the runtime was 29 | // already defined globally. 30 | return; 31 | } 32 | 33 | // Define the runtime globally (as expected by generated code) as either 34 | // module.exports (if we're in a module) or a new, empty object. 35 | runtime = global.regeneratorRuntime = inModule ? module.exports : {}; 36 | 37 | function wrap(innerFn, outerFn, self, tryLocsList) { 38 | // If outerFn provided, then outerFn.prototype instanceof Generator. 39 | var generator = Object.create((outerFn || Generator).prototype); 40 | var context = new Context(tryLocsList || []); 41 | 42 | // The ._invoke method unifies the implementations of the .next, 43 | // .throw, and .return methods. 44 | generator._invoke = makeInvokeMethod(innerFn, self, context); 45 | 46 | return generator; 47 | } 48 | runtime.wrap = wrap; 49 | 50 | // Try/catch helper to minimize deoptimizations. Returns a completion 51 | // record like context.tryEntries[i].completion. This interface could 52 | // have been (and was previously) designed to take a closure to be 53 | // invoked without arguments, but in all the cases we care about we 54 | // already have an existing method we want to call, so there's no need 55 | // to create a new function object. We can even get away with assuming 56 | // the method takes exactly one argument, since that happens to be true 57 | // in every case, so we don't have to touch the arguments object. The 58 | // only additional allocation required is the completion record, which 59 | // has a stable shape and so hopefully should be cheap to allocate. 60 | function tryCatch(fn, obj, arg) { 61 | try { 62 | return { type: "normal", arg: fn.call(obj, arg) }; 63 | } catch (err) { 64 | return { type: "throw", arg: err }; 65 | } 66 | } 67 | 68 | var GenStateSuspendedStart = "suspendedStart"; 69 | var GenStateSuspendedYield = "suspendedYield"; 70 | var GenStateExecuting = "executing"; 71 | var GenStateCompleted = "completed"; 72 | 73 | // Returning this object from the innerFn has the same effect as 74 | // breaking out of the dispatch switch statement. 75 | var ContinueSentinel = {}; 76 | 77 | // Dummy constructor functions that we use as the .constructor and 78 | // .constructor.prototype properties for functions that return Generator 79 | // objects. For full spec compliance, you may wish to configure your 80 | // minifier not to mangle the names of these two functions. 81 | function Generator() {} 82 | function GeneratorFunction() {} 83 | function GeneratorFunctionPrototype() {} 84 | 85 | var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype; 86 | GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; 87 | GeneratorFunctionPrototype.constructor = GeneratorFunction; 88 | GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction"; 89 | 90 | // Helper for defining the .next, .throw, and .return methods of the 91 | // Iterator interface in terms of a single ._invoke method. 92 | function defineIteratorMethods(prototype) { 93 | ["next", "throw", "return"].forEach(function(method) { 94 | prototype[method] = function(arg) { 95 | return this._invoke(method, arg); 96 | }; 97 | }); 98 | } 99 | 100 | runtime.isGeneratorFunction = function(genFun) { 101 | var ctor = typeof genFun === "function" && genFun.constructor; 102 | return ctor 103 | ? ctor === GeneratorFunction || 104 | // For the native GeneratorFunction constructor, the best we can 105 | // do is to check its .name property. 106 | (ctor.displayName || ctor.name) === "GeneratorFunction" 107 | : false; 108 | }; 109 | 110 | runtime.mark = function(genFun) { 111 | if (Object.setPrototypeOf) { 112 | Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); 113 | } else { 114 | genFun.__proto__ = GeneratorFunctionPrototype; 115 | if (!(toStringTagSymbol in genFun)) { 116 | genFun[toStringTagSymbol] = "GeneratorFunction"; 117 | } 118 | } 119 | genFun.prototype = Object.create(Gp); 120 | return genFun; 121 | }; 122 | 123 | // Within the body of any async function, `await x` is transformed to 124 | // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test 125 | // `value instanceof AwaitArgument` to determine if the yielded value is 126 | // meant to be awaited. Some may consider the name of this method too 127 | // cutesy, but they are curmudgeons. 128 | runtime.awrap = function(arg) { 129 | return new AwaitArgument(arg); 130 | }; 131 | 132 | function AwaitArgument(arg) { 133 | this.arg = arg; 134 | } 135 | 136 | function AsyncIterator(generator) { 137 | function invoke(method, arg, resolve, reject) { 138 | var record = tryCatch(generator[method], generator, arg); 139 | if (record.type === "throw") { 140 | reject(record.arg); 141 | } else { 142 | var result = record.arg; 143 | var value = result.value; 144 | if (value instanceof AwaitArgument) { 145 | return Promise.resolve(value.arg).then(function(value) { 146 | invoke("next", value, resolve, reject); 147 | }, function(err) { 148 | invoke("throw", err, resolve, reject); 149 | }); 150 | } 151 | 152 | return Promise.resolve(value).then(function(unwrapped) { 153 | // When a yielded Promise is resolved, its final value becomes 154 | // the .value of the Promise<{value,done}> result for the 155 | // current iteration. If the Promise is rejected, however, the 156 | // result for this iteration will be rejected with the same 157 | // reason. Note that rejections of yielded Promises are not 158 | // thrown back into the generator function, as is the case 159 | // when an awaited Promise is rejected. This difference in 160 | // behavior between yield and await is important, because it 161 | // allows the consumer to decide what to do with the yielded 162 | // rejection (swallow it and continue, manually .throw it back 163 | // into the generator, abandon iteration, whatever). With 164 | // await, by contrast, there is no opportunity to examine the 165 | // rejection reason outside the generator function, so the 166 | // only option is to throw it from the await expression, and 167 | // let the generator function handle the exception. 168 | result.value = unwrapped; 169 | resolve(result); 170 | }, reject); 171 | } 172 | } 173 | 174 | if (typeof process === "object" && process.domain) { 175 | invoke = process.domain.bind(invoke); 176 | } 177 | 178 | var previousPromise; 179 | 180 | function enqueue(method, arg) { 181 | function callInvokeWithMethodAndArg() { 182 | return new Promise(function(resolve, reject) { 183 | invoke(method, arg, resolve, reject); 184 | }); 185 | } 186 | 187 | return previousPromise = 188 | // If enqueue has been called before, then we want to wait until 189 | // all previous Promises have been resolved before calling invoke, 190 | // so that results are always delivered in the correct order. If 191 | // enqueue has not been called before, then it is important to 192 | // call invoke immediately, without waiting on a callback to fire, 193 | // so that the async generator function has the opportunity to do 194 | // any necessary setup in a predictable way. This predictability 195 | // is why the Promise constructor synchronously invokes its 196 | // executor callback, and why async functions synchronously 197 | // execute code before the first await. Since we implement simple 198 | // async functions in terms of async generators, it is especially 199 | // important to get this right, even though it requires care. 200 | previousPromise ? previousPromise.then( 201 | callInvokeWithMethodAndArg, 202 | // Avoid propagating failures to Promises returned by later 203 | // invocations of the iterator. 204 | callInvokeWithMethodAndArg 205 | ) : callInvokeWithMethodAndArg(); 206 | } 207 | 208 | // Define the unified helper method that is used to implement .next, 209 | // .throw, and .return (see defineIteratorMethods). 210 | this._invoke = enqueue; 211 | } 212 | 213 | defineIteratorMethods(AsyncIterator.prototype); 214 | 215 | // Note that simple async functions are implemented on top of 216 | // AsyncIterator objects; they just return a Promise for the value of 217 | // the final result produced by the iterator. 218 | runtime.async = function(innerFn, outerFn, self, tryLocsList) { 219 | var iter = new AsyncIterator( 220 | wrap(innerFn, outerFn, self, tryLocsList) 221 | ); 222 | 223 | return runtime.isGeneratorFunction(outerFn) 224 | ? iter // If outerFn is a generator, return the full iterator. 225 | : iter.next().then(function(result) { 226 | return result.done ? result.value : iter.next(); 227 | }); 228 | }; 229 | 230 | function makeInvokeMethod(innerFn, self, context) { 231 | var state = GenStateSuspendedStart; 232 | 233 | return function invoke(method, arg) { 234 | if (state === GenStateExecuting) { 235 | throw new Error("Generator is already running"); 236 | } 237 | 238 | if (state === GenStateCompleted) { 239 | if (method === "throw") { 240 | throw arg; 241 | } 242 | 243 | // Be forgiving, per 25.3.3.3.3 of the spec: 244 | // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume 245 | return doneResult(); 246 | } 247 | 248 | while (true) { 249 | var delegate = context.delegate; 250 | if (delegate) { 251 | if (method === "return" || 252 | (method === "throw" && delegate.iterator[method] === undefined)) { 253 | // A return or throw (when the delegate iterator has no throw 254 | // method) always terminates the yield* loop. 255 | context.delegate = null; 256 | 257 | // If the delegate iterator has a return method, give it a 258 | // chance to clean up. 259 | var returnMethod = delegate.iterator["return"]; 260 | if (returnMethod) { 261 | var record = tryCatch(returnMethod, delegate.iterator, arg); 262 | if (record.type === "throw") { 263 | // If the return method threw an exception, let that 264 | // exception prevail over the original return or throw. 265 | method = "throw"; 266 | arg = record.arg; 267 | continue; 268 | } 269 | } 270 | 271 | if (method === "return") { 272 | // Continue with the outer return, now that the delegate 273 | // iterator has been terminated. 274 | continue; 275 | } 276 | } 277 | 278 | var record = tryCatch( 279 | delegate.iterator[method], 280 | delegate.iterator, 281 | arg 282 | ); 283 | 284 | if (record.type === "throw") { 285 | context.delegate = null; 286 | 287 | // Like returning generator.throw(uncaught), but without the 288 | // overhead of an extra function call. 289 | method = "throw"; 290 | arg = record.arg; 291 | continue; 292 | } 293 | 294 | // Delegate generator ran and handled its own exceptions so 295 | // regardless of what the method was, we continue as if it is 296 | // "next" with an undefined arg. 297 | method = "next"; 298 | arg = undefined; 299 | 300 | var info = record.arg; 301 | if (info.done) { 302 | context[delegate.resultName] = info.value; 303 | context.next = delegate.nextLoc; 304 | } else { 305 | state = GenStateSuspendedYield; 306 | return info; 307 | } 308 | 309 | context.delegate = null; 310 | } 311 | 312 | if (method === "next") { 313 | if (state === GenStateSuspendedYield) { 314 | context.sent = arg; 315 | } else { 316 | context.sent = undefined; 317 | } 318 | 319 | } else if (method === "throw") { 320 | if (state === GenStateSuspendedStart) { 321 | state = GenStateCompleted; 322 | throw arg; 323 | } 324 | 325 | if (context.dispatchException(arg)) { 326 | // If the dispatched exception was caught by a catch block, 327 | // then let that catch block handle the exception normally. 328 | method = "next"; 329 | arg = undefined; 330 | } 331 | 332 | } else if (method === "return") { 333 | context.abrupt("return", arg); 334 | } 335 | 336 | state = GenStateExecuting; 337 | 338 | var record = tryCatch(innerFn, self, context); 339 | if (record.type === "normal") { 340 | // If an exception is thrown from innerFn, we leave state === 341 | // GenStateExecuting and loop back for another invocation. 342 | state = context.done 343 | ? GenStateCompleted 344 | : GenStateSuspendedYield; 345 | 346 | var info = { 347 | value: record.arg, 348 | done: context.done 349 | }; 350 | 351 | if (record.arg === ContinueSentinel) { 352 | if (context.delegate && method === "next") { 353 | // Deliberately forget the last sent value so that we don't 354 | // accidentally pass it on to the delegate. 355 | arg = undefined; 356 | } 357 | } else { 358 | return info; 359 | } 360 | 361 | } else if (record.type === "throw") { 362 | state = GenStateCompleted; 363 | // Dispatch the exception by looping back around to the 364 | // context.dispatchException(arg) call above. 365 | method = "throw"; 366 | arg = record.arg; 367 | } 368 | } 369 | }; 370 | } 371 | 372 | // Define Generator.prototype.{next,throw,return} in terms of the 373 | // unified ._invoke helper method. 374 | defineIteratorMethods(Gp); 375 | 376 | Gp[iteratorSymbol] = function() { 377 | return this; 378 | }; 379 | 380 | Gp[toStringTagSymbol] = "Generator"; 381 | 382 | Gp.toString = function() { 383 | return "[object Generator]"; 384 | }; 385 | 386 | function pushTryEntry(locs) { 387 | var entry = { tryLoc: locs[0] }; 388 | 389 | if (1 in locs) { 390 | entry.catchLoc = locs[1]; 391 | } 392 | 393 | if (2 in locs) { 394 | entry.finallyLoc = locs[2]; 395 | entry.afterLoc = locs[3]; 396 | } 397 | 398 | this.tryEntries.push(entry); 399 | } 400 | 401 | function resetTryEntry(entry) { 402 | var record = entry.completion || {}; 403 | record.type = "normal"; 404 | delete record.arg; 405 | entry.completion = record; 406 | } 407 | 408 | function Context(tryLocsList) { 409 | // The root entry object (effectively a try statement without a catch 410 | // or a finally block) gives us a place to store values thrown from 411 | // locations where there is no enclosing try statement. 412 | this.tryEntries = [{ tryLoc: "root" }]; 413 | tryLocsList.forEach(pushTryEntry, this); 414 | this.reset(true); 415 | } 416 | 417 | runtime.keys = function(object) { 418 | var keys = []; 419 | for (var key in object) { 420 | keys.push(key); 421 | } 422 | keys.reverse(); 423 | 424 | // Rather than returning an object with a next method, we keep 425 | // things simple and return the next function itself. 426 | return function next() { 427 | while (keys.length) { 428 | var key = keys.pop(); 429 | if (key in object) { 430 | next.value = key; 431 | next.done = false; 432 | return next; 433 | } 434 | } 435 | 436 | // To avoid creating an additional object, we just hang the .value 437 | // and .done properties off the next function object itself. This 438 | // also ensures that the minifier will not anonymize the function. 439 | next.done = true; 440 | return next; 441 | }; 442 | }; 443 | 444 | function values(iterable) { 445 | if (iterable) { 446 | var iteratorMethod = iterable[iteratorSymbol]; 447 | if (iteratorMethod) { 448 | return iteratorMethod.call(iterable); 449 | } 450 | 451 | if (typeof iterable.next === "function") { 452 | return iterable; 453 | } 454 | 455 | if (!isNaN(iterable.length)) { 456 | var i = -1, next = function next() { 457 | while (++i < iterable.length) { 458 | if (hasOwn.call(iterable, i)) { 459 | next.value = iterable[i]; 460 | next.done = false; 461 | return next; 462 | } 463 | } 464 | 465 | next.value = undefined; 466 | next.done = true; 467 | 468 | return next; 469 | }; 470 | 471 | return next.next = next; 472 | } 473 | } 474 | 475 | // Return an iterator with no values. 476 | return { next: doneResult }; 477 | } 478 | runtime.values = values; 479 | 480 | function doneResult() { 481 | return { value: undefined, done: true }; 482 | } 483 | 484 | Context.prototype = { 485 | constructor: Context, 486 | 487 | reset: function(skipTempReset) { 488 | this.prev = 0; 489 | this.next = 0; 490 | this.sent = undefined; 491 | this.done = false; 492 | this.delegate = null; 493 | 494 | this.tryEntries.forEach(resetTryEntry); 495 | 496 | if (!skipTempReset) { 497 | for (var name in this) { 498 | // Not sure about the optimal order of these conditions: 499 | if (name.charAt(0) === "t" && 500 | hasOwn.call(this, name) && 501 | !isNaN(+name.slice(1))) { 502 | this[name] = undefined; 503 | } 504 | } 505 | } 506 | }, 507 | 508 | stop: function() { 509 | this.done = true; 510 | 511 | var rootEntry = this.tryEntries[0]; 512 | var rootRecord = rootEntry.completion; 513 | if (rootRecord.type === "throw") { 514 | throw rootRecord.arg; 515 | } 516 | 517 | return this.rval; 518 | }, 519 | 520 | dispatchException: function(exception) { 521 | if (this.done) { 522 | throw exception; 523 | } 524 | 525 | var context = this; 526 | function handle(loc, caught) { 527 | record.type = "throw"; 528 | record.arg = exception; 529 | context.next = loc; 530 | return !!caught; 531 | } 532 | 533 | for (var i = this.tryEntries.length - 1; i >= 0; --i) { 534 | var entry = this.tryEntries[i]; 535 | var record = entry.completion; 536 | 537 | if (entry.tryLoc === "root") { 538 | // Exception thrown outside of any try block that could handle 539 | // it, so set the completion value of the entire function to 540 | // throw the exception. 541 | return handle("end"); 542 | } 543 | 544 | if (entry.tryLoc <= this.prev) { 545 | var hasCatch = hasOwn.call(entry, "catchLoc"); 546 | var hasFinally = hasOwn.call(entry, "finallyLoc"); 547 | 548 | if (hasCatch && hasFinally) { 549 | if (this.prev < entry.catchLoc) { 550 | return handle(entry.catchLoc, true); 551 | } else if (this.prev < entry.finallyLoc) { 552 | return handle(entry.finallyLoc); 553 | } 554 | 555 | } else if (hasCatch) { 556 | if (this.prev < entry.catchLoc) { 557 | return handle(entry.catchLoc, true); 558 | } 559 | 560 | } else if (hasFinally) { 561 | if (this.prev < entry.finallyLoc) { 562 | return handle(entry.finallyLoc); 563 | } 564 | 565 | } else { 566 | throw new Error("try statement without catch or finally"); 567 | } 568 | } 569 | } 570 | }, 571 | 572 | abrupt: function(type, arg) { 573 | for (var i = this.tryEntries.length - 1; i >= 0; --i) { 574 | var entry = this.tryEntries[i]; 575 | if (entry.tryLoc <= this.prev && 576 | hasOwn.call(entry, "finallyLoc") && 577 | this.prev < entry.finallyLoc) { 578 | var finallyEntry = entry; 579 | break; 580 | } 581 | } 582 | 583 | if (finallyEntry && 584 | (type === "break" || 585 | type === "continue") && 586 | finallyEntry.tryLoc <= arg && 587 | arg <= finallyEntry.finallyLoc) { 588 | // Ignore the finally entry if control is not jumping to a 589 | // location outside the try/catch block. 590 | finallyEntry = null; 591 | } 592 | 593 | var record = finallyEntry ? finallyEntry.completion : {}; 594 | record.type = type; 595 | record.arg = arg; 596 | 597 | if (finallyEntry) { 598 | this.next = finallyEntry.finallyLoc; 599 | } else { 600 | this.complete(record); 601 | } 602 | 603 | return ContinueSentinel; 604 | }, 605 | 606 | complete: function(record, afterLoc) { 607 | if (record.type === "throw") { 608 | throw record.arg; 609 | } 610 | 611 | if (record.type === "break" || 612 | record.type === "continue") { 613 | this.next = record.arg; 614 | } else if (record.type === "return") { 615 | this.rval = record.arg; 616 | this.next = "end"; 617 | } else if (record.type === "normal" && afterLoc) { 618 | this.next = afterLoc; 619 | } 620 | }, 621 | 622 | finish: function(finallyLoc) { 623 | for (var i = this.tryEntries.length - 1; i >= 0; --i) { 624 | var entry = this.tryEntries[i]; 625 | if (entry.finallyLoc === finallyLoc) { 626 | this.complete(entry.completion, entry.afterLoc); 627 | resetTryEntry(entry); 628 | return ContinueSentinel; 629 | } 630 | } 631 | }, 632 | 633 | "catch": function(tryLoc) { 634 | for (var i = this.tryEntries.length - 1; i >= 0; --i) { 635 | var entry = this.tryEntries[i]; 636 | if (entry.tryLoc === tryLoc) { 637 | var record = entry.completion; 638 | if (record.type === "throw") { 639 | var thrown = record.arg; 640 | resetTryEntry(entry); 641 | } 642 | return thrown; 643 | } 644 | } 645 | 646 | // The context.catch method must only be called with a location 647 | // argument that corresponds to a known catch block. 648 | throw new Error("illegal catch attempt"); 649 | }, 650 | 651 | delegateYield: function(iterable, resultName, nextLoc) { 652 | this.delegate = { 653 | iterator: values(iterable), 654 | resultName: resultName, 655 | nextLoc: nextLoc 656 | }; 657 | 658 | return ContinueSentinel; 659 | } 660 | }; 661 | })( 662 | // Among the various tricks for obtaining a reference to the global 663 | // object, this seems to be the most reliable technique that does not 664 | // use indirect eval (which violates Content Security Policy). 665 | typeof global === "object" ? global : 666 | typeof window === "object" ? window : 667 | typeof self === "object" ? self : this 668 | ); -------------------------------------------------------------------------------- /src/demos/gif-stream/stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 |

Decoding MPEG

16 | 17 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/demos/gif-stream/sw.js: -------------------------------------------------------------------------------- 1 | importScripts('jsmpeg.js', 'gif.js'); 2 | 3 | self.addEventListener('install', event => { 4 | self.skipWaiting(); 5 | }); 6 | 7 | self.addEventListener('activate', event => { 8 | clients.claim(); 9 | }); 10 | 11 | self.addEventListener('fetch', event => { 12 | const requestURL = new URL(event.request.url); 13 | 14 | if (requestURL.origin != location.origin) return; 15 | 16 | if (requestURL.pathname.endsWith("/demos/gif-stream/")) { 17 | event.respondWith(fetch('gif.html')); 18 | return; 19 | } 20 | 21 | if (requestURL.pathname.endsWith(".gif")) { 22 | event.respondWith(streamGIF(requestURL.pathname.replace(/\.gif$/, '.mpg'))); 23 | return; 24 | } 25 | }); 26 | 27 | function streamGIF(url) { 28 | return fetch(url).then(response => { 29 | const jsmpeg = new JSMPEG(); 30 | const reader = response.body.getReader(); 31 | 32 | function readToJSMPEG() { 33 | return reader.read().then(result => { 34 | if (result.done) { 35 | jsmpeg.writeEnd(); 36 | return; 37 | } 38 | jsmpeg.write(result.value); 39 | return readToJSMPEG(); 40 | }); 41 | } 42 | 43 | // read the response into jsmpeg 44 | readToJSMPEG(); 45 | 46 | // wait for width/height info 47 | return jsmpeg.ready.then(function() { 48 | const gif = new GIFEncoder(jsmpeg.width, jsmpeg.height); 49 | gif.start(); 50 | gif.setQuality(10); 51 | gif.setRepeat(0); 52 | gif.setFrameRate(jsmpeg.pictureRate); 53 | 54 | const frameReader = jsmpeg.readable.getReader(); 55 | 56 | function readFramesToGIF() { 57 | frameReader.read().then(function(result) { 58 | if (result.done) { 59 | gif.finish(); 60 | return; 61 | } 62 | gif.addFrame(result.value); 63 | setTimeout(readFramesToGIF, 0); 64 | }); 65 | } 66 | 67 | readFramesToGIF(); 68 | 69 | return new Response(gif.readable, { 70 | headers: {'Content-Type': 'image/gif'} 71 | }); 72 | }); 73 | }); 74 | } -------------------------------------------------------------------------------- /src/demos/globalapis/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /src/demos/globalapis/sw.js: -------------------------------------------------------------------------------- 1 | console.log("SW startup"); 2 | console.log("Request", this.Request); 3 | console.log("Response", this.Response); 4 | console.log("fetch", this.fetch); 5 | console.log("Cache", this.Cache); 6 | console.log("caches", this.caches); 7 | console.log("getAll", this.getAll); 8 | -------------------------------------------------------------------------------- /src/demos/headers-log/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/demos/headers-log/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', function(event) { 2 | console.log("Fetching", event.request.url); 3 | console.log("Headers", new Set(event.request.headers)); 4 | event.respondWith(fetch(event.request)); 5 | }); 6 | -------------------------------------------------------------------------------- /src/demos/http-redirect/blah/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

13 | It worked! 14 |

15 | 16 | 17 | -------------------------------------------------------------------------------- /src/demos/http-redirect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

13 | This test is reliant on a bug in github pages where a 14 | relative url such as 15 | blah will redirect to the http 16 | version of blah/ if blah/index.html 17 | exists. 18 |

19 |

Click me

20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/demos/http-redirect/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('activate', _ => { 2 | clients.claim(); 3 | }); 4 | 5 | self.addEventListener('fetch', event => { 6 | console.log(event.request); 7 | event.respondWith( 8 | fetch(event.request).catch(function() { 9 | return new Response("The fetch, it failed :("); 10 | }) 11 | ); 12 | }); -------------------------------------------------------------------------------- /src/demos/img-rewrite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 33 | 34 | -------------------------------------------------------------------------------- /src/demos/img-rewrite/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', function(event) { 2 | if (/\.jpg$/.test(event.request.url)) { 3 | event.respondWith( 4 | fetch('https://www.google.co.uk/logos/doodles/2014/60th-anniversary-of-the-unveiling-of-the-first-routemaster-bus-4922931108904960.3-hp.gif', { 5 | mode: 'no-cors' 6 | }) 7 | ); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/demos/install-fail/error-thrown-oninstall/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /src/demos/install-fail/error-thrown-oninstall/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', function(event) { 2 | throw Error("This should fail the install"); 3 | }); 4 | 5 | self.addEventListener('fetch', function(event) { 6 | // we never get here 7 | event.respondWith(new Response("If you get this response, there's a bug")); 8 | }); 9 | -------------------------------------------------------------------------------- /src/demos/install-fail/execution-error/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /src/demos/install-fail/execution-error/sw.js: -------------------------------------------------------------------------------- 1 | throw Error("Faillllllll"); 2 | 3 | self.addEventListener('fetch', function(event) { 4 | // we never get here 5 | event.respondWith(new Response("If you get this response, there's a bug")); 6 | }); 7 | -------------------------------------------------------------------------------- /src/demos/install-fail/rejected-promise/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /src/demos/install-fail/rejected-promise/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', function(event) { 2 | debugger; 3 | event.waitUntil(doSomeStuff()); 4 | }); 5 | 6 | self.addEventListener('fetch', function(event) { 7 | // we never get here 8 | event.respondWith(new Response("If you get this response, there's a bug")); 9 | }); 10 | 11 | function doSomeStuff() { 12 | return new Promise(function(resolve) { 13 | setTimeout(resolve, 5000); 14 | }).then(function() { 15 | return functionDoesNotExist(); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/demos/installactivate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /src/demos/installactivate/sw.js: -------------------------------------------------------------------------------- 1 | console.log("SW startup"); 2 | 3 | this.oninstall = function(event) { 4 | console.log("Install event", event); 5 | console.log(".replace", event.replace); 6 | console.log("self.skipWaiting", self.skipWaiting); 7 | 8 | if (event.waitUntil) { 9 | console.log("Testing waitUntil:"); 10 | event.waitUntil(new Promise(function(resolve) { 11 | setTimeout(function() { 12 | console.log("This should appear before activate"); 13 | resolve(); 14 | }, 3000); 15 | })); 16 | } 17 | }; 18 | 19 | this.onactivate = function(event) { 20 | console.log("Activate event", event); 21 | console.log(".waitUntil", event.waitUntil); 22 | }; 23 | -------------------------------------------------------------------------------- /src/demos/json-stream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | 22 |

Streaming JSON demo

23 | 24 | 25 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /src/demos/manual-response/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /src/demos/manual-response/sw.js: -------------------------------------------------------------------------------- 1 | // The SW will be shutdown when not in use to save memory, 2 | // be aware that any global state is likely to disappear 3 | console.log("SW startup"); 4 | 5 | self.addEventListener('install', function(event) { 6 | console.log("SW installed"); 7 | }); 8 | 9 | self.addEventListener('activate', function(event) { 10 | console.log("SW activated"); 11 | }); 12 | 13 | self.addEventListener('fetch', function(event) { 14 | console.log("Caught a fetch!"); 15 | event.respondWith(new Response("Hello world!")); 16 | }); 17 | -------------------------------------------------------------------------------- /src/demos/nav-preload/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | font-size: 14px; 4 | line-height: 1.5; 5 | color: #333; 6 | background-color: #fff; 7 | } 8 | .content { 9 | position: relative; 10 | margin: 1rem 0; 11 | } 12 | .actions li { 13 | margin: 1rem 0; 14 | } 15 | .actions form { 16 | display: inline; 17 | } 18 | .pl-c { color: rgb(150, 152, 150); } 19 | .pl-c1, .pl-s .pl-v { color: rgb(0, 134, 179); } 20 | .pl-e, .pl-en { color: rgb(121, 93, 163); } 21 | .pl-smi, .pl-s .pl-s1 { color: rgb(51, 51, 51); } 22 | .pl-k { color: rgb(167, 29, 93); } 23 | .pl-s, .pl-pds, .pl-s .pl-pse .pl-s1, .pl-sr, .pl-sr .pl-cce, .pl-sr .pl-sre, .pl-sr .pl-sra { color: rgb(24, 54, 145); } 24 | .pl-v { color: rgb(237, 106, 67); } 25 | .octicon { display: inline-block; vertical-align: text-top; fill: currentcolor; } 26 | a { background-color: transparent; } 27 | b, strong { font-weight: inherit; } 28 | b, strong { font-weight: bolder; } 29 | img { border-style: none; } 30 | svg:not(:root) { overflow: hidden; } 31 | code, kbd, pre, samp { font-family: monospace, monospace; font-size: 1em; } 32 | hr { box-sizing: content-box; height: 0px; overflow: visible; } 33 | button, input, select, textarea { font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; font-size: inherit; line-height: inherit; font-family: inherit; margin: 0px; } 34 | button, input { overflow: visible; } 35 | button, select { text-transform: none; } 36 | button, html [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } 37 | * { box-sizing: border-box; } 38 | input, select, textarea, button { font-family: inherit; font-size: inherit; line-height: inherit; } 39 | a { color: rgb(64, 120, 192); text-decoration: none; } 40 | strong { font-weight: 600; } 41 | hr, .rule { height: 0px; margin: 15px 0px; overflow: hidden; background: transparent; border-width: 0px 0px 1px; border-top-style: initial; border-right-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-left-color: initial; border-image: initial; border-bottom-style: solid; border-bottom-color: rgb(221, 221, 221); } 42 | hr::before, .rule::before { display: table; content: ""; } 43 | table { border-spacing: 0px; border-collapse: collapse; } 44 | td, th { padding: 0px; } 45 | button { cursor: pointer; } 46 | h1, h2, h3, h4, h5, h6 { margin-top: 0px; margin-bottom: 0px; } 47 | h2 { font-size: 24px; font-weight: 600; } 48 | h3 { font-size: 20px; font-weight: 600; } 49 | h4 { font-size: 16px; font-weight: 600; } 50 | h5 { font-size: 14px; font-weight: 600; } 51 | p { margin-top: 0px; margin-bottom: 10px; } 52 | blockquote { margin: 0px; } 53 | ul, ol { padding-left: 0px; margin-top: 0px; margin-bottom: 0px; } 54 | ol ol, ul ol { list-style-type: lower-roman; } 55 | tt, code { font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; } 56 | pre { margin-top: 0px; margin-bottom: 0px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 12px; line-height: normal; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 57 | .octicon { vertical-align: text-bottom; } 58 | .btn { position: relative; display: inline-block; padding: 6px 12px; font-size: 14px; font-weight: 600; line-height: 20px; color: rgb(51, 51, 51); white-space: nowrap; vertical-align: middle; cursor: pointer; user-select: none; background-color: rgb(238, 238, 238); background-image: linear-gradient(rgb(252, 252, 252), rgb(238, 238, 238)); border: 1px solid rgb(213, 213, 213); border-radius: 3px; -webkit-appearance: none; } 59 | .btn-primary { color: rgb(255, 255, 255); text-shadow: rgba(0, 0, 0, 0.14902) 0px -1px 0px; background-color: rgb(108, 198, 68); background-image: linear-gradient(rgb(145, 221, 112), rgb(85, 174, 46)); border: 1px solid rgb(90, 173, 53); } 60 | .hidden-text-expander { display: block; } 61 | .hidden-text-expander.inline { position: relative; top: -1px; display: inline-block; margin-left: 5px; line-height: 0; } 62 | .hidden-text-expander a, .ellipsis-expander { display: inline-block; height: 12px; padding: 0px 5px 5px; font-size: 12px; font-weight: bold; line-height: 6px; color: rgb(85, 85, 85); text-decoration: none; vertical-align: middle; background: rgb(221, 221, 221); border: 0px; border-radius: 1px; } 63 | .btn-link { display: inline-block; padding: 0px; font-size: inherit; color: rgb(64, 120, 192); text-decoration: none; white-space: nowrap; cursor: pointer; user-select: none; background-color: transparent; border: 0px; -webkit-appearance: none; } 64 | .btn-link:disabled { color: rgb(118, 118, 118); pointer-events: none; cursor: default; } 65 | input, textarea { font-feature-settings: 'liga' 0; } 66 | .tooltipped { position: relative; } 67 | .tooltipped::after { position: absolute; z-index: 1000000; display: none; padding: 5px 8px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 11px; line-height: 1.5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; -webkit-font-smoothing: subpixel-antialiased; color: rgb(255, 255, 255); text-align: center; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-wrap: break-word; white-space: pre; pointer-events: none; content: attr(aria-label); background: rgba(0, 0, 0, 0.8); border-radius: 3px; opacity: 0; } 68 | .tooltipped::before { position: absolute; z-index: 1000001; display: none; width: 0px; height: 0px; color: rgba(0, 0, 0, 0.8); pointer-events: none; content: ""; border: 5px solid transparent; opacity: 0; } 69 | .tooltipped-s::before, .tooltipped-se::before, .tooltipped-sw::before { top: auto; right: 50%; bottom: -5px; margin-right: -5px; border-bottom-color: rgba(0, 0, 0, 0.8); } 70 | .tooltipped-se::after { right: auto; left: 50%; margin-left: -15px; } 71 | .tooltipped-n::before, .tooltipped-ne::before, .tooltipped-nw::before { top: -5px; right: 50%; bottom: auto; margin-right: -5px; border-top-color: rgba(0, 0, 0, 0.8); } 72 | .tooltipped-s::after, .tooltipped-n::after { transform: translateX(50%); } 73 | .tooltipped-multiline::after { width: max-content; max-width: 250px; word-break: break-word; word-wrap: normal; white-space: pre-line; border-collapse: separate; } 74 | .float-right { float: right !important; } 75 | .d-block { display: block !important; } 76 | .d-none { display: none !important; } 77 | .mr-1 { margin-right: 4px !important; } 78 | .text-bold { font-weight: 600 !important; } 79 | .avatar { display: inline-block; overflow: hidden; line-height: 1; vertical-align: middle; border-radius: 3px; } 80 | .avatar-small { border-radius: 2px; } 81 | .markdown-body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; line-height: 1.5; word-wrap: break-word; } 82 | .markdown-body::before { display: table; content: ""; } 83 | .markdown-body::after { display: table; clear: both; content: ""; } 84 | .markdown-body > :first-child { margin-top: 0px !important; } 85 | .markdown-body > :last-child { margin-bottom: 0px !important; } 86 | .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre { margin-top: 0px; margin-bottom: 16px; } 87 | .markdown-body hr { height: 0.25em; padding: 0px; margin: 24px 0px; background-color: rgb(231, 231, 231); border: 0px; } 88 | .markdown-body blockquote { padding: 0px 1em; color: rgb(119, 119, 119); border-left: 0.25em solid rgb(221, 221, 221); } 89 | .markdown-body blockquote > :first-child { margin-top: 0px; } 90 | .markdown-body blockquote > :last-child { margin-bottom: 0px; } 91 | .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; } 92 | .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { font-size: inherit; } 93 | .markdown-body h2 { padding-bottom: 0.3em; font-size: 1.5em; border-bottom: 1px solid rgb(238, 238, 238); } 94 | .markdown-body h3 { font-size: 1.25em; } 95 | .markdown-body h4 { font-size: 1em; } 96 | .markdown-body h5 { font-size: 0.875em; } 97 | .markdown-body ul, .markdown-body ol { padding-left: 2em; } 98 | .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0px; margin-bottom: 0px; } 99 | .markdown-body li + li { margin-top: 0.25em; } 100 | .markdown-body code, .markdown-body tt { padding: 0.2em 0px; margin: 0px; font-size: 85%; background-color: rgba(0, 0, 0, 0.0392157); border-radius: 3px; } 101 | .markdown-body code::before, .markdown-body code::after, .markdown-body tt::before, .markdown-body tt::after { letter-spacing: -0.2em; content: " "; } 102 | .markdown-body pre { word-wrap: normal; } 103 | .markdown-body pre > code { padding: 0px; margin: 0px; font-size: 100%; word-break: normal; white-space: pre; background: transparent; border: 0px; } 104 | .markdown-body .highlight { margin-bottom: 16px; } 105 | .markdown-body .highlight pre { margin-bottom: 0px; word-break: normal; } 106 | .markdown-body .highlight pre, .markdown-body pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: rgb(247, 247, 247); border-radius: 3px; } 107 | .markdown-body pre code, .markdown-body pre tt { display: inline; padding: 0px; margin: 0px; overflow: visible; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0px; } 108 | .markdown-body pre code::before, .markdown-body pre code::after, .markdown-body pre tt::before, .markdown-body pre tt::after { content: normal; } 109 | .state { display: inline-block; padding: 4px 8px; font-weight: 600; line-height: 20px; color: rgb(255, 255, 255); text-align: center; background-color: rgb(153, 153, 153); border-radius: 3px; } 110 | .state-open, .state-proposed, .state-reopened { background-color: rgb(108, 198, 68); } 111 | .state-closed { background-color: rgb(189, 44, 0); } 112 | .comment-body { width: 100%; padding: 15px; overflow: visible; font-size: 14px; } 113 | .comment-body .highlight { background-color: transparent; overflow: visible !important; } 114 | .commit-desc { display: none; } 115 | .commit-desc pre { max-width: 700px; margin-top: 10px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 11px; line-height: 1.45; color: rgb(89, 96, 99); white-space: pre-wrap; } 116 | .timeline-commits { width: 100%; margin-top: 5px; border-collapse: separate; } 117 | .timeline-commits td { padding-top: 4px; padding-right: 8px; padding-bottom: 4px; font-size: 12px; line-height: 16px; vertical-align: top; background-color: transparent; } 118 | .discussion-item .timeline-commits .commit-author { display: none; } 119 | .timeline-commits .commit-gravatar { width: 16px; padding-left: 10px; } 120 | .timeline-commits .commit-author { width: 200px; padding-right: 20px; white-space: nowrap; } 121 | .timeline-commits .author { font-weight: bold; color: rgb(85, 85, 85); } 122 | .timeline-commits .commit-message { max-width: 550px; min-height: 0px; } 123 | .timeline-commits .commit-message > code a { color: rgb(85, 85, 85); } 124 | .timeline-commits .commit-desc pre { overflow: visible; color: rgb(118, 118, 118); } 125 | .timeline-commits .hidden-text-expander { margin-top: 3px; margin-left: 0px; vertical-align: top; } 126 | .timeline-commits .hidden-text-expander .ellipsis-expander { height: 13px; background-color: rgb(238, 238, 238); } 127 | .timeline-commits .commit-sig-status { width: 60px; padding-right: 4px; text-align: right; } 128 | .timeline-commits .commit-ci-status { width: 16px; padding-right: 4px; } 129 | .timeline-commits .commit-meta { width: 50px; text-align: right; } 130 | .commit-icon { display: table-cell; width: 16px; color: rgb(204, 204, 204); } 131 | .commit-icon .octicon { background-color: rgb(255, 255, 255); } 132 | .commit-id { color: rgb(187, 187, 187); } 133 | .discussion-timeline::before { position: absolute; top: 0px; bottom: 0px; left: 79px; z-index: -1; display: block; width: 2px; content: ""; background-color: rgb(243, 243, 243); } 134 | .timeline-comment-wrapper > .timeline-comment::after, .timeline-comment-wrapper > .timeline-comment::before, .timeline-new-comment .timeline-comment::after, .timeline-new-comment .timeline-comment::before { position: absolute; top: 11px; right: 100%; left: -16px; display: block; width: 0px; height: 0px; pointer-events: none; content: " "; border-color: transparent; border-style: solid solid outset; } 135 | .timeline-comment-wrapper > .timeline-comment::before, .timeline-new-comment .timeline-comment::before { border-width: 8px; border-right-color: rgb(221, 221, 221); } 136 | .timeline-comment-wrapper { position: relative; padding-left: 60px; margin-top: 15px; margin-bottom: 15px; border-top: 2px solid rgb(255, 255, 255); border-bottom: 2px solid rgb(255, 255, 255); } 137 | .timeline-comment-wrapper:first-child { margin-top: 0px; } 138 | .timeline-comment-avatar { float: left; margin-left: -60px; border-radius: 3px; } 139 | .timeline-comment { position: relative; background-color: rgb(255, 255, 255); border: 1px solid rgb(221, 221, 221); border-radius: 3px; } 140 | .timeline-comment-header { padding-right: 15px; padding-left: 15px; color: rgb(118, 118, 118); background-color: rgb(247, 247, 247); border-bottom: 1px solid rgb(221, 221, 221); border-top-left-radius: 3px; border-top-right-radius: 3px; } 141 | .timeline-comment-header .author { color: rgb(85, 85, 85); } 142 | .timeline-comment-header .timestamp { color: inherit; white-space: nowrap; } 143 | .timeline-comment-header .timestamp.timestamp-edited { cursor: default; } 144 | .timeline-comment-label { float: right; padding: 2px 5px; margin: 8px 0px 0px 10px; font-size: 12px; cursor: default; border: 1px solid rgba(0, 0, 0, 0.0980392); border-radius: 3px; } 145 | .timeline-comment-header-text { max-width: 78%; padding-top: 10px; padding-bottom: 10px; } 146 | .timeline-comment-actions { float: right; margin-right: -5px; margin-left: 10px; } 147 | .discussion-item-ref .commit-gravatar { padding-right: 5px; padding-left: 2px; } 148 | .discussion-item-ref .state { padding: 1px 5px; margin-left: 8px; font-size: 12px; } 149 | .discussion-item-ref .state .octicon { width: 1em; font-size: 14px; } 150 | .discussion-item + .discussion-item, .discussion-item-review + .discussion-item { padding-top: 15px; border-top: 1px solid rgb(245, 245, 245); } 151 | .discussion-item { position: relative; padding-left: 25px; margin: 15px 0px 15px 79px; } 152 | .discussion-item .author { font-weight: 600; color: rgb(85, 85, 85); } 153 | .discussion-item .timestamp { color: inherit; white-space: nowrap; } 154 | .discussion-item-icon { float: left; width: 32px; height: 32px; margin-top: -7px; margin-left: -40px; line-height: 28px; color: rgb(118, 118, 118); text-align: center; background-color: rgb(243, 243, 243); border: 2px solid rgb(255, 255, 255); border-radius: 50%; } 155 | .discussion-item-icon .octicon-pencil { font-size: 14px; } 156 | .discussion-item-header { min-height: 30px; padding-top: 5px; padding-bottom: 5px; line-height: 20px; color: rgb(118, 118, 118); word-wrap: break-word; } 157 | .discussion-item-header .avatar { width: 16px; height: 16px; } 158 | .discussion-item-header:last-child { padding-bottom: 0px; } 159 | .discussion-item-entity { font-weight: 600; color: rgb(51, 51, 51); } 160 | .discussion-item-ref-title .issue-num { font-weight: normal; color: rgb(118, 118, 118); } 161 | .discussion-item-ref-title .title-link { color: rgb(51, 51, 51); } 162 | .discussion-item-rollup-ref .state { margin-top: 2px; } 163 | .discussion-item .renamed-was, .discussion-item .renamed-is { font-weight: bold; color: rgb(51, 51, 51); } 164 | .discussion-timeline-actions { background-color: rgb(255, 255, 255); border-top: 2px solid rgb(243, 243, 243); } 165 | g-emoji { font-family: "Apple Color Emoji", "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 18px; font-weight: normal; line-height: 20px; vertical-align: middle; } 166 | html.emoji-size-boost g-emoji { margin-right: 3px; } 167 | .user-mention, .team-mention { font-weight: 600; color: rgb(51, 51, 51); white-space: nowrap; } 168 | .comment-reactions::before { display: table; content: ""; } 169 | .comment-reactions::after { display: table; clear: both; content: ""; } 170 | .comment-reactions.has-reactions { border-top: 1px solid rgb(229, 229, 229); } 171 | .reaction-summary-item { float: left; padding: 9px 15px 7px; line-height: 18px; border-right: 1px solid rgb(229, 229, 229); } 172 | .comment-reactions-options .reaction-summary-item:first-child { border-bottom-left-radius: 2px; } 173 | .signed-out-comment { padding: 15px; margin-top: 15px; margin-left: 64px; background-color: rgb(255, 249, 234); border: 1px solid rgb(223, 216, 194); border-radius: 3px; } 174 | .signed-out-comment .btn { margin-right: 3px; vertical-align: baseline; } 175 | hr { border-bottom-color: rgb(238, 238, 238); } 176 | .btn-link { font-family: inherit; } 177 | .text-bold { font-weight: 500 !important; } -------------------------------------------------------------------------------- /src/demos/nav-preload/sw.js: -------------------------------------------------------------------------------- 1 | // Slow the serviceworker down a bit 2 | const start = Date.now(); 3 | while (Date.now() - start < 500); 4 | 5 | addEventListener('install', event => { 6 | event.waitUntil(async function() { 7 | const cache = await caches.open('nav-preload-demo-v1'); 8 | await cache.add('styles.css'); 9 | }()); 10 | }); 11 | 12 | addEventListener('activate', event => { 13 | event.waitUntil(async function() { 14 | if (self.registration.navigationPreload) { 15 | await self.registration.navigationPreload.enable(); 16 | } 17 | }()); 18 | }); 19 | 20 | addEventListener('fetch', event => { 21 | event.respondWith(async function() { 22 | // Respond from the cache if we can 23 | const cachedResponse = await caches.match(event.request); 24 | if (cachedResponse) return cachedResponse; 25 | 26 | // Use the preloaded response, if it's there 27 | const response = await event.preloadResponse; 28 | if (response) return response; 29 | 30 | // Else try the network. 31 | return fetch(event.request); 32 | }()); 33 | }); -------------------------------------------------------------------------------- /src/demos/navigator.serviceWorker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/demos/page-cache-bug/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 |

17 |

18 |

19 |

20 | 
89 | 
90 | 


--------------------------------------------------------------------------------
/src/demos/page-cache-bug/sw.js:
--------------------------------------------------------------------------------
 1 | self.oninstall = function() {
 2 |   self.skipWaiting();
 3 | };
 4 | 
 5 | self.onactivate = function() {
 6 |   clients.claim();
 7 | };
 8 | 
 9 | 
10 | self.onfetch = function(event) {
11 |   event.respondWith(
12 |     caches.match(event.request).then(function(response) {
13 |       return response || fetch(event.request);
14 |     })
15 |   );
16 | };


--------------------------------------------------------------------------------
/src/demos/postMessage/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
11 | 
12 | 
59 | 
60 | 


--------------------------------------------------------------------------------
/src/demos/postMessage/sw.js:
--------------------------------------------------------------------------------
 1 | this.onmessage = function(event) {
 2 |   console.log("Got message in SW", event.data.text);
 3 | 
 4 |   if (event.source) {
 5 |     console.log("event.source present");
 6 |     event.source.postMessage("Woop!");
 7 |   }
 8 |   else if (self.clients) {
 9 |     console.log("Attempting postMessage via clients API");
10 |     clients.matchAll().then(function(clients) {
11 |       for (var client of clients) {
12 |         client.postMessage("Whoop! (via client api)");
13 |       }
14 |     });
15 |   }
16 |   else if (event.data.port) {
17 |     event.data.port.postMessage("Woop!");
18 |   }
19 |   else {
20 |     console.log('No useful return channel');
21 |   }
22 | };
23 | 


--------------------------------------------------------------------------------
/src/demos/redirect/destination/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 9 | 
10 | 
11 |   
19 | 
20 | 
21 | 


--------------------------------------------------------------------------------
/src/demos/redirect/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 9 | 
10 | 
11 |   Click me!
12 |   
15 | 
16 | 
17 | 


--------------------------------------------------------------------------------
/src/demos/redirect/sw.js:
--------------------------------------------------------------------------------
1 | self.onfetch = function(event) {
2 |   event.respondWith(fetch(event.request));
3 | };


--------------------------------------------------------------------------------
/src/demos/registerunregister/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
11 | 
12 | 
46 | 
47 | 


--------------------------------------------------------------------------------
/src/demos/registerunregister/sw.js:
--------------------------------------------------------------------------------
1 | console.log("SW startup");
2 | 


--------------------------------------------------------------------------------
/src/demos/scope/app-a/index.html:
--------------------------------------------------------------------------------
1 | 
2 | Scope demo
3 | 
8 | 

App A

9 | back -------------------------------------------------------------------------------- /src/demos/scope/app-a/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | console.log('Intercepted fetch for', event.request.url); 3 | console.log('Intercepted by', location.href, 'with scope', registration.scope); 4 | }); -------------------------------------------------------------------------------- /src/demos/scope/app-b-sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | console.log('Intercepted fetch for', event.request.url); 3 | console.log('Intercepted by', location.href, 'with scope', registration.scope); 4 | }); -------------------------------------------------------------------------------- /src/demos/scope/app-b/index.html: -------------------------------------------------------------------------------- 1 | 2 | Scope demo 3 | 8 |

App B

9 | back -------------------------------------------------------------------------------- /src/demos/scope/index.html: -------------------------------------------------------------------------------- 1 | 2 | Scope demo 3 | 8 | 13 |

Managing multiple scopes

14 |

15 | This page registers 3 sevice workers: 16 |

17 | 22 |

23 | Usually these would be registered by each individual app, but I've done it all 24 | on this page so they're all registered by the time you visit the following pages. 25 |

26 | 32 |

33 | Each service worker logs when it intercepts a fetch, use "preserve log" in the console 34 | to see them all. 35 |

36 |

What you should see:

37 | -------------------------------------------------------------------------------- /src/demos/scope/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | console.log('Intercepted fetch for', event.request.url); 3 | console.log('Intercepted by', location.href, 'with scope', registration.scope); 4 | }); -------------------------------------------------------------------------------- /src/demos/simple-stream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

Loading service worker…

13 |
14 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/demos/simple-stream/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', event => { 2 | self.skipWaiting(); 3 | }); 4 | 5 | self.addEventListener('activate', event => { 6 | clients.claim(); 7 | }); 8 | 9 | self.addEventListener('fetch', event => { 10 | const requestURL = new URL(event.request.url); 11 | 12 | if (requestURL.origin != location.origin) return; 13 | 14 | if (requestURL.pathname.endsWith("/demos/simple-stream/")) { 15 | event.respondWith(umpaLumpaStream()); 16 | } 17 | }); 18 | 19 | function retab(str) { 20 | // remove blank lines 21 | str = str.replace(/^\s*\n|\n\s*$/g, ''); 22 | const firstIndent = /^\s*/.exec(str)[0]; 23 | return str.replace(RegExp('^' + firstIndent, 'mg'), ''); 24 | } 25 | 26 | function umpaLumpaStream() { 27 | const html = retab(` 28 | 29 | 34 | 35 |

The Oompa Loompa song

36 | 37 |

38 | Oompa loompa doompety doo
39 | I've got a perfect puzzle for you
40 | Oompa loompa doompety dee
41 | If you are wise you'll listen to me 42 |

43 | 44 |

45 | What do you get when you guzzle down sweets
46 | Eating as much as an elephant eats
47 | What are you at, getting terribly fat
48 | What do you think will come of that
49 | I don't like the look of it 50 |

51 | 52 |

53 | Oompa loompa doompety da
54 | If you're not greedy, you will go far
55 | You will live in happiness too
56 | Like the Oompa Loompa Doompety do
57 | Doompety do 58 |

59 | 60 |

61 | Oompa loompa doompety doo
62 | I've got another puzzle for you
63 | Oompa loompa doompeda dee
64 | If you are wise you'll listen to me 65 |

66 | 67 |

68 | Gum chewing's fine when it's once in a while
69 | It stops you from smoking and brightens your smile
70 | But it's repulsive, revolting and wrong
71 | Chewing and chewing all day long
72 | The way that a cow does 73 |

74 | 75 |

76 | Oompa loompa doompety da
77 | Given good manners you will go far
78 | You will live in happiness too
79 | Like the Oompa Loompa Doompety do
80 |

81 | 82 |

83 | Oompa loompa doompety doo
84 | I've got another puzzle for you
85 | Oompa loompa doompety dee
86 | If you are wise you'll listen to me 87 |

88 | 89 |

90 | Who do you blame when your kid is a brat
91 | Pampered and spoiled like a siamese cat
92 | Blaming the kids is a lie and a shame
93 | You know exactly who's to blame
94 | The mother and the father 95 |

96 | 97 |

98 | Oompa loompa doompety da
99 | If you're not spoiled then you will go far
100 | You will live in happiness too
101 | Like the Oompa Loompa Doompety do 102 |

103 | 104 |

105 | Oompa loompa doompety doo
106 | I've got another puzzle for you
107 | Oompa loompa doompeda dee
108 | If you are wise you'll listen to me 109 |

110 | 111 |

112 | What do you get from a glut of TV
113 | A pain in the neck and an IQ of three
114 | Why don't you try simply reading a book
115 | Or could you just not bear to look
116 | You'll get no
117 | You'll get no
118 | You'll get no
119 | You'll get no
120 | You'll get no commercials 121 |

122 | 123 |

124 | Oompa loompa doompety da
125 | If you're not greedy you will go far
126 | You will live in happiness too
127 | Like the - Oompa -
128 | Oompa Loompa Doompety do 129 |

130 | `); 131 | 132 | const stream = new ReadableStream({ 133 | start: controller => { 134 | const encoder = new TextEncoder(); 135 | let pos = 0; 136 | let chunkSize = 1; 137 | 138 | function push() { 139 | if (pos >= html.length) { 140 | controller.close(); 141 | return; 142 | } 143 | 144 | controller.enqueue( 145 | encoder.encode(html.slice(pos, pos + chunkSize)) 146 | ); 147 | 148 | pos += chunkSize; 149 | setTimeout(push, 5); 150 | } 151 | 152 | push(); 153 | } 154 | }); 155 | 156 | return new Response(stream, { 157 | headers: { 158 | 'Content-Type': 'text/html' 159 | } 160 | }); 161 | } -------------------------------------------------------------------------------- /src/demos/slow-update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /src/demos/slow-update/sw.js: -------------------------------------------------------------------------------- 1 | function wait(ms) { 2 | return new Promise(function(resolve) { 3 | setTimeout(resolve, ms); 4 | }); 5 | } 6 | 7 | self.addEventListener('install', function(event) { 8 | console.log("Installing…"); 9 | event.waitUntil( 10 | wait(5000).then(function() { 11 | console.log("Installed!"); 12 | }) 13 | ); 14 | }); 15 | 16 | self.addEventListener('activate', function(event) { 17 | console.log("Activating…"); 18 | event.waitUntil( 19 | wait(5000).then(function() { 20 | console.log("Activated!"); 21 | }) 22 | ); 23 | }); 24 | 25 | self.addEventListener('fetch', function(event) { 26 | event.respondWith(new Response("Hello everyone!")); 27 | }); 28 | -------------------------------------------------------------------------------- /src/demos/sync/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

13 | One-off background sync doesn't require permissions, but notifications do, 14 | and that's how we're going to tell you it worked. 15 |

16 |

Registering from the page

17 | 18 |
19 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/demos/sync/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', function(event) { 2 | self.skipWaiting(); 3 | }); 4 | 5 | self.addEventListener('sync', function(event) { 6 | self.registration.showNotification("Sync event fired!"); 7 | }); 8 | -------------------------------------------------------------------------------- /src/demos/template-stream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

Loading service worker…

13 |
14 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/demos/template-stream/prism.js: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=clike+javascript */ 2 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(m instanceof a)){u.lastIndex=0;var y=u.exec(m),v=1;if(!y&&h&&p!=r.length-1){var b=r[p+1].matchedStr||r[p+1],k=m+b;if(p=m.length)continue;var _=y.index+y[0].length,P=m.length+b.length;if(v=3,P>=_){if(r[p+1].greedy)continue;v=2,k=k.slice(0,P)}m=k}if(y){g&&(f=y[1].length);var w=y.index+f,y=y[0].slice(f),_=w+y.length,S=m.slice(0,w),O=m.slice(_),j=[p,v];S&&j.push(S);var A=new a(i,c?n.tokenize(y,c):y,d,y,h);j.push(A),O&&j.push(O),Array.prototype.splice.apply(r,j)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,l=0;r=a[l++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.matchedStr=a||null,this.greedy=!!r};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var l={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==l.type&&(l.attributes.spellcheck="true"),e.alias){var i="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run("wrap",l);var o="";for(var s in l.attributes)o+=(o?" ":"")+s+'="'+(l.attributes[s]||"")+'"';return"<"+l.tag+' class="'+l.classes.join(" ")+'" '+o+">"+l.content+""},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,l=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),l&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",n.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 3 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:{pattern:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; 4 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0}}),Prism.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\\\|\\?[^\\])*?`/,greedy:!0,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\w\W]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript; -------------------------------------------------------------------------------- /src/demos/template-stream/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | overflow-x: hidden; 4 | } 5 | 6 | h1 { 7 | font-family: georgia, serif; 8 | font-weight: normal; 9 | } 10 | 11 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=clike+javascript */ 12 | /** 13 | * okaidia theme for JavaScript, CSS and HTML 14 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 15 | * @author ocodia 16 | */ 17 | 18 | code[class*="language-"], 19 | pre[class*="language-"] { 20 | color: #f8f8f2; 21 | background: none; 22 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 23 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 24 | text-align: left; 25 | white-space: pre; 26 | word-spacing: normal; 27 | word-break: normal; 28 | word-wrap: normal; 29 | line-height: 1.5; 30 | 31 | -moz-tab-size: 4; 32 | -o-tab-size: 4; 33 | tab-size: 4; 34 | 35 | -webkit-hyphens: none; 36 | -moz-hyphens: none; 37 | -ms-hyphens: none; 38 | hyphens: none; 39 | } 40 | 41 | /* Code blocks */ 42 | pre[class*="language-"] { 43 | padding: 1em; 44 | margin: .5em 0; 45 | overflow: auto; 46 | border-radius: 0.3em; 47 | } 48 | 49 | :not(pre) > code[class*="language-"], 50 | pre[class*="language-"] { 51 | background: #272822; 52 | } 53 | 54 | /* Inline code */ 55 | :not(pre) > code[class*="language-"] { 56 | padding: .1em; 57 | border-radius: .3em; 58 | white-space: normal; 59 | } 60 | 61 | .token.comment, 62 | .token.prolog, 63 | .token.doctype, 64 | .token.cdata { 65 | color: slategray; 66 | } 67 | 68 | .token.punctuation { 69 | color: #f8f8f2; 70 | } 71 | 72 | .namespace { 73 | opacity: .7; 74 | } 75 | 76 | .token.property, 77 | .token.tag, 78 | .token.constant, 79 | .token.symbol, 80 | .token.deleted { 81 | color: #f92672; 82 | } 83 | 84 | .token.boolean, 85 | .token.number { 86 | color: #ae81ff; 87 | } 88 | 89 | .token.selector, 90 | .token.attr-name, 91 | .token.string, 92 | .token.char, 93 | .token.builtin, 94 | .token.inserted { 95 | color: #a6e22e; 96 | } 97 | 98 | .token.operator, 99 | .token.entity, 100 | .token.url, 101 | .language-css .token.string, 102 | .style .token.string, 103 | .token.variable { 104 | color: #f8f8f2; 105 | } 106 | 107 | .token.atrule, 108 | .token.attr-value, 109 | .token.function { 110 | color: #e6db74; 111 | } 112 | 113 | .token.keyword { 114 | color: #66d9ef; 115 | } 116 | 117 | .token.regex, 118 | .token.important { 119 | color: #fd971f; 120 | } 121 | 122 | .token.important, 123 | .token.bold { 124 | font-weight: bold; 125 | } 126 | .token.italic { 127 | font-style: italic; 128 | } -------------------------------------------------------------------------------- /src/demos/template-stream/sw.js: -------------------------------------------------------------------------------- 1 | function streamingTemplateResponse() { 2 | // Fetch photo data from Flickr 3 | const kittenPhoto = fetch('https://api.flickr.com/services/rest/?api_key=f2cca7d09b75c6cdea6864aca72e9895&format=json&text=kitten&extras=url_m&per_page=1&nojsoncallback=1&method=flickr.photos.search') 4 | .then(r => r.json()) 5 | .then(data => data.photos.photo[0]); 6 | 7 | // Get the parts of the image data we need 8 | const kittenWidth = kittenPhoto.then(data => htmlEscape(data.width_m)); 9 | const kittenHeight = kittenPhoto.then(data => htmlEscape(data.height_m)); 10 | const kittenURL = kittenPhoto.then(data => htmlEscape(data.url_m)); 11 | const kittenAlt = kittenPhoto.then(data => htmlEscape(data.title)); 12 | 13 | // Some artificially slow content for demo purposes 14 | const slowContent = new Promise(r => setTimeout(r, 2000)).then(() => 'delayed'); 15 | 16 | // Fetch the service worker script and get its content stream 17 | const serviceWorkerScript = fetch('sw.js').then(r => htmlEscapeStream(r.body)); 18 | 19 | // Generate the stream 20 | const body = templateStream` 21 | 22 | 23 | Streaming template literals Batman! 24 | 25 | 26 | 27 | 28 | 29 |

This content is streamed from the service worker

30 |

For instance, this image tag is populated from a request to Flickr's API:

31 | ${kittenAlt} 32 |

The final word in this paragraph is artificially ${slowContent}.

33 |

And just to be really meta, here's the service worker that created this streaming response also streamed into this response:

34 |
${serviceWorkerScript}
35 | 36 | 37 | `; 38 | 39 | // Create a response with the stream 40 | return new Response(body, { 41 | headers: {'Content-Type': 'text/html'} 42 | }); 43 | } 44 | 45 | // This generates a stream from a template 46 | function templateStream(strings, ...values) { 47 | let items = []; 48 | 49 | // Create a single array of strings and values interleaved 50 | strings.forEach((str, i) => { 51 | items.push(str); 52 | if (i in values) items.push(values[i]); 53 | }); 54 | 55 | // Turn them all into promises - makes it easier. 56 | // Then get an iterator for the values 57 | items = items.map(i => Promise.resolve(i)).entries(); 58 | 59 | // So we can turn our text into bytes 60 | const encoder = new TextEncoder(); 61 | 62 | // Return the stream 63 | return new ReadableStream({ 64 | pull(controller) { 65 | // Get the next value 66 | const result = items.next(); 67 | 68 | // End the stream if there are no more values 69 | if (result.done) { 70 | controller.close(); 71 | return; 72 | } 73 | 74 | // Wait for it to resolve 75 | return result.value[1].then(val => { 76 | // Does it look like a stream? 77 | if (val.getReader) { 78 | // If so, 'pipe' the data to our stream 79 | const reader = val.getReader(); 80 | return reader.read().then(function process(result) { 81 | if (result.done) return; 82 | controller.enqueue(result.value); 83 | return reader.read().then(process); 84 | }); 85 | } 86 | // If not, encode the string and pass it to our stream 87 | controller.enqueue(encoder.encode(val)); 88 | }); 89 | } 90 | }); 91 | } 92 | 93 | const htmlEscapes = { 94 | '&': '&', 95 | '<': '<', 96 | '>': '>', 97 | '"': '"', 98 | "'": ''', 99 | '`': '`' 100 | }; 101 | 102 | function htmlEscape(str) { 103 | return str.replace(/[&<>"'`]/g, item => htmlEscapes[item]); 104 | } 105 | 106 | function htmlEscapeStream(stream) { 107 | const reader = stream.getReader(); 108 | const decoder = new TextDecoder(); 109 | const encoder = new TextEncoder(); 110 | 111 | return new ReadableStream({ 112 | pull(controller) { 113 | return reader.read().then(result => { 114 | if (result.done) { 115 | controller.close(); 116 | return; 117 | } 118 | 119 | const val = htmlEscape(decoder.decode(result.value, {stream:true})); 120 | controller.enqueue(encoder.encode(val)); 121 | }) 122 | } 123 | }); 124 | } 125 | 126 | self.addEventListener('fetch', event => { 127 | const url = new URL(event.request.url); 128 | 129 | if (url.origin == location.origin && url.pathname.endsWith('/template-stream/')) { 130 | event.respondWith(streamingTemplateResponse()); 131 | } 132 | }); 133 | 134 | self.addEventListener('install', event => { 135 | self.skipWaiting(); 136 | }); 137 | 138 | self.addEventListener('activate', event => { 139 | clients.claim(); 140 | }); -------------------------------------------------------------------------------- /src/demos/transform-stream/edge-cases.md: -------------------------------------------------------------------------------- 1 | # Searching a stream 2 | 3 | When searching for something in a stream there's a little gotcha when it comes to chunk boundaries. Say we were searching for "service workers", because things arrive in chunks, one chunk could be "this next section is about se" and the next one "rvice workers". Neither chunk contains "service workers", so a naive check of each chunk isn't good enough. 4 | 5 | To work around this, a buffer is kept. The buffer needs to be at least the length of the search term - 1 to avoid missing matches across boundaries. 6 | 7 | # Search & replace in a stream 8 | 9 | In addition to above, there's another gotcha: 10 | 11 | Say we were replacing "lol" with "goal" in "lolol". We maintain a buffer of two chars because that's `"lol".length - 1`, meaning we don't miss matches between boundaries. It could go like this: 12 | 13 | 1. Buffer is `""` 14 | 2. Chunk arrives `"lolol"`, add to buffer 15 | 3. Buffer is `"lolol"` 16 | 4. Replace "lol" with "goal" in buffer 17 | 5. Buffer is `"goalol"` 18 | 6. Send buffer up to position `"lol".length - 1` - `"goal"` 19 | 7. Set buffer to the remainder of the previous step - `"ol"` 20 | 8. Incoming stream ends 21 | 9. Send remaining buffer `"ol"` 22 | 23 | This sends `"goalol"`, which is correct. But what if: 24 | 25 | 1. Buffer is `""` 26 | 2. Chunk arrives `"lol"`, add to buffer 27 | 3. Buffer is `"lol"` 28 | 4. Replace "lol" with "goal" in buffer 29 | 5. Buffer is `"goal"` 30 | 6. Send buffer up to position `"lol".length - 1` - `"go"` 31 | 7. Set buffer to the remainder of the previous step - `"al"` 32 | 8. Chunk arrives `"ol"`, add to buffer 33 | 9. Buffer is `"alol"` 34 | 10. Replace "lol" with "goal" in buffer 35 | 11. Buffer is `"agoal"` 36 | 12. Send buffer up to position `"lol".length - 1` - `"ago"` 37 | 13. Set buffer to the remainder of the previous step - `"al"` 38 | 14. Incoming stream ends 39 | 15. Send remaining buffer `"al"` 40 | 41 | This sends `"goagoal"`, which is wrong. To fix this, the buffer should be flushed until the position of the last replacement, or up to position `buffer.length - ("lol".length - 1)`, whichever's greater. So: 42 | 43 | 1. Buffer is `""` 44 | 2. Chunk arrives `"lol"`, add to buffer 45 | 3. Buffer is `"lol"` 46 | 4. Replace "lol" with "goal" in buffer 47 | 5. Buffer is `"goal"` 48 | 6. Send buffer until the end of the last replacement, or `buffer.length - ("lol" - 1)`, whichever's greater - `"goal"` 49 | 7. Set buffer to the remainder of the previous step - `""` 50 | 8. Chunk arrives `"ol"`, add to buffer 51 | 9. Buffer is `"ol"` 52 | 10. Replace "lol" with "goal" in buffer 53 | 11. Buffer is `"ol"` 54 | 12. Send buffer until the end of the last replacement, or `buffer.length - ("lol" - 1)`, whichever's greater - `""` 55 | 13. Set buffer to the last `"lol".length - 1` chars of buffer - `"ol"` 56 | 14. Incoming stream ends 57 | 15. Send remaining buffer `"ol"` 58 | 59 | Phew! Because of this, doing a regex replacement in a stream is tricky, as regex can match varying lengths, eg `/clo+ud/`, making choosing a buffer size tricky. 60 | -------------------------------------------------------------------------------- /src/demos/transform-stream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

Loading service worker…

13 |
14 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/demos/transform-stream/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', event => { 2 | self.skipWaiting(); 3 | }); 4 | 5 | self.addEventListener('activate', event => { 6 | clients.claim(); 7 | }); 8 | 9 | self.addEventListener('fetch', event => { 10 | const requestURL = new URL(event.request.url); 11 | 12 | if (requestURL.origin != location.origin) return; 13 | 14 | if (requestURL.pathname.endsWith("/demos/transform-stream/")) { 15 | event.respondWith( 16 | fetch('cloud.html').then(response => { 17 | return replaceResponse(response, 4, /cloud/ig, match => { 18 | if (match.toUpperCase() == match) return 'BUTT'; 19 | if (match[0] == 'C') return 'Butt'; 20 | return 'butt'; 21 | }); 22 | }) 23 | ); 24 | } 25 | }); 26 | 27 | // There are some fun edge cases when it comes to replacing within a stream, 28 | // if you're interested, see: 29 | // https://github.com/jakearchibald/isserviceworkerready/blob/master/src/demos/transform-stream/edge-cases.md 30 | function replaceResponse(response, bufferSize, match, replacer) { 31 | const reader = response.body.getReader(); 32 | const encoder = new TextEncoder(); 33 | const decoder = new TextDecoder(); 34 | let bufferStr = ''; 35 | 36 | const stream = new ReadableStream({ 37 | pull: controller => { 38 | return reader.read().then(result => { 39 | if (result.done) { 40 | controller.enqueue(encoder.encode(bufferStr)); 41 | controller.close(); 42 | return; 43 | } 44 | 45 | const bytes = result.value; 46 | bufferStr += decoder.decode(bytes, {stream: true}); 47 | 48 | // this is the end of the final replacement in the FINAL string 49 | let lastReplaceEnds = 0; 50 | let replacedLengthDiff = 0; 51 | bufferStr = bufferStr.replace(match, (...args) => { 52 | const matched = args[0]; 53 | // offset is the offset in the original string, hence replacedLengthDiff 54 | const offset = args[args.length - 2]; 55 | const replacement = replacer(...args); 56 | 57 | replacedLengthDiff += replacement.length - matched.length; 58 | lastReplaceEnds = offset + matched.length + replacedLengthDiff; 59 | return replacement; 60 | }); 61 | 62 | const newBufferStart = Math.max(bufferStr.length - bufferSize, lastReplaceEnds); 63 | controller.enqueue(encoder.encode(bufferStr.slice(0, newBufferStart))); 64 | bufferStr = bufferStr.slice(newBufferStart); 65 | }); 66 | }, 67 | cancel: () => { 68 | reader.cancel(); 69 | } 70 | }); 71 | 72 | return new Response(stream, { 73 | headers: response.headers 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/img/chrome-canary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/chrome-canary.png -------------------------------------------------------------------------------- /src/img/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/chrome.png -------------------------------------------------------------------------------- /src/img/edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/edge.png -------------------------------------------------------------------------------- /src/img/firefox-nightly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/firefox-nightly.png -------------------------------------------------------------------------------- /src/img/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/firefox.png -------------------------------------------------------------------------------- /src/img/opera-developer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/opera-developer.png -------------------------------------------------------------------------------- /src/img/opera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/opera.png -------------------------------------------------------------------------------- /src/img/safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/safari.png -------------------------------------------------------------------------------- /src/img/samsung-internet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/samsung-internet.png -------------------------------------------------------------------------------- /src/img/webkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/isserviceworkerready/cda40edfbd47d023a06534e1d0e68b55a6a9f6d0/src/img/webkit.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Is service worker ready?{% endblock %} 4 | 5 | {% block body %} 6 | {% include 'masthead.html' %} 7 | {% for feature in features %} 8 | {% set featureId = feature.name|striptags|lower|replace("\s+", '-', 'g') %} 9 |
10 |
11 | 12 |

13 | {{ feature.name|safe }} 14 |

15 |
16 |

{{ feature.description|safe }}

17 |
18 |
19 | {% for browser in browsers %} 20 |
21 |

{{browser.name}}

22 | {% if feature[browser.id].supported %} 23 |

24 | {% if feature[browser.id].supported == 0.5 %} 25 | Somewhat supported 26 | {% else %} 27 | Supported 28 | {% endif %} 29 | {% if feature[browser.id].minVersion %} 30 | since version {{ feature[browser.id].minVersion }} 31 | {% endif %} 32 |

33 | {% else %} 34 |

No support

35 | {% endif %} 36 |
37 | {% endfor %} 38 |
39 |
    40 | {% for browser in browsers %} 41 | {% for detail in feature[browser.id].details %} 42 |
  • {{ browser.name }}: {{ detail|safe }}
  • 43 | {% endfor %} 44 | {% endfor %} 45 | {% for detail in feature.details %} 46 |
  • {{ detail|safe }}
  • 47 | {% endfor %} 48 |
49 |
50 | {% endfor %} 51 | 52 |

Service worker is here. Get busy with it.

53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /src/masthead.html: -------------------------------------------------------------------------------- 1 |
2 |

Is ServiceWorker ready?

3 | 4 | 12 | 13 | Yes. 14 | 15 | 31 |
32 | -------------------------------------------------------------------------------- /src/resources.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Service Worker Resources{% endblock %} 4 | 5 | {% block body %} 6 | {% include 'masthead.html' %} 7 | 8 | 22 | 23 | 37 | 38 | 52 | 53 | 66 | 67 | 83 | 84 |

ServiceWorker is here. Get busy with it.

85 | {% endblock %} 86 | --------------------------------------------------------------------------------