├── .gitignore ├── package.json ├── README.md └── src └── skyport.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skyport", 3 | "version": "1.0.0", 4 | "description": "Provide offline functionality for your web app", 5 | "main": "skyport.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/MJB-code6/SkyPort.git" 12 | }, 13 | "keywords": [ 14 | "offline", 15 | "service worker", 16 | "progressive" 17 | ], 18 | "author": "Brandon Mizuno, Masha Diminsky, Joe Huang", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/MJB-code6/SkyPort/issues" 22 | }, 23 | "homepage": "https://github.com/MJB-code6/SkyPort#readme" 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SkyPort 2 | > Simple setup. Offline-first. Customizable. 3 | 4 | ### Why SkyPort? 5 | 6 | * Create an offline-first user experience for your web apps. 7 | * Optimize online web performance by reducing network requests. 8 | * Progressive web functionality. 9 | 10 | SkyPort is a library designed to make it easier for developers to take full advantage of a powerful progressive web technology—Service Workers. As a proxy server that exists between the client and network, Service Workers have the potential to optimize web apps by reducing network requests, creating an offline-first experience so users can navigate your app even when they are offline, and even improve current online user experience. 11 | 12 | #####So what's the problem with Service Workers? 13 | 14 | Service Workers are still a new and experimental technology, setup is long and tedious and understanding the Service Worker lifecycle can create a strenuous development experience. SkyPort is a library that simplifies setup, and provides developers flexibility and customization of their offline-first user experience. 15 | 16 | # Getting Started 17 | 18 | 1. Download skyport.js file and include in your app's client folder. 19 | 2. Add a script tag to your html files linking to skyport.js. 20 | 3. Apply the Skyport methods below. 21 | 22 | ## skyport.cache(*jsonFile*) 23 | 24 | > *Include in a script tag on your homepage, or call from a file that you won’t cache.* 25 | 26 | #####Benefits: 27 | 28 | 1. Cache both static and dynamic assets with one method. 29 | 2. Reduce network requests, optimize your app's web performance. 30 | 3. Better user experience—users unlikely to notice your app is offline. 31 | 4. Fallback page automatically served when user is offline and the requested assets not found in the cache. 32 | 5. For static file updates: 33 | + The version number can be set to a number or a string. 34 | + Change the version number anytime you change the contents of the cache 35 | 36 | 37 | *index.html:* 38 | ```javascript 39 | skyport.cache('/assetList.json'); 40 | ``` 41 | 42 | *assetList.json:* 43 | 44 | + In this example, the static cache, the dynamic cache and the fallback page are all included in the JSON file—include only what you are using. **For example if you are only static assets, include the version number and the static property with the list of assets you want to store in the static cache. 45 | 46 | ```javascript 47 | { 48 | version: 1, 49 | static: [ 50 | '/index.html', 51 | '/messages.html', 52 | '/another-page.html', 53 | '/style.css', 54 | '/index.js', 55 | '/assets/some-image.png', 56 | '/assets/some-video.mp4' 57 | ], 58 | dynamic: [ 59 | '/messages', 60 | '/dynamicData.html' 61 | ], 62 | fallback: '/fallback.html' 63 | } 64 | ``` 65 | 66 | ## skyport.static(*jsonFile* / *versionNum*, [*files*] ) 67 | > *Include in file that you won’t cache, like index.html.* 68 | 69 | #####Benefits: 70 | 1. For smaller apps that need to cache static files only. 71 | 2. Either include the JSON file and only static files will be cached or the files in an array with the version number. 72 | 3. Improve web performance by reducing network requests. 73 | 4. Optimize user experience as static files appear when user is offline. 74 | 75 | ##### Method 1: 76 | *index.html:* 77 | ```javascript 78 | skyport.static(1, [ 79 | '/index.html', 80 | '/messages.html', 81 | '/another-page.html', 82 | '/style.css', 83 | '/index.js', 84 | '/assets/some-image.png', 85 | '/assets/some-video.mp4' 86 | ]); 87 | ``` 88 | 89 | ##### Method 2: 90 | *index.html:* 91 | ```javascript 92 | skyport.static('/assetList.json'); 93 | ``` 94 | 95 | *assetList.json:* 96 | 97 | + only static files will be cached, the rest is ignored. 98 | + JSON file should have 'version' and 'static'(array) properties. 99 | 100 | ```javascript 101 | { 102 | version: 1, 103 | static: [ 104 | '/index.html', 105 | '/messages.html', 106 | '/another-page.html', 107 | '/style.css', 108 | '/index.js', 109 | '/assets/some-image.png', 110 | '/assets/some-video.mp4' 111 | ], 112 | dynamic: [ 113 | '/messages', 114 | '/dynamicData.html' 115 | ], 116 | fallback: '/fallback.html' 117 | } 118 | ``` 119 | 120 | ## skyport.dynamic(*jsonFile* / [*files*]) 121 | 122 | #####Benefits: 123 | 1. For smaller apps that need to cache dynamic files only. 124 | 2. Either include the JSON file and only dynamic files will be cached or the files in an array. 125 | 126 | ##### Method 1: 127 | ```javascript 128 | skyport.dynamic(['/messages','/dynamicData.html',]); 129 | ``` 130 | 131 | ##### Method 2: 132 | ```javascript 133 | skyport.dynamic('/assetList.json'); 134 | ``` 135 | 136 | *assetList.json:* 137 | 138 | + only dynamic files will be cached, the rest is ignored. 139 | + JSON file should have 'dynamic'(array) property to work. 140 | 141 | ```javascript 142 | { 143 | version: 1, 144 | static: [ 145 | '/index.html', 146 | '/messages.html', 147 | '/another-page.html', 148 | '/style.css', 149 | '/index.js', 150 | '/assets/some-image.png', 151 | '/assets/some-video.mp4' 152 | ], 153 | dynamic: [ 154 | '/messages', 155 | '/dynamicData.html' 156 | ], 157 | fallback: '/fallback.html' 158 | } 159 | ``` 160 | 161 | ## skyport.direct(*data, callback*) 162 | 163 | #####Benefits: 164 | 165 | 1. Function runs immediately if user is online, or queued if user is offline and synced when user is online again. 166 | 2. Convenient for post requests and functions that can only run when user is online. 167 | 168 | ```javascript 169 | function sendMessage() { 170 | var msg = $('input').val(); 171 | var user = $('#user-input').val(); 172 | var msgObj = {}; 173 | msgObj.message = msg; 174 | msgObj.author = user; 175 | 176 | skyport.direct(function() { 177 | $.ajax({ 178 | type: POST, 179 | url: '/messages', 180 | data: msgObj, 181 | ... 182 | ... 183 | }).then { 184 | getNewMessages(); 185 | }; 186 | }); 187 | }; 188 | ``` 189 | 190 | ## skyport.fallback(*fallback*) 191 | 192 | #####Benefits: 193 | 1. When user is offline and assets they request are not in cache, custom fallback page will be served. 194 | 2. Create a simple fallback page for users to see when your web app is offline. 195 | 196 | ```javascript 197 | skyport.fallback('/fallbackPage.html'); 198 | ``` 199 | 200 | ## skyport.reset() 201 | 202 | #####Benefits: 203 | 1. During development making resetting the cache easy. 204 | 2. Resetting indexedb useful when using *skyport.direct()* method. 205 | 3. Easily delete current Service Worker. 206 | 207 | ```javascript 208 | skyport.reset() // resets caches, Service Worker, indexedb 209 | skyport.reset('cache') // resets static and dynamic caches only 210 | skyport.reset('cache', 'indexedb') // resets caches and indexedb only. 211 | skyport.reset('sw') // deletes current Service Worker 212 | ``` 213 | 214 | --- 215 | 216 | ### License 217 | MIT License (MIT) 218 | 219 | Copyright (c) 2016 Team SkyPort ([Brandon](https://github.com/ranmizu), [Masha](https://github.com/Mashadim), [Joe](https://github.com/ZhouxiangHuang)) 220 | -------------------------------------------------------------------------------- /src/skyport.js: -------------------------------------------------------------------------------- 1 | var window; 2 | /* 3 | The code below runs in the service worker global scope 4 | */ 5 | if (!window) { 6 | //three variables bellow are declared for accessing caches 7 | var precache = precache || caches.open('sky-static').then(function(cache) { 8 | return cache; 9 | }); 10 | 11 | var postcache = postcache || caches.open('sky-dynamic').then(function(cache) { 12 | return cache; 13 | }); 14 | 15 | var fallback = fallback || caches.open('sky-fallback').then(function(cache) { 16 | return cache; 17 | }); 18 | 19 | var fallbackURL = registration.scope; 20 | 21 | //skipWaiting within install listenner allows waiting service worker become active 22 | self.addEventListener('install', function(event) { 23 | return self.skipWaiting(); 24 | }); 25 | 26 | //runIDB runs initially in the listenner to create objectstore in indexedDB 27 | self.addEventListener('activate', function(event) { 28 | runIDB(); 29 | event.waitUntil(self.clients.claim()); 30 | }); 31 | 32 | //within the fetch listener is the caching system that controls the interaction 33 | //between client and server 34 | self.addEventListener('fetch', function(event) { 35 | event.respondWith( 36 | precache.match(event.request).then(function(response) { 37 | if(response) { 38 | //if requested data is found in static cache, serve the asset to the client 39 | return response; 40 | } else if (navigator.onLine) { 41 | //if requested data is not found in static cache AND there is network connection 42 | //request data from the server 43 | return fetch(event.request).then(function(netRes) { 44 | //looks for dynamic cache to check wether data is considered dynamic 45 | return postcache.match(event.request).then(function(response) { 46 | //if the request is dynamic data, update cache 47 | if (response && event.request.method === 'GET') { 48 | postcache.put(event.request, netRes.clone()); 49 | } 50 | //send response from the network to the client 51 | return netRes; 52 | }); 53 | }); 54 | } else { 55 | //if there is no network connection AND requested data is not 56 | //found in static cache, serve data from dynamic cache 57 | return postcache.match(event.request).then(function(response) { 58 | if (response) return response; 59 | else if (/\.html$/.test(event.request.url)) { 60 | //if no match found from dynamic, serve the fallback page 61 | return fallback.match(fallbackURL).then(function(response) { 62 | return response; 63 | }); 64 | } 65 | else { 66 | console.error('(SkyPort) Error: a resource ', event.request.url, 67 | ' was not found in cache'); 68 | } 69 | }); 70 | } 71 | }) 72 | ); 73 | }); 74 | 75 | 76 | self.addEventListener('message', function(event) { 77 | var command = event.data.command; 78 | var info = event.data.info; 79 | 80 | if (command === "cacheJSON" && navigator.onLine) { 81 | return fetch(info.fileRoute).then(function(response) { 82 | return response.json(); 83 | }) 84 | .then(function(parsedFile) { 85 | if (info.cacheType === 'cache') { 86 | 87 | if (!parsedFile.static && !parsedFile.dynamic && !parsedFile.fallback) { 88 | console.error('(SkyPort) Error: JSON file passed to \'cache\' ' + 89 | 'function must have at least one of the following fields: ' + 90 | '\'static\', \'dynamic\', \'fallback\''); 91 | return; 92 | } 93 | 94 | if (parsedFile.static) { 95 | if (!Array.isArray(parsedFile.static)) { 96 | console.error('(SkyPort) Error: static assets must be an array'); 97 | return; 98 | } 99 | if (!parsedFile.version) { 100 | console.error('(SkyPort) Error: JSON files with static assets ' + 101 | 'must include a version field (number or string)'); 102 | return; 103 | } 104 | addToCache('static', parsedFile.static, parsedFile.version); 105 | } 106 | 107 | if (parsedFile.dynamic) { 108 | if (!Array.isArray(parsedFile.dynamic)) { 109 | console.error('(SkyPort) Error: dynamic assets must be an array'); 110 | return; 111 | } 112 | addToCache('dynamic', parsedFile.dynamic); 113 | } 114 | 115 | if (parsedFile.fallback) { 116 | if (!/\.html$/.test(fallbackURL)) { 117 | fallbackURL += parsedFile.fallback.slice(parsedFile.fallback.indexOf( 118 | parsedFile.fallback.match(/\w/))); 119 | addToCache('fallback', [parsedFile.fallback]); 120 | } 121 | } 122 | } 123 | 124 | else if (info.cacheType === 'static') { 125 | if (!parsedFile.static) { 126 | if (!parsedFile.assets) { 127 | console.error('(SkyPort) Error: JSON file passed to static ' + 128 | 'function must have a \'static\' field'); 129 | return; 130 | } 131 | parsedFile.static = parsedFile.assets; 132 | } 133 | 134 | if (!Array.isArray(parsedFile.static)) { 135 | console.error('(SkyPort) Error: static assets must be an array'); 136 | return; 137 | } 138 | if (!parsedFile.version) { 139 | console.error('(SkyPort) Error: JSON files with static assets ' + 140 | 'must include a version field (number or string)'); 141 | return; 142 | } 143 | addToCache('static', parsedFile.static, parsedFile.version); 144 | } 145 | 146 | else if (info.cacheType === 'dynamic') { 147 | if (!parsedFile.dynamic) { 148 | if (!parsedFile.assets) { 149 | console.error('(SkyPort) Error: JSON file passed to dynamic ' + 150 | 'function must have a \'dynamic\' field'); 151 | return; 152 | } 153 | parsedFile.dynamic = parsedFile.assets; 154 | } 155 | 156 | if (!Array.isArray(parsedFile.dynamic)) { 157 | console.error('(SkyPort) Error: dynamic assets must be an array'); 158 | return; 159 | } 160 | addToCache('dynamic', parsedFile.dynamic); 161 | } 162 | return; 163 | }); 164 | } 165 | 166 | 167 | if (command === "cacheArray" && navigator.onLine) { 168 | addToCache(info.cacheType, info.assets, info.version || null); 169 | } 170 | 171 | if(command === "fallback") { 172 | fallbackURL += info.fileRoute.slice(info.fileRoute.indexOf( 173 | info.fileRoute.match(/\w/))); 174 | addToCache('fallback', [info.fileRoute]); 175 | } 176 | 177 | if (command === "queue") { 178 | runIDB(event.data); 179 | } 180 | }); 181 | 182 | function runIDB(data) { 183 | var openRequest = indexedDB.open('skyport', 1); 184 | 185 | openRequest.onupgradeneeded = function(e) { 186 | db = e.target.result; 187 | var objectStore = db.createObjectStore("redirected", { keyPath: "domain" }); 188 | }; 189 | 190 | openRequest.onsuccess = function(e) { 191 | db = e.target.result; 192 | var objectStore = db.transaction("redirected", "readwrite").objectStore("redirected"); 193 | 194 | if (!data) { 195 | objectStore.add({domain: registration.scope, requests: []}); 196 | } 197 | 198 | else if (data.command === 'queue') { 199 | var retrieveRequest = objectStore.get(registration.scope); 200 | 201 | retrieveRequest.onsuccess = function(e) { 202 | // Get the old value that we want to update 203 | var deferredQueue = retrieveRequest.result.requests; 204 | 205 | // update the value(s) in the object that you want to change 206 | deferredQueue.push({ 207 | data: data.info.dataObj, 208 | callback: data.info.deferredFunc 209 | }); 210 | 211 | // Put this updated object back into the database. 212 | var requestUpdate = objectStore.put({domain: registration.scope, requests: deferredQueue}); 213 | }; 214 | } 215 | 216 | else if (data.command === 'dequeue') { 217 | var retrieveRequest = objectStore.get(registration.scope); 218 | 219 | retrieveRequest.onsuccess = function(event) { 220 | var deferredQueue = retrieveRequest.result["requests"]; 221 | 222 | while(navigator.onLine && deferredQueue.length) { 223 | var nextRequest = deferredQueue.shift(); 224 | var deferredFunc = eval(nextRequest.callback); 225 | if (typeof(deferredFunc) === "function") deferredFunc(JSON.parse(nextRequest.data)); 226 | var requestUpdate = objectStore.put({domain: registration.scope, requests: deferredQueue}); 227 | } 228 | } 229 | } 230 | }; 231 | } 232 | 233 | function addToCache(type, itemsToAdd, version) { 234 | var cacheName = 'sky-' + type; 235 | if (version) cacheName += '-v' + version; 236 | caches.open(cacheName).then(function(cache) { 237 | if (type === 'static') { 238 | if (precache) caches.delete('sky-static'); 239 | precache = cache; 240 | itemsToAdd.forEach(function(item, i) { 241 | cache.match(item).then(function(response) { 242 | if (!response) cache.add(item); 243 | }) 244 | }) 245 | //cleanCache(itemsToAdd); 246 | } 247 | else { 248 | if (type === 'dynamic') postcache = cache; 249 | if (type === 'fallback') fallback = cache; 250 | cache.addAll(itemsToAdd).then(function() { 251 | }); 252 | } 253 | }); 254 | } 255 | 256 | function cleanCache(newItems) { 257 | caches.keys().then(function(keylist) { 258 | keylist.filter(function(key) { 259 | return /^sky-static/.test(key); 260 | }).forEach(function(cacheName) { 261 | caches.open(cacheName).then(function(cache) { 262 | cache.keys().then(function(keylist) { 263 | keylist.forEach(function(key) { 264 | if (newItems.indexOf(key.url.replace(registration.scope, '')) < 0) { 265 | cache.delete(key); 266 | } 267 | }) 268 | }) 269 | }) 270 | }) 271 | }) 272 | } 273 | } 274 | 275 | 276 | /* 277 | The code below runs in the window scope 278 | */ 279 | if (window) { 280 | 281 | (function() { 282 | // Service Workers are not (yet) supported by all browsers 283 | if (!navigator.serviceWorker) return; 284 | 285 | var serviceWorker = navigator.serviceWorker.controller; 286 | 287 | // Register the service worker once on load 288 | if (!serviceWorker) { 289 | navigator.serviceWorker.register('/skyport.js', {scope: '.'}).then(function(registration) { 290 | serviceWorker = registration.active || registration.waiting || registration.installing; 291 | 292 | // This file should be included in the cache for offline use 293 | skyport.dynamic(['/skyport.js']); 294 | }); 295 | } 296 | 297 | // Make useful functions available on the window object 298 | window.skyport = window.skyport || { 299 | 300 | cache: function(jsonFile) { 301 | if (typeof jsonFile !== 'string' || !/\.json$/.test(jsonFile)) { 302 | console.error('(SkyPort) Error: skyport.cache function parameter ' + 303 | 'must be a JSON file'); 304 | return; 305 | } 306 | sendToSW({ 307 | command: 'cacheJSON', 308 | info: { 309 | cacheType: 'cache', 310 | fileRoute: jsonFile, 311 | } 312 | }); 313 | return; 314 | }, 315 | 316 | static: function(version, assets) { 317 | if (typeof version === 'string' && /\.json$/.test(version)) { 318 | sendToSW({ 319 | command: 'cacheJSON', 320 | info: { 321 | cacheType: 'static', 322 | fileRoute: version, 323 | } 324 | }); 325 | return; 326 | } 327 | 328 | if (typeof version !== 'number' && typeof version !== 'string') { 329 | console.error('(SkyPort) Error: skyport.static must receive a JSON ' + 330 | 'file or a version (number or string) as it\'s first argument'); 331 | return; 332 | } 333 | 334 | if (!Array.isArray(assets)) { 335 | console.error('(SkyPort) Error: assets passed to skyport.static must ' + 336 | 'be either an array (after a version parameter) or a JSON file'); 337 | return; 338 | } 339 | 340 | sendToSW({ 341 | command: 'cacheArray', 342 | info: { 343 | cacheType: 'static', 344 | version: version, 345 | assets: assets, 346 | } 347 | }); 348 | }, 349 | 350 | dynamic: function(assets) { 351 | 352 | // Function was passed a JSON file 353 | if (typeof assets === 'string' && /\.json$/.test(assets)) { 354 | sendToSW({ 355 | command: 'cacheJSON', 356 | info: { 357 | cacheType: 'dynamic', 358 | fileRoute: assets, 359 | } 360 | }); 361 | return; 362 | } 363 | 364 | // Function should otherwise be passed an array 365 | if (!Array.isArray(assets)) { 366 | console.error('(SkyPort) Error: assets passed to skyport.dynamic must' + 367 | ' be either an array or a JSON file. HINT: skyport.dynamic does ' + 368 | 'not take a version parameter'); 369 | return; 370 | } 371 | 372 | sendToSW({ 373 | command: 'cacheArray', 374 | info: { 375 | cacheType: 'dynamic', 376 | assets: assets, 377 | } 378 | }); 379 | }, 380 | 381 | // Use this function to add a default page if a resource is not cached 382 | fallback: function(htmlFile) { 383 | if (!htmlFile || typeof htmlFile !== 'string' || !/\.html$/.test(htmlFile)) { 384 | console.error('(SkyPort) Error: parameter of fallback function must ' + 385 | 'be an HTML file'); 386 | return; 387 | } 388 | sendToSW({ 389 | command: 'fallback', 390 | info: { fileRoute: htmlFile } 391 | }); 392 | }, 393 | 394 | 395 | // 396 | direct: function(dataObj, deferredFunc) { 397 | if (navigator.onLine) return deferredFunc(dataObj); 398 | if (typeof(deferredFunc) !== "function") return; 399 | sendToSW({ 400 | command: 'queue', 401 | info: { 402 | dataObj: JSON.stringify(dataObj), 403 | deferredFunc: '(' + deferredFunc.toString() + ')' 404 | } 405 | }); 406 | }, 407 | 408 | reset: function() { 409 | var args = Array.prototype.slice.call(arguments); 410 | 411 | if(args.length === 0) { 412 | resetCache(); 413 | resetIndexedDB(); 414 | resetSW(); 415 | } else { 416 | args.forEach(function(arg) { 417 | if(arg.toLowerCase() === "cache") resetCache(); 418 | else if(arg.toLowerCase() === "indexeddb") resetIndexedDB(); 419 | else if(arg.toLowerCase() === "sw") resetSW(); 420 | }); 421 | } 422 | 423 | function resetCache() { 424 | caches.keys().then(function(keylist) { 425 | return Promise.all( 426 | keylist.filter(function(cacheName) { 427 | return caches.delete(cacheName) 428 | }) 429 | ); 430 | }).then(function() { 431 | console.log('(SkyPort) Success: SkyPort caches deleted'); 432 | }); 433 | } 434 | 435 | function resetIndexedDB() { 436 | var openRequest = indexedDB.open('skyport', 1); 437 | openRequest.onsuccess = function(event) { 438 | var deleteReq = indexedDB.deleteDatabase(event.target.result); 439 | 440 | deleteReq.onsuccess= function(e) { 441 | console.log('(SkyPort) Success: SkyPort indexedDB deleted'); 442 | }; 443 | 444 | deleteReq.onerror= function(e) { 445 | console.error('(SkyPort) Error: SkyPort indexedDB not deleted'); 446 | }; 447 | } 448 | } 449 | 450 | function resetSW() { 451 | navigator.serviceWorker.getRegistrations().then(function(registrations) { 452 | for (var registration in registrations) { 453 | registrations[registration].unregister().then(function() { 454 | console.log('(SkyPort) Success: SkyPort service worker deleted'); 455 | }); 456 | } 457 | }); 458 | } 459 | } 460 | }; 461 | 462 | 463 | window.addEventListener('online', function(event) { 464 | dequeue(); 465 | }); 466 | 467 | 468 | window.addEventListener('load', function(event) { 469 | // 470 | }); 471 | 472 | 473 | function dequeue() { 474 | var openRequest = indexedDB.open('skyport', 1); 475 | 476 | openRequest.onsuccess = function(e) { 477 | var db = e.target.result; 478 | var objectStore = db.transaction("redirected", "readwrite").objectStore("redirected"); 479 | var retrieveRequest = objectStore.get(window.location.origin + '/'); 480 | 481 | retrieveRequest.onsuccess = function(event) { 482 | var deferredQueue = retrieveRequest.result["requests"]; 483 | 484 | while(navigator.onLine && deferredQueue.length) { 485 | var nextRequest = deferredQueue.shift(); 486 | var deferredFunc = eval(nextRequest.callback); 487 | if (typeof(deferredFunc) === "function") deferredFunc(JSON.parse(nextRequest.data)); 488 | var requestUpdate = objectStore.put({ 489 | domain: window.location.origin + '/', 490 | requests: deferredQueue 491 | }); 492 | } 493 | } 494 | } 495 | } 496 | 497 | var messageQueue = messageQueue || []; 498 | 499 | function sendToSW(messageObj) { 500 | messageQueue.push(messageObj); 501 | 502 | if (!serviceWorker) { 503 | navigator.serviceWorker.oncontrollerchange = function() { 504 | serviceWorker = navigator.serviceWorker.controller; 505 | sendQueue(); 506 | } 507 | } else { 508 | sendQueue(); 509 | } 510 | 511 | function sendQueue() { 512 | while (messageQueue.length) { 513 | serviceWorker.postMessage(messageQueue.shift()); 514 | } 515 | } 516 | } 517 | })(); 518 | } 519 | --------------------------------------------------------------------------------