diff --git a/_config.amane.yml b/_config.amane.yml index 546b6f0..b00f7ea 100644 --- a/_config.amane.yml +++ b/_config.amane.yml @@ -197,7 +197,7 @@ widgets: # Author's current location # location: CLOUDFLARE.COM # URL or path to the avatar image - avatar: https://cdn.nofated.win/avatar/256 + avatar: https://xgjalbum.oss-cn-hangzhou.aliyuncs.com/623fec2f97df7d3eee768933/AC1BFF0F-E891-41D0-8BB7-08C4DDB70CA9.png?x-oss-process=image/resize,m_fill,h_256,w_256,limit_0/format,webp # Whether show the rounded avatar image avatar_rounded: true # Email address for the Gravatar diff --git a/scripts/service-worker-injector.js b/scripts/service-worker-injector.js index ee22056..4b412be 100644 --- a/scripts/service-worker-injector.js +++ b/scripts/service-worker-injector.js @@ -1 +1,3 @@ -hexo.extend.injector.register('body_end', '', 'default'); +hexo.extend.injector.register('body_end', '', 'default'); + +hexo.extend.injector.register('body_end', '', 'default'); diff --git a/source/js/quicklink.js b/source/js/quicklink.js new file mode 100644 index 0000000..153efcf --- /dev/null +++ b/source/js/quicklink.js @@ -0,0 +1 @@ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(e.quicklink={})}(this,function(e){function n(e){return new Promise(function(n,r,t){(t=new XMLHttpRequest).open("GET",e,t.withCredentials=!0),t.onload=function(){200===t.status?n():r()},t.send()})}var r,t=(r=document.createElement("link")).relList&&r.relList.supports&&r.relList.supports("prefetch")?function(e){return new Promise(function(n,r,t){(t=document.createElement("link")).rel="prefetch",t.href=e,t.onload=n,t.onerror=r,document.head.appendChild(t)})}:n,o=window.requestIdleCallback||function(e){var n=Date.now();return setTimeout(function(){e({didTimeout:!1,timeRemaining:function(){return Math.max(0,50-(Date.now()-n))}})},1)},i=new Set,c=new Set,u=!1;function a(e){if(e){if(e.saveData)return new Error("Save-Data is enabled");if(/2g/.test(e.effectiveType))return new Error("network conditions are poor")}return!0}function s(e,r,o){var s=a(navigator.connection);return s instanceof Error?Promise.reject(new Error("Cannot prefetch, "+s.message)):(c.size>0&&!u&&console.warn("[Warning] You are using both prefetching and prerendering on the same document"),Promise.all([].concat(e).map(function(e){if(!i.has(e))return i.add(e),(r?function(e){return window.fetch?fetch(e,{credentials:"include"}):n(e)}:t)(new URL(e,location.href).toString())})))}function f(e,n){var r=a(navigator.connection);if(r instanceof Error)return Promise.reject(new Error("Cannot prerender, "+r.message));if(!HTMLScriptElement.supports("speculationrules"))return s(e),Promise.reject(new Error("This browser does not support the speculation rules API. Falling back to prefetch."));if(document.querySelector('script[type="speculationrules"]'))return Promise.reject(new Error("Speculation Rules is already defined and cannot be altered."));for(var t=0,o=[].concat(e);t0&&!u&&console.warn("[Warning] You are using both prefetching and prerendering on the same document");var l=function(e){var n=document.createElement("script");n.type="speculationrules",n.text='{"prerender":[{"source": "list","urls": ["'+Array.from(e).join('","')+'"]}]}';try{document.head.appendChild(n)}catch(e){return e}return!0}(c);return!0===l?Promise.resolve():Promise.reject(l)}e.listen=function(e){if(e||(e={}),window.IntersectionObserver){var n=function(e){e=e||1;var n=[],r=0;function t(){r0&&(n.shift()(),r++)}return[function(e){n.push(e)>1||t()},function(){r--,t()}]}(e.throttle||1/0),r=n[0],t=n[1],a=e.limit||1/0,l=e.origins||[location.hostname],d=e.ignores||[],h=e.delay||0,p=[],m=e.timeoutFn||o,w="function"==typeof e.hrefFn&&e.hrefFn,g=e.prerender||!1;u=e.prerenderAndPrefetch||!1;var v=new IntersectionObserver(function(n){n.forEach(function(n){if(n.isIntersecting)p.push((n=n.target).href),function(e,n){n?setTimeout(e,n):e()}(function(){-1!==p.indexOf(n.href)&&(v.unobserve(n),(u||g)&&c.size<1?f(w?w(n):n.href).catch(function(n){if(!e.onError)throw n;e.onError(n)}):i.size-1&&p.splice(o)}})},{threshold:e.threshold||0});return m(function(){(e.el||document).querySelectorAll("a").forEach(function(e){l.length&&!l.includes(e.hostname)||function e(n,r){return Array.isArray(r)?r.some(function(r){return e(n,r)}):(r.test||r).call(r,n.href,n)}(e,d)||v.observe(e)})},{timeout:e.timeout||2e3}),function(){i.clear(),v.disconnect()}}},e.prefetch=s,e.prerender=f}); diff --git a/source/js/sw.js b/source/js/sw.js index 8c9877e..f3e44fd 100644 --- a/source/js/sw.js +++ b/source/js/sw.js @@ -1,7 +1,7 @@ -importScripts('https://cdn.jsdelivr.net/npm/workbox-cdn@5.1.4/workbox/workbox-sw.js'); +importScripts('/js/workbox/workbox-sw.js'); workbox.setConfig({ - modulePathPrefix: 'https://cdn.jsdelivr.net/npm/workbox-cdn@5.1.4/workbox/' + modulePathPrefix: '/js/workbox/' }); const { core, precaching, routing, strategies, expiration, cacheableResponse, backgroundSync } = workbox; @@ -9,7 +9,7 @@ const { CacheFirst, NetworkFirst, NetworkOnly, StaleWhileRevalidate } = strategi const { ExpirationPlugin } = expiration; const { CacheableResponsePlugin } = cacheableResponse; -const cacheSuffixVersion = '-231126', +const cacheSuffixVersion = '-231126a', // precacheCacheName = core.cacheNames.precache, // runtimeCacheName = core.cacheNames.runtime, maxEntries = 100; @@ -18,8 +18,8 @@ self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => { return Promise.all(keys.map((key) => { - if (key.includes('disqus-cdn-cache')) return caches.delete(key); - if (key.includes('disqus-img-cache')) return caches.delete(key); + if (key.includes('waline-cdn-cache')) return caches.delete(key); + if (key.includes('waline-img-cache')) return caches.delete(key); if (!key.includes(cacheSuffixVersion)) return caches.delete(key); })); }) @@ -203,10 +203,10 @@ routing.registerRoute( ); routing.registerRoute( - new RegExp('disqus'), + new RegExp('waline'), new NetworkFirst({ plugins: [ - new backgroundSync.BackgroundSyncPlugin('disqus', { + new backgroundSync.BackgroundSyncPlugin('waline', { maxRetentionTime: 12 * 60 }), ] @@ -276,4 +276,4 @@ routing.registerRoute( */ routing.setDefaultHandler( new NetworkOnly() -); \ No newline at end of file +); diff --git a/source/js/workbox/workbox-background-sync.dev.js b/source/js/workbox/workbox-background-sync.dev.js new file mode 100644 index 0000000..ee97c46 --- /dev/null +++ b/source/js/workbox/workbox-background-sync.dev.js @@ -0,0 +1,818 @@ +this.workbox = this.workbox || {}; +this.workbox.backgroundSync = (function (exports, WorkboxError_js, logger_js, assert_js, getFriendlyURL_js, DBWrapper_js) { + 'use strict'; + + try { + self['workbox:background-sync:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const DB_VERSION = 3; + const DB_NAME = 'workbox-background-sync'; + const OBJECT_STORE_NAME = 'requests'; + const INDEXED_PROP = 'queueName'; + /** + * A class to manage storing requests from a Queue in IndexedDB, + * indexed by their queue name for easier access. + * + * @private + */ + + class QueueStore { + /** + * Associates this instance with a Queue instance, so entries added can be + * identified by their queue name. + * + * @param {string} queueName + * @private + */ + constructor(queueName) { + this._queueName = queueName; + this._db = new DBWrapper_js.DBWrapper(DB_NAME, DB_VERSION, { + onupgradeneeded: this._upgradeDb + }); + } + /** + * Append an entry last in the queue. + * + * @param {Object} entry + * @param {Object} entry.requestData + * @param {number} [entry.timestamp] + * @param {Object} [entry.metadata] + * @private + */ + + + async pushEntry(entry) { + { + assert_js.assert.isType(entry, 'object', { + moduleName: 'workbox-background-sync', + className: 'QueueStore', + funcName: 'pushEntry', + paramName: 'entry' + }); + assert_js.assert.isType(entry.requestData, 'object', { + moduleName: 'workbox-background-sync', + className: 'QueueStore', + funcName: 'pushEntry', + paramName: 'entry.requestData' + }); + } // Don't specify an ID since one is automatically generated. + + + delete entry.id; + entry.queueName = this._queueName; + await this._db.add(OBJECT_STORE_NAME, entry); + } + /** + * Prepend an entry first in the queue. + * + * @param {Object} entry + * @param {Object} entry.requestData + * @param {number} [entry.timestamp] + * @param {Object} [entry.metadata] + * @private + */ + + + async unshiftEntry(entry) { + { + assert_js.assert.isType(entry, 'object', { + moduleName: 'workbox-background-sync', + className: 'QueueStore', + funcName: 'unshiftEntry', + paramName: 'entry' + }); + assert_js.assert.isType(entry.requestData, 'object', { + moduleName: 'workbox-background-sync', + className: 'QueueStore', + funcName: 'unshiftEntry', + paramName: 'entry.requestData' + }); + } + + const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, { + count: 1 + }); + + if (firstEntry) { + // Pick an ID one less than the lowest ID in the object store. + entry.id = firstEntry.id - 1; + } else { + // Otherwise let the auto-incrementor assign the ID. + delete entry.id; + } + + entry.queueName = this._queueName; + await this._db.add(OBJECT_STORE_NAME, entry); + } + /** + * Removes and returns the last entry in the queue matching the `queueName`. + * + * @return {Promise} + * @private + */ + + + async popEntry() { + return this._removeEntry({ + direction: 'prev' + }); + } + /** + * Removes and returns the first entry in the queue matching the `queueName`. + * + * @return {Promise} + * @private + */ + + + async shiftEntry() { + return this._removeEntry({ + direction: 'next' + }); + } + /** + * Returns all entries in the store matching the `queueName`. + * + * @param {Object} options See {@link module:workbox-background-sync.Queue~getAll} + * @return {Promise>} + * @private + */ + + + async getAll() { + return await this._db.getAllMatching(OBJECT_STORE_NAME, { + index: INDEXED_PROP, + query: IDBKeyRange.only(this._queueName) + }); + } + /** + * Deletes the entry for the given ID. + * + * WARNING: this method does not ensure the deleted enry belongs to this + * queue (i.e. matches the `queueName`). But this limitation is acceptable + * as this class is not publicly exposed. An additional check would make + * this method slower than it needs to be. + * + * @private + * @param {number} id + */ + + + async deleteEntry(id) { + await this._db.delete(OBJECT_STORE_NAME, id); + } + /** + * Removes and returns the first or last entry in the queue (based on the + * `direction` argument) matching the `queueName`. + * + * @return {Promise} + * @private + */ + + + async _removeEntry({ + direction + }) { + const [entry] = await this._db.getAllMatching(OBJECT_STORE_NAME, { + direction, + index: INDEXED_PROP, + query: IDBKeyRange.only(this._queueName), + count: 1 + }); + + if (entry) { + await this.deleteEntry(entry.id); + return entry; + } + } + /** + * Upgrades the database given an `upgradeneeded` event. + * + * @param {Event} event + * @private + */ + + + _upgradeDb(event) { + const db = event.target.result; + + if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) { + if (db.objectStoreNames.contains(OBJECT_STORE_NAME)) { + db.deleteObjectStore(OBJECT_STORE_NAME); + } + } + + const objStore = db.createObjectStore(OBJECT_STORE_NAME, { + autoIncrement: true, + keyPath: 'id' + }); + objStore.createIndex(INDEXED_PROP, INDEXED_PROP, { + unique: false + }); + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const serializableProperties = ['method', 'referrer', 'referrerPolicy', 'mode', 'credentials', 'cache', 'redirect', 'integrity', 'keepalive']; + /** + * A class to make it easier to serialize and de-serialize requests so they + * can be stored in IndexedDB. + * + * @private + */ + + class StorableRequest { + /** + * Accepts an object of request data that can be used to construct a + * `Request` but can also be stored in IndexedDB. + * + * @param {Object} requestData An object of request data that includes the + * `url` plus any relevant properties of + * [requestInit]{@link https://fetch.spec.whatwg.org/#requestinit}. + * @private + */ + constructor(requestData) { + { + assert_js.assert.isType(requestData, 'object', { + moduleName: 'workbox-background-sync', + className: 'StorableRequest', + funcName: 'constructor', + paramName: 'requestData' + }); + assert_js.assert.isType(requestData.url, 'string', { + moduleName: 'workbox-background-sync', + className: 'StorableRequest', + funcName: 'constructor', + paramName: 'requestData.url' + }); + } // If the request's mode is `navigate`, convert it to `same-origin` since + // navigation requests can't be constructed via script. + + + if (requestData['mode'] === 'navigate') { + requestData['mode'] = 'same-origin'; + } + + this._requestData = requestData; + } + /** + * Converts a Request object to a plain object that can be structured + * cloned or JSON-stringified. + * + * @param {Request} request + * @return {Promise} + * + * @private + */ + + + static async fromRequest(request) { + const requestData = { + url: request.url, + headers: {} + }; // Set the body if present. + + if (request.method !== 'GET') { + // Use ArrayBuffer to support non-text request bodies. + // NOTE: we can't use Blobs becuse Safari doesn't support storing + // Blobs in IndexedDB in some cases: + // https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457 + requestData.body = await request.clone().arrayBuffer(); + } // Convert the headers from an iterable to an object. + + + for (const [key, value] of request.headers.entries()) { + requestData.headers[key] = value; + } // Add all other serializable request properties + + + for (const prop of serializableProperties) { + if (request[prop] !== undefined) { + requestData[prop] = request[prop]; + } + } + + return new StorableRequest(requestData); + } + /** + * Returns a deep clone of the instances `_requestData` object. + * + * @return {Object} + * + * @private + */ + + + toObject() { + const requestData = Object.assign({}, this._requestData); + requestData.headers = Object.assign({}, this._requestData.headers); + + if (requestData.body) { + requestData.body = requestData.body.slice(0); + } + + return requestData; + } + /** + * Converts this instance to a Request. + * + * @return {Request} + * + * @private + */ + + + toRequest() { + return new Request(this._requestData.url, this._requestData); + } + /** + * Creates and returns a deep clone of the instance. + * + * @return {StorableRequest} + * + * @private + */ + + + clone() { + return new StorableRequest(this.toObject()); + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const TAG_PREFIX = 'workbox-background-sync'; + const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes + + const queueNames = new Set(); + /** + * Converts a QueueStore entry into the format exposed by Queue. This entails + * converting the request data into a real request and omitting the `id` and + * `queueName` properties. + * + * @param {Object} queueStoreEntry + * @return {Object} + * @private + */ + + const convertEntry = queueStoreEntry => { + const queueEntry = { + request: new StorableRequest(queueStoreEntry.requestData).toRequest(), + timestamp: queueStoreEntry.timestamp + }; + + if (queueStoreEntry.metadata) { + queueEntry.metadata = queueStoreEntry.metadata; + } + + return queueEntry; + }; + /** + * A class to manage storing failed requests in IndexedDB and retrying them + * later. All parts of the storing and replaying process are observable via + * callbacks. + * + * @memberof module:workbox-background-sync + */ + + + class Queue { + /** + * Creates an instance of Queue with the given options + * + * @param {string} name The unique name for this queue. This name must be + * unique as it's used to register sync events and store requests + * in IndexedDB specific to this instance. An error will be thrown if + * a duplicate name is detected. + * @param {Object} [options] + * @param {Function} [options.onSync] A function that gets invoked whenever + * the 'sync' event fires. The function is invoked with an object + * containing the `queue` property (referencing this instance), and you + * can use the callback to customize the replay behavior of the queue. + * When not set the `replayRequests()` method is called. + * Note: if the replay fails after a sync event, make sure you throw an + * error, so the browser knows to retry the sync event later. + * @param {number} [options.maxRetentionTime=7 days] The amount of time (in + * minutes) a request may be retried. After this amount of time has + * passed, the request will be deleted from the queue. + */ + constructor(name, { + onSync, + maxRetentionTime + } = {}) { + this._syncInProgress = false; + this._requestsAddedDuringSync = false; // Ensure the store name is not already being used + + if (queueNames.has(name)) { + throw new WorkboxError_js.WorkboxError('duplicate-queue-name', { + name + }); + } else { + queueNames.add(name); + } + + this._name = name; + this._onSync = onSync || this.replayRequests; + this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME; + this._queueStore = new QueueStore(this._name); + + this._addSyncListener(); + } + /** + * @return {string} + */ + + + get name() { + return this._name; + } + /** + * Stores the passed request in IndexedDB (with its timestamp and any + * metadata) at the end of the queue. + * + * @param {Object} entry + * @param {Request} entry.request The request to store in the queue. + * @param {Object} [entry.metadata] Any metadata you want associated with the + * stored request. When requests are replayed you'll have access to this + * metadata object in case you need to modify the request beforehand. + * @param {number} [entry.timestamp] The timestamp (Epoch time in + * milliseconds) when the request was first added to the queue. This is + * used along with `maxRetentionTime` to remove outdated requests. In + * general you don't need to set this value, as it's automatically set + * for you (defaulting to `Date.now()`), but you can update it if you + * don't want particular requests to expire. + */ + + + async pushRequest(entry) { + { + assert_js.assert.isType(entry, 'object', { + moduleName: 'workbox-background-sync', + className: 'Queue', + funcName: 'pushRequest', + paramName: 'entry' + }); + assert_js.assert.isInstance(entry.request, Request, { + moduleName: 'workbox-background-sync', + className: 'Queue', + funcName: 'pushRequest', + paramName: 'entry.request' + }); + } + + await this._addRequest(entry, 'push'); + } + /** + * Stores the passed request in IndexedDB (with its timestamp and any + * metadata) at the beginning of the queue. + * + * @param {Object} entry + * @param {Request} entry.request The request to store in the queue. + * @param {Object} [entry.metadata] Any metadata you want associated with the + * stored request. When requests are replayed you'll have access to this + * metadata object in case you need to modify the request beforehand. + * @param {number} [entry.timestamp] The timestamp (Epoch time in + * milliseconds) when the request was first added to the queue. This is + * used along with `maxRetentionTime` to remove outdated requests. In + * general you don't need to set this value, as it's automatically set + * for you (defaulting to `Date.now()`), but you can update it if you + * don't want particular requests to expire. + */ + + + async unshiftRequest(entry) { + { + assert_js.assert.isType(entry, 'object', { + moduleName: 'workbox-background-sync', + className: 'Queue', + funcName: 'unshiftRequest', + paramName: 'entry' + }); + assert_js.assert.isInstance(entry.request, Request, { + moduleName: 'workbox-background-sync', + className: 'Queue', + funcName: 'unshiftRequest', + paramName: 'entry.request' + }); + } + + await this._addRequest(entry, 'unshift'); + } + /** + * Removes and returns the last request in the queue (along with its + * timestamp and any metadata). The returned object takes the form: + * `{request, timestamp, metadata}`. + * + * @return {Promise} + */ + + + async popRequest() { + return this._removeRequest('pop'); + } + /** + * Removes and returns the first request in the queue (along with its + * timestamp and any metadata). The returned object takes the form: + * `{request, timestamp, metadata}`. + * + * @return {Promise} + */ + + + async shiftRequest() { + return this._removeRequest('shift'); + } + /** + * Returns all the entries that have not expired (per `maxRetentionTime`). + * Any expired entries are removed from the queue. + * + * @return {Promise>} + */ + + + async getAll() { + const allEntries = await this._queueStore.getAll(); + const now = Date.now(); + const unexpiredEntries = []; + + for (const entry of allEntries) { + // Ignore requests older than maxRetentionTime. Call this function + // recursively until an unexpired request is found. + const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000; + + if (now - entry.timestamp > maxRetentionTimeInMs) { + await this._queueStore.deleteEntry(entry.id); + } else { + unexpiredEntries.push(convertEntry(entry)); + } + } + + return unexpiredEntries; + } + /** + * Adds the entry to the QueueStore and registers for a sync event. + * + * @param {Object} entry + * @param {Request} entry.request + * @param {Object} [entry.metadata] + * @param {number} [entry.timestamp=Date.now()] + * @param {string} operation ('push' or 'unshift') + * @private + */ + + + async _addRequest({ + request, + metadata, + timestamp = Date.now() + }, operation) { + const storableRequest = await StorableRequest.fromRequest(request.clone()); + const entry = { + requestData: storableRequest.toObject(), + timestamp + }; // Only include metadata if it's present. + + if (metadata) { + entry.metadata = metadata; + } + + await this._queueStore[`${operation}Entry`](entry); + + { + logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(request.url)}' has ` + `been added to background sync queue '${this._name}'.`); + } // Don't register for a sync if we're in the middle of a sync. Instead, + // we wait until the sync is complete and call register if + // `this._requestsAddedDuringSync` is true. + + + if (this._syncInProgress) { + this._requestsAddedDuringSync = true; + } else { + await this.registerSync(); + } + } + /** + * Removes and returns the first or last (depending on `operation`) entry + * from the QueueStore that's not older than the `maxRetentionTime`. + * + * @param {string} operation ('pop' or 'shift') + * @return {Object|undefined} + * @private + */ + + + async _removeRequest(operation) { + const now = Date.now(); + const entry = await this._queueStore[`${operation}Entry`](); + + if (entry) { + // Ignore requests older than maxRetentionTime. Call this function + // recursively until an unexpired request is found. + const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000; + + if (now - entry.timestamp > maxRetentionTimeInMs) { + return this._removeRequest(operation); + } + + return convertEntry(entry); + } else { + return undefined; + } + } + /** + * Loops through each request in the queue and attempts to re-fetch it. + * If any request fails to re-fetch, it's put back in the same position in + * the queue (which registers a retry for the next sync event). + */ + + + async replayRequests() { + let entry; + + while (entry = await this.shiftRequest()) { + try { + await fetch(entry.request.clone()); + + if ("dev" !== 'production') { + logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `has been replayed in queue '${this._name}'`); + } + } catch (error) { + await this.unshiftRequest(entry); + + { + logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `failed to replay, putting it back in queue '${this._name}'`); + } + + throw new WorkboxError_js.WorkboxError('queue-replay-failed', { + name: this._name + }); + } + } + + { + logger_js.logger.log(`All requests in queue '${this.name}' have successfully ` + `replayed; the queue is now empty!`); + } + } + /** + * Registers a sync event with a tag unique to this instance. + */ + + + async registerSync() { + if ('sync' in self.registration) { + try { + await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`); + } catch (err) { + // This means the registration failed for some reason, possibly due to + // the user disabling it. + { + logger_js.logger.warn(`Unable to register sync event for '${this._name}'.`, err); + } + } + } + } + /** + * In sync-supporting browsers, this adds a listener for the sync event. + * In non-sync-supporting browsers, this will retry the queue on service + * worker startup. + * + * @private + */ + + + _addSyncListener() { + if ('sync' in self.registration) { + self.addEventListener('sync', event => { + if (event.tag === `${TAG_PREFIX}:${this._name}`) { + { + logger_js.logger.log(`Background sync for tag '${event.tag}'` + `has been received`); + } + + const syncComplete = async () => { + this._syncInProgress = true; + let syncError; + + try { + await this._onSync({ + queue: this + }); + } catch (error) { + syncError = error; // Rethrow the error. Note: the logic in the finally clause + // will run before this gets rethrown. + + throw syncError; + } finally { + // New items may have been added to the queue during the sync, + // so we need to register for a new sync if that's happened... + // Unless there was an error during the sync, in which + // case the browser will automatically retry later, as long + // as `event.lastChance` is not true. + if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) { + await this.registerSync(); + } + + this._syncInProgress = false; + this._requestsAddedDuringSync = false; + } + }; + + event.waitUntil(syncComplete()); + } + }); + } else { + { + logger_js.logger.log(`Background sync replaying without background sync event`); + } // If the browser doesn't support background sync, retry + // every time the service worker starts up as a fallback. + + + this._onSync({ + queue: this + }); + } + } + /** + * Returns the set of queue names. This is primarily used to reset the list + * of queue names in tests. + * + * @return {Set} + * + * @private + */ + + + static get _queueNames() { + return queueNames; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A class implementing the `fetchDidFail` lifecycle callback. This makes it + * easier to add failed requests to a background sync Queue. + * + * @memberof module:workbox-background-sync + */ + + class BackgroundSyncPlugin { + /** + * @param {string} name See the [Queue]{@link module:workbox-background-sync.Queue} + * documentation for parameter details. + * @param {Object} [options] See the + * [Queue]{@link module:workbox-background-sync.Queue} documentation for + * parameter details. + */ + constructor(name, options) { + /** + * @param {Object} options + * @param {Request} options.request + * @private + */ + this.fetchDidFail = async ({ + request + }) => { + await this._queue.pushRequest({ + request + }); + }; + + this._queue = new Queue(name, options); + } + + } + + exports.BackgroundSyncPlugin = BackgroundSyncPlugin; + exports.Queue = Queue; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); + diff --git a/source/js/workbox/workbox-background-sync.prod.js b/source/js/workbox/workbox-background-sync.prod.js new file mode 100644 index 0000000..248a352 --- /dev/null +++ b/source/js/workbox/workbox-background-sync.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.backgroundSync=function(t,e,s,i,n){"use strict";try{self["workbox:background-sync:5.1.4"]&&_()}catch(t){}class a{constructor(t){this.t=t,this.s=new n.DBWrapper("workbox-background-sync",3,{onupgradeneeded:this.i})}async pushEntry(t){delete t.id,t.queueName=this.t,await this.s.add("requests",t)}async unshiftEntry(t){const[e]=await this.s.getAllMatching("requests",{count:1});e?t.id=e.id-1:delete t.id,t.queueName=this.t,await this.s.add("requests",t)}async popEntry(){return this.h({direction:"prev"})}async shiftEntry(){return this.h({direction:"next"})}async getAll(){return await this.s.getAllMatching("requests",{index:"queueName",query:IDBKeyRange.only(this.t)})}async deleteEntry(t){await this.s.delete("requests",t)}async h({direction:t}){const[e]=await this.s.getAllMatching("requests",{direction:t,index:"queueName",query:IDBKeyRange.only(this.t),count:1});if(e)return await this.deleteEntry(e.id),e}i(t){const e=t.target.result;t.oldVersion>0&&t.oldVersion<3&&e.objectStoreNames.contains("requests")&&e.deleteObjectStore("requests");e.createObjectStore("requests",{autoIncrement:!0,keyPath:"id"}).createIndex("queueName","queueName",{unique:!1})}}const r=["method","referrer","referrerPolicy","mode","credentials","cache","redirect","integrity","keepalive"];class c{constructor(t){"navigate"===t.mode&&(t.mode="same-origin"),this.u=t}static async fromRequest(t){const e={url:t.url,headers:{}};"GET"!==t.method&&(e.body=await t.clone().arrayBuffer());for(const[s,i]of t.headers.entries())e.headers[s]=i;for(const s of r)void 0!==t[s]&&(e[s]=t[s]);return new c(e)}toObject(){const t=Object.assign({},this.u);return t.headers=Object.assign({},this.u.headers),t.body&&(t.body=t.body.slice(0)),t}toRequest(){return new Request(this.u.url,this.u)}clone(){return new c(this.toObject())}}const h=new Set,u=t=>{const e={request:new c(t.requestData).toRequest(),timestamp:t.timestamp};return t.metadata&&(e.metadata=t.metadata),e};class o{constructor(t,{onSync:s,maxRetentionTime:i}={}){if(this.o=!1,this.q=!1,h.has(t))throw new e.WorkboxError("duplicate-queue-name",{name:t});h.add(t),this.l=t,this.m=s||this.replayRequests,this.p=i||10080,this.g=new a(this.l),this.R()}get name(){return this.l}async pushRequest(t){await this.k(t,"push")}async unshiftRequest(t){await this.k(t,"unshift")}async popRequest(){return this.D("pop")}async shiftRequest(){return this.D("shift")}async getAll(){const t=await this.g.getAll(),e=Date.now(),s=[];for(const i of t){const t=60*this.p*1e3;e-i.timestamp>t?await this.g.deleteEntry(i.id):s.push(u(i))}return s}async k({request:t,metadata:e,timestamp:s=Date.now()},i){const n={requestData:(await c.fromRequest(t.clone())).toObject(),timestamp:s};e&&(n.metadata=e),await this.g[i+"Entry"](n),this.o?this.q=!0:await this.registerSync()}async D(t){const e=Date.now(),s=await this.g[t+"Entry"]();if(s){const i=60*this.p*1e3;return e-s.timestamp>i?this.D(t):u(s)}}async replayRequests(){let t;for(;t=await this.shiftRequest();)try{await fetch(t.request.clone())}catch(s){throw await this.unshiftRequest(t),new e.WorkboxError("queue-replay-failed",{name:this.l})}}async registerSync(){if("sync"in self.registration)try{await self.registration.sync.register("workbox-background-sync:"+this.l)}catch(t){}}R(){"sync"in self.registration?self.addEventListener("sync",t=>{if(t.tag==="workbox-background-sync:"+this.l){const e=async()=>{let e;this.o=!0;try{await this.m({queue:this})}catch(t){throw e=t,e}finally{!this.q||e&&!t.lastChance||await this.registerSync(),this.o=!1,this.q=!1}};t.waitUntil(e())}}):this.m({queue:this})}static get _(){return h}}return t.BackgroundSyncPlugin=class{constructor(t,e){this.fetchDidFail=async({request:t})=>{await this.v.pushRequest({request:t})},this.v=new o(t,e)}},t.Queue=o,t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private); + diff --git a/source/js/workbox/workbox-broadcast-update.dev.js b/source/js/workbox/workbox-broadcast-update.dev.js new file mode 100644 index 0000000..1f58f81 --- /dev/null +++ b/source/js/workbox/workbox-broadcast-update.dev.js @@ -0,0 +1,288 @@ +this.workbox = this.workbox || {}; +this.workbox.broadcastUpdate = (function (exports, assert_js, timeout_js, resultingClientExists_js, logger_js, WorkboxError_js, dontWaitFor_js) { + 'use strict'; + + try { + self['workbox:broadcast-update:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Given two `Response's`, compares several header values to see if they are + * the same or not. + * + * @param {Response} firstResponse + * @param {Response} secondResponse + * @param {Array} headersToCheck + * @return {boolean} + * + * @memberof module:workbox-broadcast-update + */ + + const responsesAreSame = (firstResponse, secondResponse, headersToCheck) => { + { + if (!(firstResponse instanceof Response && secondResponse instanceof Response)) { + throw new WorkboxError_js.WorkboxError('invalid-responses-are-same-args'); + } + } + + const atLeastOneHeaderAvailable = headersToCheck.some(header => { + return firstResponse.headers.has(header) && secondResponse.headers.has(header); + }); + + if (!atLeastOneHeaderAvailable) { + { + logger_js.logger.warn(`Unable to determine where the response has been updated ` + `because none of the headers that would be checked are present.`); + logger_js.logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck); + } // Just return true, indicating the that responses are the same, since we + // can't determine otherwise. + + + return true; + } + + return headersToCheck.every(header => { + const headerStateComparison = firstResponse.headers.has(header) === secondResponse.headers.has(header); + const headerValueComparison = firstResponse.headers.get(header) === secondResponse.headers.get(header); + return headerStateComparison && headerValueComparison; + }); + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED'; + const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update'; + const DEFAULT_HEADERS_TO_CHECK = ['content-length', 'etag', 'last-modified']; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + // TODO(philipwalton): remove once this Safari bug fix has been released. + // https://bugs.webkit.org/show_bug.cgi?id=201169 + + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + /** + * Generates the default payload used in update messages. By default the + * payload includes the `cacheName` and `updatedURL` fields. + * + * @return Object + * @private + */ + + function defaultPayloadGenerator(data) { + return { + cacheName: data.cacheName, + updatedURL: data.request.url + }; + } + /** + * Uses the `postMessage()` API to inform any open windows/tabs when a cached + * response has been updated. + * + * For efficiency's sake, the underlying response bodies are not compared; + * only specific response headers are checked. + * + * @memberof module:workbox-broadcast-update + */ + + + class BroadcastCacheUpdate { + /** + * Construct a BroadcastCacheUpdate instance with a specific `channelName` to + * broadcast messages on + * + * @param {Object} options + * @param {Array} [options.headersToCheck=['content-length', 'etag', 'last-modified']] + * A list of headers that will be used to determine whether the responses + * differ. + * @param {string} [options.generatePayload] A function whose return value + * will be used as the `payload` field in any cache update messages sent + * to the window clients. + */ + constructor({ + headersToCheck, + generatePayload + } = {}) { + this._headersToCheck = headersToCheck || DEFAULT_HEADERS_TO_CHECK; + this._generatePayload = generatePayload || defaultPayloadGenerator; + } + /** + * Compares two [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response) + * and sends a message (via `postMessage()`) to all window clients if the + * responses differ (note: neither of the Responses can be + * {@link http://stackoverflow.com/questions/39109789|opaque}). + * + * The message that's posted has the following format (where `payload` can + * be customized via the `generatePayload` option the instance is created + * with): + * + * ``` + * { + * type: 'CACHE_UPDATED', + * meta: 'workbox-broadcast-update', + * payload: { + * cacheName: 'the-cache-name', + * updatedURL: 'https://example.com/' + * } + * } + * ``` + * + * @param {Object} options + * @param {Response} [options.oldResponse] Cached response to compare. + * @param {Response} options.newResponse Possibly updated response to compare. + * @param {Request} options.request The request. + * @param {string} options.cacheName Name of the cache the responses belong + * to. This is included in the broadcast message. + * @param {Event} [options.event] event An optional event that triggered + * this possible cache update. + * @return {Promise} Resolves once the update is sent. + */ + + + async notifyIfUpdated(options) { + { + assert_js.assert.isType(options.cacheName, 'string', { + moduleName: 'workbox-broadcast-update', + className: 'BroadcastCacheUpdate', + funcName: 'notifyIfUpdated', + paramName: 'cacheName' + }); + assert_js.assert.isInstance(options.newResponse, Response, { + moduleName: 'workbox-broadcast-update', + className: 'BroadcastCacheUpdate', + funcName: 'notifyIfUpdated', + paramName: 'newResponse' + }); + assert_js.assert.isInstance(options.request, Request, { + moduleName: 'workbox-broadcast-update', + className: 'BroadcastCacheUpdate', + funcName: 'notifyIfUpdated', + paramName: 'request' + }); + } // Without two responses there is nothing to compare. + + + if (!options.oldResponse) { + return; + } + + if (!responsesAreSame(options.oldResponse, options.newResponse, this._headersToCheck)) { + { + logger_js.logger.log(`Newer response found (and cached) for:`, options.request.url); + } + + const messageData = { + type: CACHE_UPDATED_MESSAGE_TYPE, + meta: CACHE_UPDATED_MESSAGE_META, + payload: this._generatePayload(options) + }; // For navigation requests, wait until the new window client exists + // before sending the message + + if (options.request.mode === 'navigate') { + let resultingClientId; + + if (options.event instanceof FetchEvent) { + resultingClientId = options.event.resultingClientId; + } + + const resultingWin = await resultingClientExists_js.resultingClientExists(resultingClientId); // Safari does not currently implement postMessage buffering and + // there's no good way to feature detect that, so to increase the + // chances of the message being delivered in Safari, we add a timeout. + // We also do this if `resultingClientExists()` didn't return a client, + // which means it timed out, so it's worth waiting a bit longer. + + if (!resultingWin || isSafari) { + // 3500 is chosen because (according to CrUX data) 80% of mobile + // websites hit the DOMContentLoaded event in less than 3.5 seconds. + // And presumably sites implementing service worker are on the + // higher end of the performance spectrum. + await timeout_js.timeout(3500); + } + } + + const windows = await self.clients.matchAll({ + type: 'window' + }); + + for (const win of windows) { + win.postMessage(messageData); + } + } + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This plugin will automatically broadcast a message whenever a cached response + * is updated. + * + * @memberof module:workbox-broadcast-update + */ + + class BroadcastUpdatePlugin { + /** + * Construct a BroadcastCacheUpdate instance with the passed options and + * calls its [`notifyIfUpdated()`]{@link module:workbox-broadcast-update.BroadcastCacheUpdate~notifyIfUpdated} + * method whenever the plugin's `cacheDidUpdate` callback is invoked. + * + * @param {Object} options + * @param {Array} [options.headersToCheck=['content-length', 'etag', 'last-modified']] + * A list of headers that will be used to determine whether the responses + * differ. + * @param {string} [options.generatePayload] A function whose return value + * will be used as the `payload` field in any cache update messages sent + * to the window clients. + */ + constructor(options) { + /** + * A "lifecycle" callback that will be triggered automatically by the + * `workbox-sw` and `workbox-runtime-caching` handlers when an entry is + * added to a cache. + * + * @private + * @param {Object} options The input object to this function. + * @param {string} options.cacheName Name of the cache being updated. + * @param {Response} [options.oldResponse] The previous cached value, if any. + * @param {Response} options.newResponse The new value in the cache. + * @param {Request} options.request The request that triggered the update. + * @param {Request} [options.event] The event that triggered the update. + */ + this.cacheDidUpdate = async options => { + dontWaitFor_js.dontWaitFor(this._broadcastUpdate.notifyIfUpdated(options)); + }; + + this._broadcastUpdate = new BroadcastCacheUpdate(options); + } + + } + + exports.BroadcastCacheUpdate = BroadcastCacheUpdate; + exports.BroadcastUpdatePlugin = BroadcastUpdatePlugin; + exports.responsesAreSame = responsesAreSame; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); + diff --git a/source/js/workbox/workbox-broadcast-update.prod.js b/source/js/workbox/workbox-broadcast-update.prod.js new file mode 100644 index 0000000..04a3b9a --- /dev/null +++ b/source/js/workbox/workbox-broadcast-update.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(t,a,o,s){"use strict";try{self["workbox:broadcast-update:5.1.4"]&&_()}catch(t){}const e=(t,a,o)=>!o.some(o=>t.headers.has(o)&&a.headers.has(o))||o.every(o=>{const s=t.headers.has(o)===a.headers.has(o),e=t.headers.get(o)===a.headers.get(o);return s&&e}),n=["content-length","etag","last-modified"],i=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function c(t){return{cacheName:t.cacheName,updatedURL:t.request.url}}class r{constructor({headersToCheck:t,generatePayload:a}={}){this.t=t||n,this.o=a||c}async notifyIfUpdated(t){if(t.oldResponse&&!e(t.oldResponse,t.newResponse,this.t)){const s={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:this.o(t)};if("navigate"===t.request.mode){let s;t.event instanceof FetchEvent&&(s=t.event.resultingClientId);await o.resultingClientExists(s)&&!i||await a.timeout(3500)}const e=await self.clients.matchAll({type:"window"});for(const t of e)t.postMessage(s)}}}return t.BroadcastCacheUpdate=r,t.BroadcastUpdatePlugin=class{constructor(t){this.cacheDidUpdate=async t=>{s.dontWaitFor(this.s.notifyIfUpdated(t))},this.s=new r(t)}},t.responsesAreSame=e,t}({},workbox.core._private,workbox.core._private,workbox.core._private); + diff --git a/source/js/workbox/workbox-cacheable-response.dev.js b/source/js/workbox/workbox-cacheable-response.dev.js new file mode 100644 index 0000000..3fb5839 --- /dev/null +++ b/source/js/workbox/workbox-cacheable-response.dev.js @@ -0,0 +1,191 @@ +this.workbox = this.workbox || {}; +this.workbox.cacheableResponse = (function (exports, assert_js, WorkboxError_js, getFriendlyURL_js, logger_js) { + 'use strict'; + + try { + self['workbox:cacheable-response:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This class allows you to set up rules determining what + * status codes and/or headers need to be present in order for a + * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) + * to be considered cacheable. + * + * @memberof module:workbox-cacheable-response + */ + + class CacheableResponse { + /** + * To construct a new CacheableResponse instance you must provide at least + * one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config = {}) { + { + if (!(config.statuses || config.headers)) { + throw new WorkboxError_js.WorkboxError('statuses-or-headers-required', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor' + }); + } + + if (config.statuses) { + assert_js.assert.isArray(config.statuses, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.statuses' + }); + } + + if (config.headers) { + assert_js.assert.isType(config.headers, 'object', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.headers' + }); + } + } + + this._statuses = config.statuses; + this._headers = config.headers; + } + /** + * Checks a response to see whether it's cacheable or not, based on this + * object's configuration. + * + * @param {Response} response The response whose cacheability is being + * checked. + * @return {boolean} `true` if the `Response` is cacheable, and `false` + * otherwise. + */ + + + isResponseCacheable(response) { + { + assert_js.assert.isInstance(response, Response, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'isResponseCacheable', + paramName: 'response' + }); + } + + let cacheable = true; + + if (this._statuses) { + cacheable = this._statuses.includes(response.status); + } + + if (this._headers && cacheable) { + cacheable = Object.keys(this._headers).some(headerName => { + return response.headers.get(headerName) === this._headers[headerName]; + }); + } + + { + if (!cacheable) { + logger_js.logger.groupCollapsed(`The request for ` + `'${getFriendlyURL_js.getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`); + logger_js.logger.groupCollapsed(`View cacheability criteria here.`); + logger_js.logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses)); + logger_js.logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2)); + logger_js.logger.groupEnd(); + const logFriendlyHeaders = {}; + response.headers.forEach((value, key) => { + logFriendlyHeaders[key] = value; + }); + logger_js.logger.groupCollapsed(`View response status and headers here.`); + logger_js.logger.log(`Response status: ` + response.status); + logger_js.logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2)); + logger_js.logger.groupEnd(); + logger_js.logger.groupCollapsed(`View full response details here.`); + logger_js.logger.log(response.headers); + logger_js.logger.log(response); + logger_js.logger.groupEnd(); + logger_js.logger.groupEnd(); + } + } + + return cacheable; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A class implementing the `cacheWillUpdate` lifecycle callback. This makes it + * easier to add in cacheability checks to requests made via Workbox's built-in + * strategies. + * + * @memberof module:workbox-cacheable-response + */ + + class CacheableResponsePlugin { + /** + * To construct a new CacheableResponsePlugin instance you must provide at + * least one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config) { + /** + * @param {Object} options + * @param {Response} options.response + * @return {Response|null} + * @private + */ + this.cacheWillUpdate = async ({ + response + }) => { + if (this._cacheableResponse.isResponseCacheable(response)) { + return response; + } + + return null; + }; + + this._cacheableResponse = new CacheableResponse(config); + } + + } + + exports.CacheableResponse = CacheableResponse; + exports.CacheableResponsePlugin = CacheableResponsePlugin; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); + diff --git a/source/js/workbox/workbox-cacheable-response.prod.js b/source/js/workbox/workbox-cacheable-response.prod.js new file mode 100644 index 0000000..54bef72 --- /dev/null +++ b/source/js/workbox/workbox-cacheable-response.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(s){"use strict";try{self["workbox:cacheable-response:5.1.4"]&&_()}catch(s){}class t{constructor(s={}){this.s=s.statuses,this.t=s.headers}isResponseCacheable(s){let t=!0;return this.s&&(t=this.s.includes(s.status)),this.t&&t&&(t=Object.keys(this.t).some(t=>s.headers.get(t)===this.t[t])),t}}return s.CacheableResponse=t,s.CacheableResponsePlugin=class{constructor(s){this.cacheWillUpdate=async({response:s})=>this.i.isResponseCacheable(s)?s:null,this.i=new t(s)}},s}({}); + diff --git a/source/js/workbox/workbox-core.dev.js b/source/js/workbox/workbox-core.dev.js new file mode 100644 index 0000000..4f0e532 --- /dev/null +++ b/source/js/workbox/workbox-core.dev.js @@ -0,0 +1,1858 @@ +this.workbox = this.workbox || {}; +this.workbox.core = (function (exports) { + 'use strict'; + + try { + self['workbox:core:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const logger = (() => { + // Don't overwrite this value if it's already set. + // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 + if (!('__WB_DISABLE_DEV_LOGS' in self)) { + self.__WB_DISABLE_DEV_LOGS = false; + } + + let inGroup = false; + const methodToColorMap = { + debug: `#7f8c8d`, + log: `#2ecc71`, + warn: `#f39c12`, + error: `#c0392b`, + groupCollapsed: `#3498db`, + groupEnd: null + }; + + const print = function (method, args) { + if (self.__WB_DISABLE_DEV_LOGS) { + return; + } + + if (method === 'groupCollapsed') { + // Safari doesn't print all console.groupCollapsed() arguments: + // https://bugs.webkit.org/show_bug.cgi?id=182754 + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + console[method](...args); + return; + } + } + + const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; // When in a group, the workbox prefix is not displayed. + + const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; + console[method](...logPrefix, ...args); + + if (method === 'groupCollapsed') { + inGroup = true; + } + + if (method === 'groupEnd') { + inGroup = false; + } + }; + + const api = {}; + const loggerMethods = Object.keys(methodToColorMap); + + for (const key of loggerMethods) { + const method = key; + + api[method] = (...args) => { + print(method, args); + }; + } + + return api; + })(); + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const messages = { + 'invalid-value': ({ + paramName, + validValueDescription, + value + }) => { + if (!paramName || !validValueDescription) { + throw new Error(`Unexpected input to 'invalid-value' error.`); + } + + return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`; + }, + 'not-an-array': ({ + moduleName, + className, + funcName, + paramName + }) => { + if (!moduleName || !className || !funcName || !paramName) { + throw new Error(`Unexpected input to 'not-an-array' error.`); + } + + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`; + }, + 'incorrect-type': ({ + expectedType, + paramName, + moduleName, + className, + funcName + }) => { + if (!expectedType || !paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-type' error.`); + } + + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className ? className + '.' : ''}` + `${funcName}()' must be of type ${expectedType}.`; + }, + 'incorrect-class': ({ + expectedClass, + paramName, + moduleName, + className, + funcName, + isReturnValueProblem + }) => { + if (!expectedClass || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-class' error.`); + } + + if (isReturnValueProblem) { + return `The return value from ` + `'${moduleName}.${className ? className + '.' : ''}${funcName}()' ` + `must be an instance of class ${expectedClass.name}.`; + } + + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className ? className + '.' : ''}${funcName}()' ` + `must be an instance of class ${expectedClass.name}.`; + }, + 'missing-a-method': ({ + expectedMethod, + paramName, + moduleName, + className, + funcName + }) => { + if (!expectedMethod || !paramName || !moduleName || !className || !funcName) { + throw new Error(`Unexpected input to 'missing-a-method' error.`); + } + + return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`; + }, + 'add-to-cache-list-unexpected-type': ({ + entry + }) => { + return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`; + }, + 'add-to-cache-list-conflicting-entries': ({ + firstEntry, + secondEntry + }) => { + if (!firstEntry || !secondEntry) { + throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`); + } + + return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry._entryId} but different revision details. Workbox is ` + `unable to cache and version the asset correctly. Please remove one ` + `of the entries.`; + }, + 'plugin-error-request-will-fetch': ({ + thrownError + }) => { + if (!thrownError) { + throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`); + } + + return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownError.message}'.`; + }, + 'invalid-cache-name': ({ + cacheNameId, + value + }) => { + if (!cacheNameId) { + throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`); + } + + return `You must provide a name containing at least one character for ` + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`; + }, + 'unregister-route-but-not-found-with-method': ({ + method + }) => { + if (!method) { + throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`); + } + + return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`; + }, + 'unregister-route-route-not-registered': () => { + return `The route you're trying to unregister was not previously ` + `registered.`; + }, + 'queue-replay-failed': ({ + name + }) => { + return `Replaying the background sync queue '${name}' failed.`; + }, + 'duplicate-queue-name': ({ + name + }) => { + return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`; + }, + 'expired-test-without-max-age': ({ + methodName, + paramName + }) => { + return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`; + }, + 'unsupported-route-type': ({ + moduleName, + className, + funcName, + paramName + }) => { + return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`; + }, + 'not-array-of-class': ({ + value, + expectedClass, + moduleName, + className, + funcName, + paramName + }) => { + return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`; + }, + 'max-entries-or-age-required': ({ + moduleName, + className, + funcName + }) => { + return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`; + }, + 'statuses-or-headers-required': ({ + moduleName, + className, + funcName + }) => { + return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`; + }, + 'invalid-string': ({ + moduleName, + funcName, + paramName + }) => { + if (!paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'invalid-string' error.`); + } + + return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`; + }, + 'channel-name-required': () => { + return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`; + }, + 'invalid-responses-are-same-args': () => { + return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`; + }, + 'expire-custom-caches-only': () => { + return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`; + }, + 'unit-must-be-bytes': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`); + } + + return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`; + }, + 'single-range-only': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'single-range-only' error.`); + } + + return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`; + }, + 'invalid-range-values': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'invalid-range-values' error.`); + } + + return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`; + }, + 'no-range-header': () => { + return `No Range header was found in the Request provided.`; + }, + 'range-not-satisfiable': ({ + size, + start, + end + }) => { + return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`; + }, + 'attempt-to-cache-non-get-request': ({ + url, + method + }) => { + return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`; + }, + 'cache-put-with-no-response': ({ + url + }) => { + return `There was an attempt to cache '${url}' but the response was not ` + `defined.`; + }, + 'no-response': ({ + url, + error + }) => { + let message = `The strategy could not generate a response for '${url}'.`; + + if (error) { + message += ` The underlying error is ${error}.`; + } + + return message; + }, + 'bad-precaching-response': ({ + url, + status + }) => { + return `The precaching request for '${url}' failed with an HTTP ` + `status of ${status}.`; + }, + 'non-precached-url': ({ + url + }) => { + return `createHandlerBoundToURL('${url}') was called, but that URL is ` + `not precached. Please pass in a URL that is precached instead.`; + }, + 'add-to-cache-list-conflicting-integrities': ({ + url + }) => { + return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${url} with different integrity values. Please remove one of them.`; + }, + 'missing-precache-entry': ({ + cacheName, + url + }) => { + return `Unable to find a precached response in ${cacheName} for ${url}.`; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + const generatorFunction = (code, details = {}) => { + const message = messages[code]; + + if (!message) { + throw new Error(`Unable to find message for code '${code}'.`); + } + + return message(details); + }; + + const messageGenerator = generatorFunction; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Workbox errors should be thrown with this class. + * This allows use to ensure the type easily in tests, + * helps developers identify errors from workbox + * easily and allows use to optimise error + * messages correctly. + * + * @private + */ + + class WorkboxError extends Error { + /** + * + * @param {string} errorCode The error code that + * identifies this particular error. + * @param {Object=} details Any relevant arguments + * that will help developers identify issues should + * be added as a key on the context object. + */ + constructor(errorCode, details) { + const message = messageGenerator(errorCode, details); + super(message); + this.name = errorCode; + this.details = details; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /* + * This method throws if the supplied value is not an array. + * The destructed values are required to produce a meaningful error for users. + * The destructed and restructured object is so it's clear what is + * needed. + */ + + const isArray = (value, details) => { + if (!Array.isArray(value)) { + throw new WorkboxError('not-an-array', details); + } + }; + + const hasMethod = (object, expectedMethod, details) => { + const type = typeof object[expectedMethod]; + + if (type !== 'function') { + details['expectedMethod'] = expectedMethod; + throw new WorkboxError('missing-a-method', details); + } + }; + + const isType = (object, expectedType, details) => { + if (typeof object !== expectedType) { + details['expectedType'] = expectedType; + throw new WorkboxError('incorrect-type', details); + } + }; + + const isInstance = (object, expectedClass, details) => { + if (!(object instanceof expectedClass)) { + details['expectedClass'] = expectedClass; + throw new WorkboxError('incorrect-class', details); + } + }; + + const isOneOf = (value, validValues, details) => { + if (!validValues.includes(value)) { + details['validValueDescription'] = `Valid values are ${JSON.stringify(validValues)}.`; + throw new WorkboxError('invalid-value', details); + } + }; + + const isArrayOfClass = (value, expectedClass, details) => { + const error = new WorkboxError('not-array-of-class', details); + + if (!Array.isArray(value)) { + throw error; + } + + for (const item of value) { + if (!(item instanceof expectedClass)) { + throw error; + } + } + }; + + const finalAssertExports = { + hasMethod, + isArray, + isInstance, + isOneOf, + isType, + isArrayOfClass + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + const quotaErrorCallbacks = new Set(); + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds a function to the set of quotaErrorCallbacks that will be executed if + * there's a quota error. + * + * @param {Function} callback + * @memberof module:workbox-core + */ + + function registerQuotaErrorCallback(callback) { + { + finalAssertExports.isType(callback, 'function', { + moduleName: 'workbox-core', + funcName: 'register', + paramName: 'callback' + }); + } + + quotaErrorCallbacks.add(callback); + + { + logger.log('Registered a callback to respond to quota errors.', callback); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const _cacheNameDetails = { + googleAnalytics: 'googleAnalytics', + precache: 'precache-v2', + prefix: 'workbox', + runtime: 'runtime', + suffix: typeof registration !== 'undefined' ? registration.scope : '' + }; + + const _createCacheName = cacheName => { + return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value && value.length > 0).join('-'); + }; + + const eachCacheNameDetail = fn => { + for (const key of Object.keys(_cacheNameDetails)) { + fn(key); + } + }; + + const cacheNames = { + updateDetails: details => { + eachCacheNameDetail(key => { + if (typeof details[key] === 'string') { + _cacheNameDetails[key] = details[key]; + } + }); + }, + getGoogleAnalyticsName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics); + }, + getPrecacheName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.precache); + }, + getPrefix: () => { + return _cacheNameDetails.prefix; + }, + getRuntimeName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.runtime); + }, + getSuffix: () => { + return _cacheNameDetails.suffix; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Runs all of the callback functions, one at a time sequentially, in the order + * in which they were registered. + * + * @memberof module:workbox-core + * @private + */ + + async function executeQuotaErrorCallbacks() { + { + logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`); + } + + for (const callback of quotaErrorCallbacks) { + await callback(); + + { + logger.log(callback, 'is complete.'); + } + } + + { + logger.log('Finished running callbacks.'); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + const getFriendlyURL = url => { + const urlObj = new URL(String(url), location.href); // See https://github.com/GoogleChrome/workbox/issues/2323 + // We want to include everything, except for the origin if it's same-origin. + + return urlObj.href.replace(new RegExp(`^${location.origin}`), ''); + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const pluginUtils = { + filter: (plugins, callbackName) => { + return plugins.filter(plugin => callbackName in plugin); + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Checks the list of plugins for the cacheKeyWillBeUsed callback, and + * executes any of those callbacks found in sequence. The final `Request` object + * returned by the last plugin is treated as the cache key for cache reads + * and/or writes. + * + * @param {Object} options + * @param {Request} options.request + * @param {string} options.mode + * @param {Array} [options.plugins=[]] + * @return {Promise} + * + * @private + * @memberof module:workbox-core + */ + + const _getEffectiveRequest = async ({ + request, + mode, + plugins = [] + }) => { + const cacheKeyWillBeUsedPlugins = pluginUtils.filter(plugins, "cacheKeyWillBeUsed" + /* CACHE_KEY_WILL_BE_USED */ + ); + let effectiveRequest = request; + + for (const plugin of cacheKeyWillBeUsedPlugins) { + effectiveRequest = await plugin["cacheKeyWillBeUsed" + /* CACHE_KEY_WILL_BE_USED */ + ].call(plugin, { + mode, + request: effectiveRequest + }); + + if (typeof effectiveRequest === 'string') { + effectiveRequest = new Request(effectiveRequest); + } + + { + finalAssertExports.isInstance(effectiveRequest, Request, { + moduleName: 'Plugin', + funcName: "cacheKeyWillBeUsed" + /* CACHE_KEY_WILL_BE_USED */ + , + isReturnValueProblem: true + }); + } + } + + return effectiveRequest; + }; + /** + * This method will call cacheWillUpdate on the available plugins (or use + * status === 200) to determine if the Response is safe and valid to cache. + * + * @param {Object} options + * @param {Request} options.request + * @param {Response} options.response + * @param {Event} [options.event] + * @param {Array} [options.plugins=[]] + * @return {Promise} + * + * @private + * @memberof module:workbox-core + */ + + + const _isResponseSafeToCache = async ({ + request, + response, + event, + plugins = [] + }) => { + let responseToCache = response; + let pluginsUsed = false; + + for (const plugin of plugins) { + if ("cacheWillUpdate" + /* CACHE_WILL_UPDATE */ + in plugin) { + pluginsUsed = true; + const pluginMethod = plugin["cacheWillUpdate" + /* CACHE_WILL_UPDATE */ + ]; + responseToCache = await pluginMethod.call(plugin, { + request, + response: responseToCache, + event + }); + + { + if (responseToCache) { + finalAssertExports.isInstance(responseToCache, Response, { + moduleName: 'Plugin', + funcName: "cacheWillUpdate" + /* CACHE_WILL_UPDATE */ + , + isReturnValueProblem: true + }); + } + } + + if (!responseToCache) { + break; + } + } + } + + if (!pluginsUsed) { + { + if (responseToCache) { + if (responseToCache.status !== 200) { + if (responseToCache.status === 0) { + logger.warn(`The response for '${request.url}' is an opaque ` + `response. The caching strategy that you're using will not ` + `cache opaque responses by default.`); + } else { + logger.debug(`The response for '${request.url}' returned ` + `a status code of '${response.status}' and won't be cached as a ` + `result.`); + } + } + } + } + + responseToCache = responseToCache && responseToCache.status === 200 ? responseToCache : undefined; + } + + return responseToCache ? responseToCache : null; + }; + /** + * This is a wrapper around cache.match(). + * + * @param {Object} options + * @param {string} options.cacheName Name of the cache to match against. + * @param {Request} options.request The Request that will be used to look up + * cache entries. + * @param {Event} [options.event] The event that prompted the action. + * @param {Object} [options.matchOptions] Options passed to cache.match(). + * @param {Array} [options.plugins=[]] Array of plugins. + * @return {Response} A cached response if available. + * + * @private + * @memberof module:workbox-core + */ + + + const matchWrapper = async ({ + cacheName, + request, + event, + matchOptions, + plugins = [] + }) => { + const cache = await self.caches.open(cacheName); + const effectiveRequest = await _getEffectiveRequest({ + plugins, + request, + mode: 'read' + }); + let cachedResponse = await cache.match(effectiveRequest, matchOptions); + + { + if (cachedResponse) { + logger.debug(`Found a cached response in '${cacheName}'.`); + } else { + logger.debug(`No cached response found in '${cacheName}'.`); + } + } + + for (const plugin of plugins) { + if ("cachedResponseWillBeUsed" + /* CACHED_RESPONSE_WILL_BE_USED */ + in plugin) { + const pluginMethod = plugin["cachedResponseWillBeUsed" + /* CACHED_RESPONSE_WILL_BE_USED */ + ]; + cachedResponse = await pluginMethod.call(plugin, { + cacheName, + event, + matchOptions, + cachedResponse, + request: effectiveRequest + }); + + { + if (cachedResponse) { + finalAssertExports.isInstance(cachedResponse, Response, { + moduleName: 'Plugin', + funcName: "cachedResponseWillBeUsed" + /* CACHED_RESPONSE_WILL_BE_USED */ + , + isReturnValueProblem: true + }); + } + } + } + } + + return cachedResponse; + }; + /** + * Wrapper around cache.put(). + * + * Will call `cacheDidUpdate` on plugins if the cache was updated, using + * `matchOptions` when determining what the old entry is. + * + * @param {Object} options + * @param {string} options.cacheName + * @param {Request} options.request + * @param {Response} options.response + * @param {Event} [options.event] + * @param {Array} [options.plugins=[]] + * @param {Object} [options.matchOptions] + * + * @private + * @memberof module:workbox-core + */ + + + const putWrapper = async ({ + cacheName, + request, + response, + event, + plugins = [], + matchOptions + }) => { + { + if (request.method && request.method !== 'GET') { + throw new WorkboxError('attempt-to-cache-non-get-request', { + url: getFriendlyURL(request.url), + method: request.method + }); + } + } + + const effectiveRequest = await _getEffectiveRequest({ + plugins, + request, + mode: 'write' + }); + + if (!response) { + { + logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`); + } + + throw new WorkboxError('cache-put-with-no-response', { + url: getFriendlyURL(effectiveRequest.url) + }); + } + + const responseToCache = await _isResponseSafeToCache({ + event, + plugins, + response, + request: effectiveRequest + }); + + if (!responseToCache) { + { + logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' will ` + `not be cached.`, responseToCache); + } + + return; + } + + const cache = await self.caches.open(cacheName); + const updatePlugins = pluginUtils.filter(plugins, "cacheDidUpdate" + /* CACHE_DID_UPDATE */ + ); + const oldResponse = updatePlugins.length > 0 ? await matchWrapper({ + cacheName, + matchOptions, + request: effectiveRequest + }) : null; + + { + logger.debug(`Updating the '${cacheName}' cache with a new Response for ` + `${getFriendlyURL(effectiveRequest.url)}.`); + } + + try { + await cache.put(effectiveRequest, responseToCache); + } catch (error) { + // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError + if (error.name === 'QuotaExceededError') { + await executeQuotaErrorCallbacks(); + } + + throw error; + } + + for (const plugin of updatePlugins) { + await plugin["cacheDidUpdate" + /* CACHE_DID_UPDATE */ + ].call(plugin, { + cacheName, + event, + oldResponse, + newResponse: responseToCache, + request: effectiveRequest + }); + } + }; + + const cacheWrapper = { + put: putWrapper, + match: matchWrapper + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let supportStatus; + /** + * A utility function that determines whether the current browser supports + * constructing a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream) + * object. + * + * @return {boolean} `true`, if the current browser can successfully + * construct a `ReadableStream`, `false` otherwise. + * + * @private + */ + + function canConstructReadableStream() { + if (supportStatus === undefined) { + // See https://github.com/GoogleChrome/workbox/issues/1473 + try { + new ReadableStream({ + start() {} + + }); + supportStatus = true; + } catch (error) { + supportStatus = false; + } + } + + return supportStatus; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let supportStatus$1; + /** + * A utility function that determines whether the current browser supports + * constructing a new `Response` from a `response.body` stream. + * + * @return {boolean} `true`, if the current browser can successfully + * construct a `Response` from a `response.body` stream, `false` otherwise. + * + * @private + */ + + function canConstructResponseFromBodyStream() { + if (supportStatus$1 === undefined) { + const testResponse = new Response(''); + + if ('body' in testResponse) { + try { + new Response(testResponse.body); + supportStatus$1 = true; + } catch (error) { + supportStatus$1 = false; + } + } + + supportStatus$1 = false; + } + + return supportStatus$1; + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A helper function that prevents a promise from being flagged as unused. + * + * @private + **/ + + function dontWaitFor(promise) { + // Effective no-op. + promise.then(() => {}); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A class that wraps common IndexedDB functionality in a promise-based API. + * It exposes all the underlying power and functionality of IndexedDB, but + * wraps the most commonly used features in a way that's much simpler to use. + * + * @private + */ + + class DBWrapper { + /** + * @param {string} name + * @param {number} version + * @param {Object=} [callback] + * @param {!Function} [callbacks.onupgradeneeded] + * @param {!Function} [callbacks.onversionchange] Defaults to + * DBWrapper.prototype._onversionchange when not specified. + * @private + */ + constructor(name, version, { + onupgradeneeded, + onversionchange + } = {}) { + this._db = null; + this._name = name; + this._version = version; + this._onupgradeneeded = onupgradeneeded; + + this._onversionchange = onversionchange || (() => this.close()); + } + /** + * Returns the IDBDatabase instance (not normally needed). + * @return {IDBDatabase|undefined} + * + * @private + */ + + + get db() { + return this._db; + } + /** + * Opens a connected to an IDBDatabase, invokes any onupgradedneeded + * callback, and added an onversionchange callback to the database. + * + * @return {IDBDatabase} + * @private + */ + + + async open() { + if (this._db) return; + this._db = await new Promise((resolve, reject) => { + // This flag is flipped to true if the timeout callback runs prior + // to the request failing or succeeding. Note: we use a timeout instead + // of an onblocked handler since there are cases where onblocked will + // never never run. A timeout better handles all possible scenarios: + // https://github.com/w3c/IndexedDB/issues/223 + let openRequestTimedOut = false; + setTimeout(() => { + openRequestTimedOut = true; + reject(new Error('The open request was blocked and timed out')); + }, this.OPEN_TIMEOUT); + const openRequest = indexedDB.open(this._name, this._version); + + openRequest.onerror = () => reject(openRequest.error); + + openRequest.onupgradeneeded = evt => { + if (openRequestTimedOut) { + openRequest.transaction.abort(); + openRequest.result.close(); + } else if (typeof this._onupgradeneeded === 'function') { + this._onupgradeneeded(evt); + } + }; + + openRequest.onsuccess = () => { + const db = openRequest.result; + + if (openRequestTimedOut) { + db.close(); + } else { + db.onversionchange = this._onversionchange.bind(this); + resolve(db); + } + }; + }); + return this; + } + /** + * Polyfills the native `getKey()` method. Note, this is overridden at + * runtime if the browser supports the native method. + * + * @param {string} storeName + * @param {*} query + * @return {Array} + * @private + */ + + + async getKey(storeName, query) { + return (await this.getAllKeys(storeName, query, 1))[0]; + } + /** + * Polyfills the native `getAll()` method. Note, this is overridden at + * runtime if the browser supports the native method. + * + * @param {string} storeName + * @param {*} query + * @param {number} count + * @return {Array} + * @private + */ + + + async getAll(storeName, query, count) { + return await this.getAllMatching(storeName, { + query, + count + }); + } + /** + * Polyfills the native `getAllKeys()` method. Note, this is overridden at + * runtime if the browser supports the native method. + * + * @param {string} storeName + * @param {*} query + * @param {number} count + * @return {Array} + * @private + */ + + + async getAllKeys(storeName, query, count) { + const entries = await this.getAllMatching(storeName, { + query, + count, + includeKeys: true + }); + return entries.map(entry => entry.key); + } + /** + * Supports flexible lookup in an object store by specifying an index, + * query, direction, and count. This method returns an array of objects + * with the signature . + * + * @param {string} storeName + * @param {Object} [opts] + * @param {string} [opts.index] The index to use (if specified). + * @param {*} [opts.query] + * @param {IDBCursorDirection} [opts.direction] + * @param {number} [opts.count] The max number of results to return. + * @param {boolean} [opts.includeKeys] When true, the structure of the + * returned objects is changed from an array of values to an array of + * objects in the form {key, primaryKey, value}. + * @return {Array} + * @private + */ + + + async getAllMatching(storeName, { + index, + query = null, + // IE/Edge errors if query === `undefined`. + direction = 'next', + count, + includeKeys = false + } = {}) { + return await this.transaction([storeName], 'readonly', (txn, done) => { + const store = txn.objectStore(storeName); + const target = index ? store.index(index) : store; + const results = []; + const request = target.openCursor(query, direction); + + request.onsuccess = () => { + const cursor = request.result; + + if (cursor) { + results.push(includeKeys ? cursor : cursor.value); + + if (count && results.length >= count) { + done(results); + } else { + cursor.continue(); + } + } else { + done(results); + } + }; + }); + } + /** + * Accepts a list of stores, a transaction type, and a callback and + * performs a transaction. A promise is returned that resolves to whatever + * value the callback chooses. The callback holds all the transaction logic + * and is invoked with two arguments: + * 1. The IDBTransaction object + * 2. A `done` function, that's used to resolve the promise when + * when the transaction is done, if passed a value, the promise is + * resolved to that value. + * + * @param {Array} storeNames An array of object store names + * involved in the transaction. + * @param {string} type Can be `readonly` or `readwrite`. + * @param {!Function} callback + * @return {*} The result of the transaction ran by the callback. + * @private + */ + + + async transaction(storeNames, type, callback) { + await this.open(); + return await new Promise((resolve, reject) => { + const txn = this._db.transaction(storeNames, type); + + txn.onabort = () => reject(txn.error); + + txn.oncomplete = () => resolve(); + + callback(txn, value => resolve(value)); + }); + } + /** + * Delegates async to a native IDBObjectStore method. + * + * @param {string} method The method name. + * @param {string} storeName The object store name. + * @param {string} type Can be `readonly` or `readwrite`. + * @param {...*} args The list of args to pass to the native method. + * @return {*} The result of the transaction. + * @private + */ + + + async _call(method, storeName, type, ...args) { + const callback = (txn, done) => { + const objStore = txn.objectStore(storeName); // TODO(philipwalton): Fix this underlying TS2684 error. + // @ts-ignore + + const request = objStore[method].apply(objStore, args); + + request.onsuccess = () => done(request.result); + }; + + return await this.transaction([storeName], type, callback); + } + /** + * Closes the connection opened by `DBWrapper.open()`. Generally this method + * doesn't need to be called since: + * 1. It's usually better to keep a connection open since opening + * a new connection is somewhat slow. + * 2. Connections are automatically closed when the reference is + * garbage collected. + * The primary use case for needing to close a connection is when another + * reference (typically in another tab) needs to upgrade it and would be + * blocked by the current, open connection. + * + * @private + */ + + + close() { + if (this._db) { + this._db.close(); + + this._db = null; + } + } + + } // Exposed on the prototype to let users modify the default timeout on a + // per-instance or global basis. + + DBWrapper.prototype.OPEN_TIMEOUT = 2000; // Wrap native IDBObjectStore methods according to their mode. + + const methodsToWrap = { + readonly: ['get', 'count', 'getKey', 'getAll', 'getAllKeys'], + readwrite: ['add', 'put', 'clear', 'delete'] + }; + + for (const [mode, methods] of Object.entries(methodsToWrap)) { + for (const method of methods) { + if (method in IDBObjectStore.prototype) { + // Don't use arrow functions here since we're outside of the class. + DBWrapper.prototype[method] = async function (storeName, ...args) { + return await this._call(method, storeName, mode, ...args); + }; + } + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Deferred class composes Promises in a way that allows for them to be + * resolved or rejected from outside the constructor. In most cases promises + * should be used directly, but Deferreds can be necessary when the logic to + * resolve a promise must be separate. + * + * @private + */ + + class Deferred { + /** + * Creates a promise and exposes its resolve and reject functions as methods. + */ + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Deletes the database. + * Note: this is exported separately from the DBWrapper module because most + * usages of IndexedDB in workbox dont need deleting, and this way it can be + * reused in tests to delete databases without creating DBWrapper instances. + * + * @param {string} name The database name. + * @private + */ + + const deleteDatabase = async name => { + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(name); + + request.onerror = () => { + reject(request.error); + }; + + request.onblocked = () => { + reject(new Error('Delete blocked')); + }; + + request.onsuccess = () => { + resolve(); + }; + }); + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Wrapper around the fetch API. + * + * Will call requestWillFetch on available plugins. + * + * @param {Object} options + * @param {Request|string} options.request + * @param {Object} [options.fetchOptions] + * @param {ExtendableEvent} [options.event] + * @param {Array} [options.plugins=[]] + * @return {Promise} + * + * @private + * @memberof module:workbox-core + */ + + const wrappedFetch = async ({ + request, + fetchOptions, + event, + plugins = [] + }) => { + if (typeof request === 'string') { + request = new Request(request); + } // We *should* be able to call `await event.preloadResponse` even if it's + // undefined, but for some reason, doing so leads to errors in our Node unit + // tests. To work around that, explicitly check preloadResponse's value first. + + + if (event instanceof FetchEvent && event.preloadResponse) { + const possiblePreloadResponse = await event.preloadResponse; + + if (possiblePreloadResponse) { + { + logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`); + } + + return possiblePreloadResponse; + } + } + + { + finalAssertExports.isInstance(request, Request, { + paramName: 'request', + expectedClass: Request, + moduleName: 'workbox-core', + className: 'fetchWrapper', + funcName: 'wrappedFetch' + }); + } + + const failedFetchPlugins = pluginUtils.filter(plugins, "fetchDidFail" + /* FETCH_DID_FAIL */ + ); // If there is a fetchDidFail plugin, we need to save a clone of the + // original request before it's either modified by a requestWillFetch + // plugin or before the original request's body is consumed via fetch(). + + const originalRequest = failedFetchPlugins.length > 0 ? request.clone() : null; + + try { + for (const plugin of plugins) { + if ("requestWillFetch" + /* REQUEST_WILL_FETCH */ + in plugin) { + const pluginMethod = plugin["requestWillFetch" + /* REQUEST_WILL_FETCH */ + ]; + const requestClone = request.clone(); + request = await pluginMethod.call(plugin, { + request: requestClone, + event + }); + + if ("dev" !== 'production') { + if (request) { + finalAssertExports.isInstance(request, Request, { + moduleName: 'Plugin', + funcName: "cachedResponseWillBeUsed" + /* CACHED_RESPONSE_WILL_BE_USED */ + , + isReturnValueProblem: true + }); + } + } + } + } + } catch (err) { + throw new WorkboxError('plugin-error-request-will-fetch', { + thrownError: err + }); + } // The request can be altered by plugins with `requestWillFetch` making + // the original request (Most likely from a `fetch` event) to be different + // to the Request we make. Pass both to `fetchDidFail` to aid debugging. + + + const pluginFilteredRequest = request.clone(); + + try { + let fetchResponse; // See https://github.com/GoogleChrome/workbox/issues/1796 + + if (request.mode === 'navigate') { + fetchResponse = await fetch(request); + } else { + fetchResponse = await fetch(request, fetchOptions); + } + + if ("dev" !== 'production') { + logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`); + } + + for (const plugin of plugins) { + if ("fetchDidSucceed" + /* FETCH_DID_SUCCEED */ + in plugin) { + fetchResponse = await plugin["fetchDidSucceed" + /* FETCH_DID_SUCCEED */ + ].call(plugin, { + event, + request: pluginFilteredRequest, + response: fetchResponse + }); + + if ("dev" !== 'production') { + if (fetchResponse) { + finalAssertExports.isInstance(fetchResponse, Response, { + moduleName: 'Plugin', + funcName: "fetchDidSucceed" + /* FETCH_DID_SUCCEED */ + , + isReturnValueProblem: true + }); + } + } + } + } + + return fetchResponse; + } catch (error) { + { + logger.error(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error); + } + + for (const plugin of failedFetchPlugins) { + await plugin["fetchDidFail" + /* FETCH_DID_FAIL */ + ].call(plugin, { + error, + event, + originalRequest: originalRequest.clone(), + request: pluginFilteredRequest.clone() + }); + } + + throw error; + } + }; + + const fetchWrapper = { + fetch: wrappedFetch + }; + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Returns a promise that resolves and the passed number of milliseconds. + * This utility is an async/await-friendly version of `setTimeout`. + * + * @param {number} ms + * @return {Promise} + * @private + */ + + function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const MAX_RETRY_TIME = 2000; + /** + * Returns a promise that resolves to a window client matching the passed + * `resultingClientId`. For browsers that don't support `resultingClientId` + * or if waiting for the resulting client to apper takes too long, resolve to + * `undefined`. + * + * @param {string} [resultingClientId] + * @return {Promise} + * @private + */ + + async function resultingClientExists(resultingClientId) { + if (!resultingClientId) { + return; + } + + let existingWindows = await self.clients.matchAll({ + type: 'window' + }); + const existingWindowIds = new Set(existingWindows.map(w => w.id)); + let resultingWindow; + const startTime = performance.now(); // Only wait up to `MAX_RETRY_TIME` to find a matching client. + + while (performance.now() - startTime < MAX_RETRY_TIME) { + existingWindows = await self.clients.matchAll({ + type: 'window' + }); + resultingWindow = existingWindows.find(w => { + if (resultingClientId) { + // If we have a `resultingClientId`, we can match on that. + return w.id === resultingClientId; + } else { + // Otherwise match on finding a window not in `existingWindowIds`. + return !existingWindowIds.has(w.id); + } + }); + + if (resultingWindow) { + break; + } // Sleep for 100ms and retry. + + + await timeout(100); + } + + return resultingWindow; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + var _private = /*#__PURE__*/Object.freeze({ + __proto__: null, + assert: finalAssertExports, + cacheNames: cacheNames, + cacheWrapper: cacheWrapper, + canConstructReadableStream: canConstructReadableStream, + canConstructResponseFromBodyStream: canConstructResponseFromBodyStream, + dontWaitFor: dontWaitFor, + DBWrapper: DBWrapper, + Deferred: Deferred, + deleteDatabase: deleteDatabase, + executeQuotaErrorCallbacks: executeQuotaErrorCallbacks, + fetchWrapper: fetchWrapper, + getFriendlyURL: getFriendlyURL, + logger: logger, + resultingClientExists: resultingClientExists, + timeout: timeout, + WorkboxError: WorkboxError + }); + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Get the current cache names and prefix/suffix used by Workbox. + * + * `cacheNames.precache` is used for precached assets, + * `cacheNames.googleAnalytics` is used by `workbox-google-analytics` to + * store `analytics.js`, and `cacheNames.runtime` is used for everything else. + * + * `cacheNames.prefix` can be used to retrieve just the current prefix value. + * `cacheNames.suffix` can be used to retrieve just the current suffix value. + * + * @return {Object} An object with `precache`, `runtime`, `prefix`, and + * `googleAnalytics` properties. + * + * @memberof module:workbox-core + */ + + const cacheNames$1 = { + get googleAnalytics() { + return cacheNames.getGoogleAnalyticsName(); + }, + + get precache() { + return cacheNames.getPrecacheName(); + }, + + get prefix() { + return cacheNames.getPrefix(); + }, + + get runtime() { + return cacheNames.getRuntimeName(); + }, + + get suffix() { + return cacheNames.getSuffix(); + } + + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Allows developers to copy a response and modify its `headers`, `status`, + * or `statusText` values (the values settable via a + * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax} + * object in the constructor). + * To modify these values, pass a function as the second argument. That + * function will be invoked with a single object with the response properties + * `{headers, status, statusText}`. The return value of this function will + * be used as the `ResponseInit` for the new `Response`. To change the values + * either modify the passed parameter(s) and return it, or return a totally + * new object. + * + * @param {Response} response + * @param {Function} modifier + * @memberof module:workbox-core + */ + + async function copyResponse(response, modifier) { + const clonedResponse = response.clone(); // Create a fresh `ResponseInit` object by cloning the headers. + + const responseInit = { + headers: new Headers(clonedResponse.headers), + status: clonedResponse.status, + statusText: clonedResponse.statusText + }; // Apply any user modifications. + + const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit; // Create the new response from the body stream and `ResponseInit` + // modifications. Note: not all browsers support the Response.body stream, + // so fall back to reading the entire body into memory as a blob. + + const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob(); + return new Response(body, modifiedResponseInit); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Claim any currently available clients once the service worker + * becomes active. This is normally used in conjunction with `skipWaiting()`. + * + * @memberof module:workbox-core + */ + + function clientsClaim() { + self.addEventListener('activate', () => self.clients.claim()); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Modifies the default cache names used by the Workbox packages. + * Cache names are generated as `--`. + * + * @param {Object} details + * @param {Object} [details.prefix] The string to add to the beginning of + * the precache and runtime cache names. + * @param {Object} [details.suffix] The string to add to the end of + * the precache and runtime cache names. + * @param {Object} [details.precache] The cache name to use for precache + * caching. + * @param {Object} [details.runtime] The cache name to use for runtime caching. + * @param {Object} [details.googleAnalytics] The cache name to use for + * `workbox-google-analytics` caching. + * + * @memberof module:workbox-core + */ + + function setCacheNameDetails(details) { + { + Object.keys(details).forEach(key => { + finalAssertExports.isType(details[key], 'string', { + moduleName: 'workbox-core', + funcName: 'setCacheNameDetails', + paramName: `details.${key}` + }); + }); + + if ('precache' in details && details['precache'].length === 0) { + throw new WorkboxError('invalid-cache-name', { + cacheNameId: 'precache', + value: details['precache'] + }); + } + + if ('runtime' in details && details['runtime'].length === 0) { + throw new WorkboxError('invalid-cache-name', { + cacheNameId: 'runtime', + value: details['runtime'] + }); + } + + if ('googleAnalytics' in details && details['googleAnalytics'].length === 0) { + throw new WorkboxError('invalid-cache-name', { + cacheNameId: 'googleAnalytics', + value: details['googleAnalytics'] + }); + } + } + + cacheNames.updateDetails(details); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Force a service worker to activate immediately, instead of + * [waiting](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting) + * for existing clients to close. + * + * @memberof module:workbox-core + */ + + function skipWaiting() { + // We need to explicitly call `self.skipWaiting()` here because we're + // shadowing `skipWaiting` with this local function. + self.addEventListener('install', () => self.skipWaiting()); + } + + exports._private = _private; + exports.cacheNames = cacheNames$1; + exports.clientsClaim = clientsClaim; + exports.copyResponse = copyResponse; + exports.registerQuotaErrorCallback = registerQuotaErrorCallback; + exports.setCacheNameDetails = setCacheNameDetails; + exports.skipWaiting = skipWaiting; + + return exports; + +}({})); + diff --git a/source/js/workbox/workbox-core.prod.js b/source/js/workbox/workbox-core.prod.js new file mode 100644 index 0000000..8693770 --- /dev/null +++ b/source/js/workbox/workbox-core.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.core=function(e){"use strict";try{self["workbox:core:5.1.4"]&&_()}catch(e){}const t=(e,...t)=>{let n=e;return t.length>0&&(n+=" :: "+JSON.stringify(t)),n};class n extends Error{constructor(e,n){super(t(e,n)),this.name=e,this.details=n}}const s=new Set;const r={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},i=e=>[r.prefix,e,r.suffix].filter(e=>e&&e.length>0).join("-"),o={updateDetails:e=>{(e=>{for(const t of Object.keys(r))e(t)})(t=>{"string"==typeof e[t]&&(r[t]=e[t])})},getGoogleAnalyticsName:e=>e||i(r.googleAnalytics),getPrecacheName:e=>e||i(r.precache),getPrefix:()=>r.prefix,getRuntimeName:e=>e||i(r.runtime),getSuffix:()=>r.suffix};async function a(){for(const e of s)await e()}const c=e=>new URL(String(e),location.href).href.replace(new RegExp("^"+location.origin),""),u=(e,t)=>e.filter(e=>t in e),l=async({request:e,mode:t,plugins:n=[]})=>{const s=u(n,"cacheKeyWillBeUsed");let r=e;for(const e of s)r=await e.cacheKeyWillBeUsed.call(e,{mode:t,request:r}),"string"==typeof r&&(r=new Request(r));return r},f=async({cacheName:e,request:t,event:n,matchOptions:s,plugins:r=[]})=>{const i=await self.caches.open(e),o=await l({plugins:r,request:t,mode:"read"});let a=await i.match(o,s);for(const t of r)if("cachedResponseWillBeUsed"in t){const r=t.cachedResponseWillBeUsed;a=await r.call(t,{cacheName:e,event:n,matchOptions:s,cachedResponse:a,request:o})}return a},h={put:async({cacheName:e,request:t,response:s,event:r,plugins:i=[],matchOptions:o})=>{const h=await l({plugins:i,request:t,mode:"write"});if(!s)throw new n("cache-put-with-no-response",{url:c(h.url)});const w=await(async({request:e,response:t,event:n,plugins:s=[]})=>{let r=t,i=!1;for(const t of s)if("cacheWillUpdate"in t){i=!0;const s=t.cacheWillUpdate;if(r=await s.call(t,{request:e,response:r,event:n}),!r)break}return i||(r=r&&200===r.status?r:void 0),r||null})({event:r,plugins:i,response:s,request:h});if(!w)return;const p=await self.caches.open(e),d=u(i,"cacheDidUpdate"),g=d.length>0?await f({cacheName:e,matchOptions:o,request:h}):null;try{await p.put(h,w)}catch(e){throw"QuotaExceededError"===e.name&&await a(),e}for(const t of d)await t.cacheDidUpdate.call(t,{cacheName:e,event:r,oldResponse:g,newResponse:w,request:h})},match:f};let w,p;function d(){if(void 0===p){const e=new Response("");if("body"in e)try{new Response(e.body),p=!0}catch(e){p=!1}p=!1}return p}class g{constructor(e,t,{onupgradeneeded:n,onversionchange:s}={}){this.t=null,this.s=e,this.i=t,this.o=n,this.u=s||(()=>this.close())}get db(){return this.t}async open(){if(!this.t)return this.t=await new Promise((e,t)=>{let n=!1;setTimeout(()=>{n=!0,t(new Error("The open request was blocked and timed out"))},this.OPEN_TIMEOUT);const s=indexedDB.open(this.s,this.i);s.onerror=()=>t(s.error),s.onupgradeneeded=e=>{n?(s.transaction.abort(),s.result.close()):"function"==typeof this.o&&this.o(e)},s.onsuccess=()=>{const t=s.result;n?t.close():(t.onversionchange=this.u.bind(this),e(t))}}),this}async getKey(e,t){return(await this.getAllKeys(e,t,1))[0]}async getAll(e,t,n){return await this.getAllMatching(e,{query:t,count:n})}async getAllKeys(e,t,n){return(await this.getAllMatching(e,{query:t,count:n,includeKeys:!0})).map(e=>e.key)}async getAllMatching(e,{index:t,query:n=null,direction:s="next",count:r,includeKeys:i=!1}={}){return await this.transaction([e],"readonly",(o,a)=>{const c=o.objectStore(e),u=t?c.index(t):c,l=[],f=u.openCursor(n,s);f.onsuccess=()=>{const e=f.result;e?(l.push(i?e:e.value),r&&l.length>=r?a(l):e.continue()):a(l)}})}async transaction(e,t,n){return await this.open(),await new Promise((s,r)=>{const i=this.t.transaction(e,t);i.onabort=()=>r(i.error),i.oncomplete=()=>s(),n(i,e=>s(e))})}async l(e,t,n,...s){return await this.transaction([t],n,(n,r)=>{const i=n.objectStore(t),o=i[e].apply(i,s);o.onsuccess=()=>r(o.result)})}close(){this.t&&(this.t.close(),this.t=null)}}g.prototype.OPEN_TIMEOUT=2e3;const y={readonly:["get","count","getKey","getAll","getAllKeys"],readwrite:["add","put","clear","delete"]};for(const[e,t]of Object.entries(y))for(const n of t)n in IDBObjectStore.prototype&&(g.prototype[n]=async function(t,...s){return await this.l(n,t,e,...s)});const m={fetch:async({request:e,fetchOptions:t,event:s,plugins:r=[]})=>{if("string"==typeof e&&(e=new Request(e)),s instanceof FetchEvent&&s.preloadResponse){const e=await s.preloadResponse;if(e)return e}const i=u(r,"fetchDidFail"),o=i.length>0?e.clone():null;try{for(const t of r)if("requestWillFetch"in t){const n=t.requestWillFetch,r=e.clone();e=await n.call(t,{request:r,event:s})}}catch(e){throw new n("plugin-error-request-will-fetch",{thrownError:e})}const a=e.clone();try{let n;n="navigate"===e.mode?await fetch(e):await fetch(e,t);for(const e of r)"fetchDidSucceed"in e&&(n=await e.fetchDidSucceed.call(e,{event:s,request:a,response:n}));return n}catch(e){for(const t of i)await t.fetchDidFail.call(t,{error:e,event:s,originalRequest:o.clone(),request:a.clone()});throw e}}};function q(e){return new Promise(t=>setTimeout(t,e))}var v=Object.freeze({__proto__:null,assert:null,cacheNames:o,cacheWrapper:h,canConstructReadableStream:function(){if(void 0===w)try{new ReadableStream({start(){}}),w=!0}catch(e){w=!1}return w},canConstructResponseFromBodyStream:d,dontWaitFor:function(e){e.then(()=>{})},DBWrapper:g,Deferred:class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},deleteDatabase:async e=>{await new Promise((t,n)=>{const s=indexedDB.deleteDatabase(e);s.onerror=()=>{n(s.error)},s.onblocked=()=>{n(new Error("Delete blocked"))},s.onsuccess=()=>{t()}})},executeQuotaErrorCallbacks:a,fetchWrapper:m,getFriendlyURL:c,logger:null,resultingClientExists:async function(e){if(!e)return;let t=await self.clients.matchAll({type:"window"});const n=new Set(t.map(e=>e.id));let s;const r=performance.now();for(;performance.now()-r<2e3&&(t=await self.clients.matchAll({type:"window"}),s=t.find(t=>e?t.id===e:!n.has(t.id)),!s);)await q(100);return s},timeout:q,WorkboxError:n});const x={get googleAnalytics(){return o.getGoogleAnalyticsName()},get precache(){return o.getPrecacheName()},get prefix(){return o.getPrefix()},get runtime(){return o.getRuntimeName()},get suffix(){return o.getSuffix()}};return e._private=v,e.cacheNames=x,e.clientsClaim=function(){self.addEventListener("activate",()=>self.clients.claim())},e.copyResponse=async function(e,t){const n=e.clone(),s={headers:new Headers(n.headers),status:n.status,statusText:n.statusText},r=t?t(s):s,i=d()?n.body:await n.blob();return new Response(i,r)},e.registerQuotaErrorCallback=function(e){s.add(e)},e.setCacheNameDetails=function(e){o.updateDetails(e)},e.skipWaiting=function(){self.addEventListener("install",()=>self.skipWaiting())},e}({}); + diff --git a/source/js/workbox/workbox-expiration.dev.js b/source/js/workbox/workbox-expiration.dev.js new file mode 100644 index 0000000..a335adb --- /dev/null +++ b/source/js/workbox/workbox-expiration.dev.js @@ -0,0 +1,649 @@ +this.workbox = this.workbox || {}; +this.workbox.expiration = (function (exports, assert_js, dontWaitFor_js, logger_js, WorkboxError_js, DBWrapper_js, deleteDatabase_js, cacheNames_js, getFriendlyURL_js, registerQuotaErrorCallback_js) { + 'use strict'; + + try { + self['workbox:expiration:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const DB_NAME = 'workbox-expiration'; + const OBJECT_STORE_NAME = 'cache-entries'; + + const normalizeURL = unNormalizedUrl => { + const url = new URL(unNormalizedUrl, location.href); + url.hash = ''; + return url.href; + }; + /** + * Returns the timestamp model. + * + * @private + */ + + + class CacheTimestampsModel { + /** + * + * @param {string} cacheName + * + * @private + */ + constructor(cacheName) { + this._cacheName = cacheName; + this._db = new DBWrapper_js.DBWrapper(DB_NAME, 1, { + onupgradeneeded: event => this._handleUpgrade(event) + }); + } + /** + * Should perform an upgrade of indexedDB. + * + * @param {Event} event + * + * @private + */ + + + _handleUpgrade(event) { + const db = event.target.result; // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we + // have to use the `id` keyPath here and create our own values (a + // concatenation of `url + cacheName`) instead of simply using + // `keyPath: ['url', 'cacheName']`, which is supported in other browsers. + + const objStore = db.createObjectStore(OBJECT_STORE_NAME, { + keyPath: 'id' + }); // TODO(philipwalton): once we don't have to support EdgeHTML, we can + // create a single index with the keyPath `['cacheName', 'timestamp']` + // instead of doing both these indexes. + + objStore.createIndex('cacheName', 'cacheName', { + unique: false + }); + objStore.createIndex('timestamp', 'timestamp', { + unique: false + }); // Previous versions of `workbox-expiration` used `this._cacheName` + // as the IDBDatabase name. + + deleteDatabase_js.deleteDatabase(this._cacheName); + } + /** + * @param {string} url + * @param {number} timestamp + * + * @private + */ + + + async setTimestamp(url, timestamp) { + url = normalizeURL(url); + const entry = { + url, + timestamp, + cacheName: this._cacheName, + // Creating an ID from the URL and cache name won't be necessary once + // Edge switches to Chromium and all browsers we support work with + // array keyPaths. + id: this._getId(url) + }; + await this._db.put(OBJECT_STORE_NAME, entry); + } + /** + * Returns the timestamp stored for a given URL. + * + * @param {string} url + * @return {number} + * + * @private + */ + + + async getTimestamp(url) { + const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url)); + return entry.timestamp; + } + /** + * Iterates through all the entries in the object store (from newest to + * oldest) and removes entries once either `maxCount` is reached or the + * entry's timestamp is less than `minTimestamp`. + * + * @param {number} minTimestamp + * @param {number} maxCount + * @return {Array} + * + * @private + */ + + + async expireEntries(minTimestamp, maxCount) { + const entriesToDelete = await this._db.transaction(OBJECT_STORE_NAME, 'readwrite', (txn, done) => { + const store = txn.objectStore(OBJECT_STORE_NAME); + const request = store.index('timestamp').openCursor(null, 'prev'); + const entriesToDelete = []; + let entriesNotDeletedCount = 0; + + request.onsuccess = () => { + const cursor = request.result; + + if (cursor) { + const result = cursor.value; // TODO(philipwalton): once we can use a multi-key index, we + // won't have to check `cacheName` here. + + if (result.cacheName === this._cacheName) { + // Delete an entry if it's older than the max age or + // if we already have the max number allowed. + if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) { + // TODO(philipwalton): we should be able to delete the + // entry right here, but doing so causes an iteration + // bug in Safari stable (fixed in TP). Instead we can + // store the keys of the entries to delete, and then + // delete the separate transactions. + // https://github.com/GoogleChrome/workbox/issues/1978 + // cursor.delete(); + // We only need to return the URL, not the whole entry. + entriesToDelete.push(cursor.value); + } else { + entriesNotDeletedCount++; + } + } + + cursor.continue(); + } else { + done(entriesToDelete); + } + }; + }); // TODO(philipwalton): once the Safari bug in the following issue is fixed, + // we should be able to remove this loop and do the entry deletion in the + // cursor loop above: + // https://github.com/GoogleChrome/workbox/issues/1978 + + const urlsDeleted = []; + + for (const entry of entriesToDelete) { + await this._db.delete(OBJECT_STORE_NAME, entry.id); + urlsDeleted.push(entry.url); + } + + return urlsDeleted; + } + /** + * Takes a URL and returns an ID that will be unique in the object store. + * + * @param {string} url + * @return {string} + * + * @private + */ + + + _getId(url) { + // Creating an ID from the URL and cache name won't be necessary once + // Edge switches to Chromium and all browsers we support work with + // array keyPaths. + return this._cacheName + '|' + normalizeURL(url); + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The `CacheExpiration` class allows you define an expiration and / or + * limit on the number of responses stored in a + * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache). + * + * @memberof module:workbox-expiration + */ + + class CacheExpiration { + /** + * To construct a new CacheExpiration instance you must provide at least + * one of the `config` properties. + * + * @param {string} cacheName Name of the cache to apply restrictions to. + * @param {Object} config + * @param {number} [config.maxEntries] The maximum number of entries to cache. + * Entries used the least will be removed as the maximum is reached. + * @param {number} [config.maxAgeSeconds] The maximum age of an entry before + * it's treated as stale and removed. + */ + constructor(cacheName, config = {}) { + this._isRunning = false; + this._rerunRequested = false; + + { + assert_js.assert.isType(cacheName, 'string', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'cacheName' + }); + + if (!(config.maxEntries || config.maxAgeSeconds)) { + throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor' + }); + } + + if (config.maxEntries) { + assert_js.assert.isType(config.maxEntries, 'number', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'config.maxEntries' + }); // TODO: Assert is positive + } + + if (config.maxAgeSeconds) { + assert_js.assert.isType(config.maxAgeSeconds, 'number', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'config.maxAgeSeconds' + }); // TODO: Assert is positive + } + } + + this._maxEntries = config.maxEntries; + this._maxAgeSeconds = config.maxAgeSeconds; + this._cacheName = cacheName; + this._timestampModel = new CacheTimestampsModel(cacheName); + } + /** + * Expires entries for the given cache and given criteria. + */ + + + async expireEntries() { + if (this._isRunning) { + this._rerunRequested = true; + return; + } + + this._isRunning = true; + const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0; + const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); // Delete URLs from the cache + + const cache = await self.caches.open(this._cacheName); + + for (const url of urlsExpired) { + await cache.delete(url); + } + + { + if (urlsExpired.length > 0) { + logger_js.logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` + `${urlsExpired.length === 1 ? 'it' : 'them'} from the ` + `'${this._cacheName}' cache.`); + logger_js.logger.log(`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`); + urlsExpired.forEach(url => logger_js.logger.log(` ${url}`)); + logger_js.logger.groupEnd(); + } else { + logger_js.logger.debug(`Cache expiration ran and found no entries to remove.`); + } + } + + this._isRunning = false; + + if (this._rerunRequested) { + this._rerunRequested = false; + dontWaitFor_js.dontWaitFor(this.expireEntries()); + } + } + /** + * Update the timestamp for the given URL. This ensures the when + * removing entries based on maximum entries, most recently used + * is accurate or when expiring, the timestamp is up-to-date. + * + * @param {string} url + */ + + + async updateTimestamp(url) { + { + assert_js.assert.isType(url, 'string', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'updateTimestamp', + paramName: 'url' + }); + } + + await this._timestampModel.setTimestamp(url, Date.now()); + } + /** + * Can be used to check if a URL has expired or not before it's used. + * + * This requires a look up from IndexedDB, so can be slow. + * + * Note: This method will not remove the cached entry, call + * `expireEntries()` to remove indexedDB and Cache entries. + * + * @param {string} url + * @return {boolean} + */ + + + async isURLExpired(url) { + if (!this._maxAgeSeconds) { + { + throw new WorkboxError_js.WorkboxError(`expired-test-without-max-age`, { + methodName: 'isURLExpired', + paramName: 'maxAgeSeconds' + }); + } + } else { + const timestamp = await this._timestampModel.getTimestamp(url); + const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000; + return timestamp < expireOlderThan; + } + } + /** + * Removes the IndexedDB object store used to keep track of cache expiration + * metadata. + */ + + + async delete() { + // Make sure we don't attempt another rerun if we're called in the middle of + // a cache expiration. + this._rerunRequested = false; + await this._timestampModel.expireEntries(Infinity); // Expires all. + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This plugin can be used in the Workbox APIs to regularly enforce a + * limit on the age and / or the number of cached requests. + * + * Whenever a cached request is used or updated, this plugin will look + * at the used Cache and remove any old or extra requests. + * + * When using `maxAgeSeconds`, requests may be used *once* after expiring + * because the expiration clean up will not have occurred until *after* the + * cached request has been used. If the request has a "Date" header, then + * a light weight expiration check is performed and the request will not be + * used immediately. + * + * When using `maxEntries`, the entry least-recently requested will be removed + * from the cache first. + * + * @memberof module:workbox-expiration + */ + + class ExpirationPlugin { + /** + * @param {Object} config + * @param {number} [config.maxEntries] The maximum number of entries to cache. + * Entries used the least will be removed as the maximum is reached. + * @param {number} [config.maxAgeSeconds] The maximum age of an entry before + * it's treated as stale and removed. + * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to + * automatic deletion if the available storage quota has been exceeded. + */ + constructor(config = {}) { + /** + * A "lifecycle" callback that will be triggered automatically by the + * `workbox-strategies` handlers when a `Response` is about to be returned + * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to + * the handler. It allows the `Response` to be inspected for freshness and + * prevents it from being used if the `Response`'s `Date` header value is + * older than the configured `maxAgeSeconds`. + * + * @param {Object} options + * @param {string} options.cacheName Name of the cache the response is in. + * @param {Response} options.cachedResponse The `Response` object that's been + * read from a cache and whose freshness should be checked. + * @return {Response} Either the `cachedResponse`, if it's + * fresh, or `null` if the `Response` is older than `maxAgeSeconds`. + * + * @private + */ + this.cachedResponseWillBeUsed = async ({ + event, + request, + cacheName, + cachedResponse + }) => { + if (!cachedResponse) { + return null; + } + + const isFresh = this._isResponseDateFresh(cachedResponse); // Expire entries to ensure that even if the expiration date has + // expired, it'll only be used once. + + + const cacheExpiration = this._getCacheExpiration(cacheName); + + dontWaitFor_js.dontWaitFor(cacheExpiration.expireEntries()); // Update the metadata for the request URL to the current timestamp, + // but don't `await` it as we don't want to block the response. + + const updateTimestampDone = cacheExpiration.updateTimestamp(request.url); + + if (event) { + try { + event.waitUntil(updateTimestampDone); + } catch (error) { + { + // The event may not be a fetch event; only log the URL if it is. + if ('request' in event) { + logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache entry for ` + `'${getFriendlyURL_js.getFriendlyURL(event.request.url)}'.`); + } + } + } + } + + return isFresh ? cachedResponse : null; + }; + /** + * A "lifecycle" callback that will be triggered automatically by the + * `workbox-strategies` handlers when an entry is added to a cache. + * + * @param {Object} options + * @param {string} options.cacheName Name of the cache that was updated. + * @param {string} options.request The Request for the cached entry. + * + * @private + */ + + + this.cacheDidUpdate = async ({ + cacheName, + request + }) => { + { + assert_js.assert.isType(cacheName, 'string', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'cacheDidUpdate', + paramName: 'cacheName' + }); + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'cacheDidUpdate', + paramName: 'request' + }); + } + + const cacheExpiration = this._getCacheExpiration(cacheName); + + await cacheExpiration.updateTimestamp(request.url); + await cacheExpiration.expireEntries(); + }; + + { + if (!(config.maxEntries || config.maxAgeSeconds)) { + throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor' + }); + } + + if (config.maxEntries) { + assert_js.assert.isType(config.maxEntries, 'number', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor', + paramName: 'config.maxEntries' + }); + } + + if (config.maxAgeSeconds) { + assert_js.assert.isType(config.maxAgeSeconds, 'number', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor', + paramName: 'config.maxAgeSeconds' + }); + } + } + + this._config = config; + this._maxAgeSeconds = config.maxAgeSeconds; + this._cacheExpirations = new Map(); + + if (config.purgeOnQuotaError) { + registerQuotaErrorCallback_js.registerQuotaErrorCallback(() => this.deleteCacheAndMetadata()); + } + } + /** + * A simple helper method to return a CacheExpiration instance for a given + * cache name. + * + * @param {string} cacheName + * @return {CacheExpiration} + * + * @private + */ + + + _getCacheExpiration(cacheName) { + if (cacheName === cacheNames_js.cacheNames.getRuntimeName()) { + throw new WorkboxError_js.WorkboxError('expire-custom-caches-only'); + } + + let cacheExpiration = this._cacheExpirations.get(cacheName); + + if (!cacheExpiration) { + cacheExpiration = new CacheExpiration(cacheName, this._config); + + this._cacheExpirations.set(cacheName, cacheExpiration); + } + + return cacheExpiration; + } + /** + * @param {Response} cachedResponse + * @return {boolean} + * + * @private + */ + + + _isResponseDateFresh(cachedResponse) { + if (!this._maxAgeSeconds) { + // We aren't expiring by age, so return true, it's fresh + return true; + } // Check if the 'date' header will suffice a quick expiration check. + // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for + // discussion. + + + const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse); + + if (dateHeaderTimestamp === null) { + // Unable to parse date, so assume it's fresh. + return true; + } // If we have a valid headerTime, then our response is fresh iff the + // headerTime plus maxAgeSeconds is greater than the current time. + + + const now = Date.now(); + return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000; + } + /** + * This method will extract the data header and parse it into a useful + * value. + * + * @param {Response} cachedResponse + * @return {number|null} + * + * @private + */ + + + _getDateHeaderTimestamp(cachedResponse) { + if (!cachedResponse.headers.has('date')) { + return null; + } + + const dateHeader = cachedResponse.headers.get('date'); + const parsedDate = new Date(dateHeader); + const headerTime = parsedDate.getTime(); // If the Date header was invalid for some reason, parsedDate.getTime() + // will return NaN. + + if (isNaN(headerTime)) { + return null; + } + + return headerTime; + } + /** + * This is a helper method that performs two operations: + * + * - Deletes *all* the underlying Cache instances associated with this plugin + * instance, by calling caches.delete() on your behalf. + * - Deletes the metadata from IndexedDB used to keep track of expiration + * details for each Cache instance. + * + * When using cache expiration, calling this method is preferable to calling + * `caches.delete()` directly, since this will ensure that the IndexedDB + * metadata is also cleanly removed and open IndexedDB instances are deleted. + * + * Note that if you're *not* using cache expiration for a given cache, calling + * `caches.delete()` and passing in the cache's name should be sufficient. + * There is no Workbox-specific method needed for cleanup in that case. + */ + + + async deleteCacheAndMetadata() { + // Do this one at a time instead of all at once via `Promise.all()` to + // reduce the chance of inconsistency if a promise rejects. + for (const [cacheName, cacheExpiration] of this._cacheExpirations) { + await self.caches.delete(cacheName); + await cacheExpiration.delete(); + } // Reset this._cacheExpirations to its initial state. + + + this._cacheExpirations = new Map(); + } + + } + + exports.CacheExpiration = CacheExpiration; + exports.ExpirationPlugin = ExpirationPlugin; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core)); + diff --git a/source/js/workbox/workbox-expiration.prod.js b/source/js/workbox/workbox-expiration.prod.js new file mode 100644 index 0000000..55a8f89 --- /dev/null +++ b/source/js/workbox/workbox-expiration.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.expiration=function(t,e,s,i,a,n,h){"use strict";try{self["workbox:expiration:5.1.4"]&&_()}catch(t){}const r=t=>{const e=new URL(t,location.href);return e.hash="",e.href};class c{constructor(t){this.t=t,this.s=new i.DBWrapper("workbox-expiration",1,{onupgradeneeded:t=>this.i(t)})}i(t){const e=t.target.result.createObjectStore("cache-entries",{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1}),a.deleteDatabase(this.t)}async setTimestamp(t,e){const s={url:t=r(t),timestamp:e,cacheName:this.t,id:this.h(t)};await this.s.put("cache-entries",s)}async getTimestamp(t){return(await this.s.get("cache-entries",this.h(t))).timestamp}async expireEntries(t,e){const s=await this.s.transaction("cache-entries","readwrite",(s,i)=>{const a=s.objectStore("cache-entries").index("timestamp").openCursor(null,"prev"),n=[];let h=0;a.onsuccess=()=>{const s=a.result;if(s){const i=s.value;i.cacheName===this.t&&(t&&i.timestamp=e?n.push(s.value):h++),s.continue()}else i(n)}}),i=[];for(const t of s)await this.s.delete("cache-entries",t.id),i.push(t.url);return i}h(t){return this.t+"|"+r(t)}}class o{constructor(t,e={}){this.o=!1,this.u=!1,this.l=e.maxEntries,this.m=e.maxAgeSeconds,this.t=t,this.p=new c(t)}async expireEntries(){if(this.o)return void(this.u=!0);this.o=!0;const t=this.m?Date.now()-1e3*this.m:0,s=await this.p.expireEntries(t,this.l),i=await self.caches.open(this.t);for(const t of s)await i.delete(t);this.o=!1,this.u&&(this.u=!1,e.dontWaitFor(this.expireEntries()))}async updateTimestamp(t){await this.p.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.m){return await this.p.getTimestamp(t){if(!a)return null;const n=this.k(a),h=this.D(i);e.dontWaitFor(h.expireEntries());const r=h.updateTimestamp(s.url);if(t)try{t.waitUntil(r)}catch(t){}return n?a:null},this.cacheDidUpdate=async({cacheName:t,request:e})=>{const s=this.D(t);await s.updateTimestamp(e.url),await s.expireEntries()},this.N=t,this.m=t.maxAgeSeconds,this.g=new Map,t.purgeOnQuotaError&&h.registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata())}D(t){if(t===n.cacheNames.getRuntimeName())throw new s.WorkboxError("expire-custom-caches-only");let e=this.g.get(t);return e||(e=new o(t,this.N),this.g.set(t,e)),e}k(t){if(!this.m)return!0;const e=this._(t);if(null===e)return!0;return e>=Date.now()-1e3*this.m}_(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async deleteCacheAndMetadata(){for(const[t,e]of this.g)await self.caches.delete(t),await e.delete();this.g=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core); + diff --git a/source/js/workbox/workbox-navigation-preload.dev.js b/source/js/workbox/workbox-navigation-preload.dev.js new file mode 100644 index 0000000..22e11f5 --- /dev/null +++ b/source/js/workbox/workbox-navigation-preload.dev.js @@ -0,0 +1,102 @@ +this.workbox = this.workbox || {}; +this.workbox.navigationPreload = (function (exports, logger_js) { + 'use strict'; + + try { + self['workbox:navigation-preload:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @return {boolean} Whether or not the current browser supports enabling + * navigation preload. + * + * @memberof module:workbox-navigation-preload + */ + + function isSupported() { + return Boolean(self.registration && self.registration.navigationPreload); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * If the browser supports Navigation Preload, then this will disable it. + * + * @memberof module:workbox-navigation-preload + */ + + function disable() { + if (isSupported()) { + self.addEventListener('activate', event => { + event.waitUntil(self.registration.navigationPreload.disable().then(() => { + { + logger_js.logger.log(`Navigation preload is disabled.`); + } + })); + }); + } else { + { + logger_js.logger.log(`Navigation preload is not supported in this browser.`); + } + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * If the browser supports Navigation Preload, then this will enable it. + * + * @param {string} [headerValue] Optionally, allows developers to + * [override](https://developers.google.com/web/updates/2017/02/navigation-preload#changing_the_header) + * the value of the `Service-Worker-Navigation-Preload` header which will be + * sent to the server when making the navigation request. + * + * @memberof module:workbox-navigation-preload + */ + + function enable(headerValue) { + if (isSupported()) { + self.addEventListener('activate', event => { + event.waitUntil(self.registration.navigationPreload.enable().then(() => { + // Defaults to Service-Worker-Navigation-Preload: true if not set. + if (headerValue) { + self.registration.navigationPreload.setHeaderValue(headerValue); + } + + { + logger_js.logger.log(`Navigation preload is enabled.`); + } + })); + }); + } else { + { + logger_js.logger.log(`Navigation preload is not supported in this browser.`); + } + } + } + + exports.disable = disable; + exports.enable = enable; + exports.isSupported = isSupported; + + return exports; + +}({}, workbox.core._private)); + diff --git a/source/js/workbox/workbox-navigation-preload.prod.js b/source/js/workbox/workbox-navigation-preload.prod.js new file mode 100644 index 0000000..3495e9f --- /dev/null +++ b/source/js/workbox/workbox-navigation-preload.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:5.1.4"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",t=>{t.waitUntil(self.registration.navigationPreload.disable().then(()=>{}))})},t.enable=function(t){e()&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{t&&self.registration.navigationPreload.setHeaderValue(t)}))})},t.isSupported=e,t}({}); + diff --git a/source/js/workbox/workbox-offline-ga.dev.js b/source/js/workbox/workbox-offline-ga.dev.js new file mode 100644 index 0000000..8486fe6 --- /dev/null +++ b/source/js/workbox/workbox-offline-ga.dev.js @@ -0,0 +1,235 @@ +this.workbox = this.workbox || {}; +this.workbox.googleAnalytics = (function (exports, BackgroundSyncPlugin_js, cacheNames_js, getFriendlyURL_js, logger_js, Route_js, Router_js, NetworkFirst_js, NetworkOnly_js) { + 'use strict'; + + try { + self['workbox:google-analytics:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const QUEUE_NAME = 'workbox-google-analytics'; + const MAX_RETENTION_TIME = 60 * 48; // Two days in minutes + + const GOOGLE_ANALYTICS_HOST = 'www.google-analytics.com'; + const GTM_HOST = 'www.googletagmanager.com'; + const ANALYTICS_JS_PATH = '/analytics.js'; + const GTAG_JS_PATH = '/gtag/js'; + const GTM_JS_PATH = '/gtm.js'; + // endpoints. Most of the time the default path (/collect) is used, but + // occasionally an experimental endpoint is used when testing new features, + // (e.g. /r/collect or /j/collect) + + const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Creates the requestWillDequeue callback to be used with the background + * sync plugin. The callback takes the failed request and adds the + * `qt` param based on the current time, as well as applies any other + * user-defined hit modifications. + * + * @param {Object} config See {@link module:workbox-google-analytics.initialize}. + * @return {Function} The requestWillDequeue callback function. + * + * @private + */ + + const createOnSyncCallback = config => { + return async ({ + queue + }) => { + let entry; + + while (entry = await queue.shiftRequest()) { + const { + request, + timestamp + } = entry; + const url = new URL(request.url); + + try { + // Measurement protocol requests can set their payload parameters in + // either the URL query string (for GET requests) or the POST body. + const params = request.method === 'POST' ? new URLSearchParams(await request.clone().text()) : url.searchParams; // Calculate the qt param, accounting for the fact that an existing + // qt param may be present and should be updated rather than replaced. + + const originalHitTime = timestamp - (Number(params.get('qt')) || 0); + const queueTime = Date.now() - originalHitTime; // Set the qt param prior to applying hitFilter or parameterOverrides. + + params.set('qt', String(queueTime)); // Apply `parameterOverrides`, if set. + + if (config.parameterOverrides) { + for (const param of Object.keys(config.parameterOverrides)) { + const value = config.parameterOverrides[param]; + params.set(param, value); + } + } // Apply `hitFilter`, if set. + + + if (typeof config.hitFilter === 'function') { + config.hitFilter.call(null, params); + } // Retry the fetch. Ignore URL search params from the URL as they're + // now in the post body. + + + await fetch(new Request(url.origin + url.pathname, { + body: params.toString(), + method: 'POST', + mode: 'cors', + credentials: 'omit', + headers: { + 'Content-Type': 'text/plain' + } + })); + + if ("dev" !== 'production') { + logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(url.href)}'` + `has been replayed`); + } + } catch (err) { + await queue.unshiftRequest(entry); + + { + logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(url.href)}'` + `failed to replay, putting it back in the queue.`); + } + + throw err; + } + } + + { + logger_js.logger.log(`All Google Analytics request successfully replayed; ` + `the queue is now empty!`); + } + }; + }; + /** + * Creates GET and POST routes to catch failed Measurement Protocol hits. + * + * @param {BackgroundSyncPlugin} bgSyncPlugin + * @return {Array} The created routes. + * + * @private + */ + + + const createCollectRoutes = bgSyncPlugin => { + const match = ({ + url + }) => url.hostname === GOOGLE_ANALYTICS_HOST && COLLECT_PATHS_REGEX.test(url.pathname); + + const handler = new NetworkOnly_js.NetworkOnly({ + plugins: [bgSyncPlugin] + }); + return [new Route_js.Route(match, handler, 'GET'), new Route_js.Route(match, handler, 'POST')]; + }; + /** + * Creates a route with a network first strategy for the analytics.js script. + * + * @param {string} cacheName + * @return {Route} The created route. + * + * @private + */ + + + const createAnalyticsJsRoute = cacheName => { + const match = ({ + url + }) => url.hostname === GOOGLE_ANALYTICS_HOST && url.pathname === ANALYTICS_JS_PATH; + + const handler = new NetworkFirst_js.NetworkFirst({ + cacheName + }); + return new Route_js.Route(match, handler, 'GET'); + }; + /** + * Creates a route with a network first strategy for the gtag.js script. + * + * @param {string} cacheName + * @return {Route} The created route. + * + * @private + */ + + + const createGtagJsRoute = cacheName => { + const match = ({ + url + }) => url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH; + + const handler = new NetworkFirst_js.NetworkFirst({ + cacheName + }); + return new Route_js.Route(match, handler, 'GET'); + }; + /** + * Creates a route with a network first strategy for the gtm.js script. + * + * @param {string} cacheName + * @return {Route} The created route. + * + * @private + */ + + + const createGtmJsRoute = cacheName => { + const match = ({ + url + }) => url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH; + + const handler = new NetworkFirst_js.NetworkFirst({ + cacheName + }); + return new Route_js.Route(match, handler, 'GET'); + }; + /** + * @param {Object=} [options] + * @param {Object} [options.cacheName] The cache name to store and retrieve + * analytics.js. Defaults to the cache names provided by `workbox-core`. + * @param {Object} [options.parameterOverrides] + * [Measurement Protocol parameters](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters), + * expressed as key/value pairs, to be added to replayed Google Analytics + * requests. This can be used to, e.g., set a custom dimension indicating + * that the request was replayed. + * @param {Function} [options.hitFilter] A function that allows you to modify + * the hit parameters prior to replaying + * the hit. The function is invoked with the original hit's URLSearchParams + * object as its only argument. + * + * @memberof module:workbox-google-analytics + */ + + + const initialize = (options = {}) => { + const cacheName = cacheNames_js.cacheNames.getGoogleAnalyticsName(options.cacheName); + const bgSyncPlugin = new BackgroundSyncPlugin_js.BackgroundSyncPlugin(QUEUE_NAME, { + maxRetentionTime: MAX_RETENTION_TIME, + onSync: createOnSyncCallback(options) + }); + const routes = [createGtmJsRoute(cacheName), createAnalyticsJsRoute(cacheName), createGtagJsRoute(cacheName), ...createCollectRoutes(bgSyncPlugin)]; + const router = new Router_js.Router(); + + for (const route of routes) { + router.registerRoute(route); + } + + router.addFetchListener(); + }; + + exports.initialize = initialize; + + return exports; + +}({}, workbox.backgroundSync, workbox.core._private, workbox.core._private, workbox.core._private, workbox.routing, workbox.routing, workbox.strategies, workbox.strategies)); + diff --git a/source/js/workbox/workbox-offline-ga.prod.js b/source/js/workbox/workbox-offline-ga.prod.js new file mode 100644 index 0000000..6f2c0a6 --- /dev/null +++ b/source/js/workbox/workbox-offline-ga.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.googleAnalytics=function(t,o,e,n,a,c,r,w,s){"use strict";try{self["workbox:google-analytics:5.1.4"]&&_()}catch(t){}const i=/^\/(\w+\/)?collect/,l=t=>{const o=({url:t})=>"www.google-analytics.com"===t.hostname&&i.test(t.pathname),e=new s.NetworkOnly({plugins:[t]});return[new c.Route(o,e,"GET"),new c.Route(o,e,"POST")]},g=t=>{const o=new w.NetworkFirst({cacheName:t});return new c.Route(({url:t})=>"www.google-analytics.com"===t.hostname&&"/analytics.js"===t.pathname,o,"GET")},m=t=>{const o=new w.NetworkFirst({cacheName:t});return new c.Route(({url:t})=>"www.googletagmanager.com"===t.hostname&&"/gtag/js"===t.pathname,o,"GET")},u=t=>{const o=new w.NetworkFirst({cacheName:t});return new c.Route(({url:t})=>"www.googletagmanager.com"===t.hostname&&"/gtm.js"===t.pathname,o,"GET")};return t.initialize=(t={})=>{const n=e.cacheNames.getGoogleAnalyticsName(t.cacheName),a=new o.BackgroundSyncPlugin("workbox-google-analytics",{maxRetentionTime:2880,onSync:(c=t,async({queue:t})=>{let o;for(;o=await t.shiftRequest();){const{request:e,timestamp:n}=o,a=new URL(e.url);try{const t="POST"===e.method?new URLSearchParams(await e.clone().text()):a.searchParams,o=n-(Number(t.get("qt"))||0),r=Date.now()-o;if(t.set("qt",String(r)),c.parameterOverrides)for(const o of Object.keys(c.parameterOverrides)){const e=c.parameterOverrides[o];t.set(o,e)}"function"==typeof c.hitFilter&&c.hitFilter.call(null,t),await fetch(new Request(a.origin+a.pathname,{body:t.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(e){throw await t.unshiftRequest(o),e}}})});var c;const w=[u(n),g(n),m(n),...l(a)],s=new r.Router;for(const t of w)s.registerRoute(t);s.addFetchListener()},t}({},workbox.backgroundSync,workbox.core._private,workbox.core._private,workbox.core._private,workbox.routing,workbox.routing,workbox.strategies,workbox.strategies); + diff --git a/source/js/workbox/workbox-precaching.dev.js b/source/js/workbox/workbox-precaching.dev.js new file mode 100644 index 0000000..42f0dec --- /dev/null +++ b/source/js/workbox/workbox-precaching.dev.js @@ -0,0 +1,1210 @@ +this.workbox = this.workbox || {}; +this.workbox.precaching = (function (exports, cacheNames_js, getFriendlyURL_js, logger_js, assert_js, cacheWrapper_js, fetchWrapper_js, WorkboxError_js, copyResponse_js) { + 'use strict'; + + try { + self['workbox:precaching:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const plugins = []; + const precachePlugins = { + /* + * @return {Array} + * @private + */ + get() { + return plugins; + }, + + /* + * @param {Array} newPlugins + * @private + */ + add(newPlugins) { + plugins.push(...newPlugins); + } + + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds plugins to precaching. + * + * @param {Array} newPlugins + * + * @memberof module:workbox-precaching + */ + + function addPlugins(newPlugins) { + precachePlugins.add(newPlugins); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + const REVISION_SEARCH_PARAM = '__WB_REVISION__'; + /** + * Converts a manifest entry into a versioned URL suitable for precaching. + * + * @param {Object|string} entry + * @return {string} A URL with versioning info. + * + * @private + * @memberof module:workbox-precaching + */ + + function createCacheKey(entry) { + if (!entry) { + throw new WorkboxError_js.WorkboxError('add-to-cache-list-unexpected-type', { + entry + }); + } // If a precache manifest entry is a string, it's assumed to be a versioned + // URL, like '/app.abcd1234.js'. Return as-is. + + + if (typeof entry === 'string') { + const urlObject = new URL(entry, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } + + const { + revision, + url + } = entry; + + if (!url) { + throw new WorkboxError_js.WorkboxError('add-to-cache-list-unexpected-type', { + entry + }); + } // If there's just a URL and no revision, then it's also assumed to be a + // versioned URL. + + + if (!revision) { + const urlObject = new URL(url, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } // Otherwise, construct a properly versioned URL using the custom Workbox + // search parameter along with the revision info. + + + const cacheKeyURL = new URL(url, location.href); + const originalURL = new URL(url, location.href); + cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision); + return { + cacheKey: cacheKeyURL.href, + url: originalURL.href + }; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} deletedURLs + * + * @private + */ + + const logGroup = (groupTitle, deletedURLs) => { + logger_js.logger.groupCollapsed(groupTitle); + + for (const url of deletedURLs) { + logger_js.logger.log(url); + } + + logger_js.logger.groupEnd(); + }; + /** + * @param {Array} deletedURLs + * + * @private + * @memberof module:workbox-precaching + */ + + + function printCleanupDetails(deletedURLs) { + const deletionCount = deletedURLs.length; + + if (deletionCount > 0) { + logger_js.logger.groupCollapsed(`During precaching cleanup, ` + `${deletionCount} cached ` + `request${deletionCount === 1 ? ' was' : 's were'} deleted.`); + logGroup('Deleted Cache Requests', deletedURLs); + logger_js.logger.groupEnd(); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} urls + * + * @private + */ + + function _nestedGroup(groupTitle, urls) { + if (urls.length === 0) { + return; + } + + logger_js.logger.groupCollapsed(groupTitle); + + for (const url of urls) { + logger_js.logger.log(url); + } + + logger_js.logger.groupEnd(); + } + /** + * @param {Array} urlsToPrecache + * @param {Array} urlsAlreadyPrecached + * + * @private + * @memberof module:workbox-precaching + */ + + + function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) { + const precachedCount = urlsToPrecache.length; + const alreadyPrecachedCount = urlsAlreadyPrecached.length; + + if (precachedCount || alreadyPrecachedCount) { + let message = `Precaching ${precachedCount} file${precachedCount === 1 ? '' : 's'}.`; + + if (alreadyPrecachedCount > 0) { + message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? ' is' : 's are'} already cached.`; + } + + logger_js.logger.groupCollapsed(message); + + _nestedGroup(`View newly precached URLs.`, urlsToPrecache); + + _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached); + + logger_js.logger.groupEnd(); + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Performs efficient precaching of assets. + * + * @memberof module:workbox-precaching + */ + + class PrecacheController { + /** + * Create a new PrecacheController. + * + * @param {string} [cacheName] An optional name for the cache, to override + * the default precache name. + */ + constructor(cacheName) { + this._cacheName = cacheNames_js.cacheNames.getPrecacheName(cacheName); + this._urlsToCacheKeys = new Map(); + this._urlsToCacheModes = new Map(); + this._cacheKeysToIntegrities = new Map(); + } + /** + * This method will add items to the precache list, removing duplicates + * and ensuring the information is valid. + * + * @param { + * Array + * } entries Array of entries to precache. + */ + + + addToCacheList(entries) { + { + assert_js.assert.isArray(entries, { + moduleName: 'workbox-precaching', + className: 'PrecacheController', + funcName: 'addToCacheList', + paramName: 'entries' + }); + } + + const urlsToWarnAbout = []; + + for (const entry of entries) { + // See https://github.com/GoogleChrome/workbox/issues/2259 + if (typeof entry === 'string') { + urlsToWarnAbout.push(entry); + } else if (entry && entry.revision === undefined) { + urlsToWarnAbout.push(entry.url); + } + + const { + cacheKey, + url + } = createCacheKey(entry); + const cacheMode = typeof entry !== 'string' && entry.revision ? 'reload' : 'default'; + + if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) { + throw new WorkboxError_js.WorkboxError('add-to-cache-list-conflicting-entries', { + firstEntry: this._urlsToCacheKeys.get(url), + secondEntry: cacheKey + }); + } + + if (typeof entry !== 'string' && entry.integrity) { + if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) { + throw new WorkboxError_js.WorkboxError('add-to-cache-list-conflicting-integrities', { + url + }); + } + + this._cacheKeysToIntegrities.set(cacheKey, entry.integrity); + } + + this._urlsToCacheKeys.set(url, cacheKey); + + this._urlsToCacheModes.set(url, cacheMode); + + if (urlsToWarnAbout.length > 0) { + const warningMessage = `Workbox is precaching URLs without revision ` + `info: ${urlsToWarnAbout.join(', ')}\nThis is generally NOT safe. ` + `Learn more at https://bit.ly/wb-precache`; + + { + logger_js.logger.warn(warningMessage); + } + } + } + } + /** + * Precaches new and updated assets. Call this method from the service worker + * install event. + * + * @param {Object} options + * @param {Event} [options.event] The install event (if needed). + * @param {Array} [options.plugins] Plugins to be used for fetching + * and caching during install. + * @return {Promise} + */ + + + async install({ + event, + plugins + } = {}) { + { + if (plugins) { + assert_js.assert.isArray(plugins, { + moduleName: 'workbox-precaching', + className: 'PrecacheController', + funcName: 'install', + paramName: 'plugins' + }); + } + } + + const toBePrecached = []; + const alreadyPrecached = []; + const cache = await self.caches.open(this._cacheName); + const alreadyCachedRequests = await cache.keys(); + const existingCacheKeys = new Set(alreadyCachedRequests.map(request => request.url)); + + for (const [url, cacheKey] of this._urlsToCacheKeys) { + if (existingCacheKeys.has(cacheKey)) { + alreadyPrecached.push(url); + } else { + toBePrecached.push({ + cacheKey, + url + }); + } + } + + const precacheRequests = toBePrecached.map(({ + cacheKey, + url + }) => { + const integrity = this._cacheKeysToIntegrities.get(cacheKey); + + const cacheMode = this._urlsToCacheModes.get(url); + + return this._addURLToCache({ + cacheKey, + cacheMode, + event, + integrity, + plugins, + url + }); + }); + await Promise.all(precacheRequests); + const updatedURLs = toBePrecached.map(item => item.url); + + { + printInstallDetails(updatedURLs, alreadyPrecached); + } + + return { + updatedURLs, + notUpdatedURLs: alreadyPrecached + }; + } + /** + * Deletes assets that are no longer present in the current precache manifest. + * Call this method from the service worker activate event. + * + * @return {Promise} + */ + + + async activate() { + const cache = await self.caches.open(this._cacheName); + const currentlyCachedRequests = await cache.keys(); + const expectedCacheKeys = new Set(this._urlsToCacheKeys.values()); + const deletedURLs = []; + + for (const request of currentlyCachedRequests) { + if (!expectedCacheKeys.has(request.url)) { + await cache.delete(request); + deletedURLs.push(request.url); + } + } + + { + printCleanupDetails(deletedURLs); + } + + return { + deletedURLs + }; + } + /** + * Requests the entry and saves it to the cache if the response is valid. + * By default, any response with a status code of less than 400 (including + * opaque responses) is considered valid. + * + * If you need to use custom criteria to determine what's valid and what + * isn't, then pass in an item in `options.plugins` that implements the + * `cacheWillUpdate()` lifecycle event. + * + * @private + * @param {Object} options + * @param {string} options.cacheKey The string to use a cache key. + * @param {string} options.url The URL to fetch and cache. + * @param {string} [options.cacheMode] The cache mode for the network request. + * @param {Event} [options.event] The install event (if passed). + * @param {Array} [options.plugins] An array of plugins to apply to + * fetch and caching. + * @param {string} [options.integrity] The value to use for the `integrity` + * field when making the request. + */ + + + async _addURLToCache({ + cacheKey, + url, + cacheMode, + event, + plugins, + integrity + }) { + const request = new Request(url, { + integrity, + cache: cacheMode, + credentials: 'same-origin' + }); + let response = await fetchWrapper_js.fetchWrapper.fetch({ + event, + plugins, + request + }); // Allow developers to override the default logic about what is and isn't + // valid by passing in a plugin implementing cacheWillUpdate(), e.g. + // a `CacheableResponsePlugin` instance. + + let cacheWillUpdatePlugin; + + for (const plugin of plugins || []) { + if ('cacheWillUpdate' in plugin) { + cacheWillUpdatePlugin = plugin; + } + } + + const isValidResponse = cacheWillUpdatePlugin ? // Use a callback if provided. It returns a truthy value if valid. + // NOTE: invoke the method on the plugin instance so the `this` context + // is correct. + await cacheWillUpdatePlugin.cacheWillUpdate({ + event, + request, + response + }) : // Otherwise, default to considering any response status under 400 valid. + // This includes, by default, considering opaque responses valid. + response.status < 400; // Consider this a failure, leading to the `install` handler failing, if + // we get back an invalid response. + + if (!isValidResponse) { + throw new WorkboxError_js.WorkboxError('bad-precaching-response', { + url, + status: response.status + }); + } // Redirected responses cannot be used to satisfy a navigation request, so + // any redirected response must be "copied" rather than cloned, so the new + // response doesn't contain the `redirected` flag. See: + // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1 + + + if (response.redirected) { + response = await copyResponse_js.copyResponse(response); + } + + await cacheWrapper_js.cacheWrapper.put({ + event, + plugins, + response, + // `request` already uses `url`. We may be able to reuse it. + request: cacheKey === url ? request : new Request(cacheKey), + cacheName: this._cacheName, + matchOptions: { + ignoreSearch: true + } + }); + } + /** + * Returns a mapping of a precached URL to the corresponding cache key, taking + * into account the revision information for the URL. + * + * @return {Map} A URL to cache key mapping. + */ + + + getURLsToCacheKeys() { + return this._urlsToCacheKeys; + } + /** + * Returns a list of all the URLs that have been precached by the current + * service worker. + * + * @return {Array} The precached URLs. + */ + + + getCachedURLs() { + return [...this._urlsToCacheKeys.keys()]; + } + /** + * Returns the cache key used for storing a given URL. If that URL is + * unversioned, like `/index.html', then the cache key will be the original + * URL with a search parameter appended to it. + * + * @param {string} url A URL whose cache key you want to look up. + * @return {string} The versioned URL that corresponds to a cache key + * for the original URL, or undefined if that URL isn't precached. + */ + + + getCacheKeyForURL(url) { + const urlObject = new URL(url, location.href); + return this._urlsToCacheKeys.get(urlObject.href); + } + /** + * This acts as a drop-in replacement for [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match) + * with the following differences: + * + * - It knows what the name of the precache is, and only checks in that cache. + * - It allows you to pass in an "original" URL without versioning parameters, + * and it will automatically look up the correct cache key for the currently + * active revision of that URL. + * + * E.g., `matchPrecache('index.html')` will find the correct precached + * response for the currently active service worker, even if the actual cache + * key is `'/index.html?__WB_REVISION__=1234abcd'`. + * + * @param {string|Request} request The key (without revisioning parameters) + * to look up in the precache. + * @return {Promise} + */ + + + async matchPrecache(request) { + const url = request instanceof Request ? request.url : request; + const cacheKey = this.getCacheKeyForURL(url); + + if (cacheKey) { + const cache = await self.caches.open(this._cacheName); + return cache.match(cacheKey); + } + + return undefined; + } + /** + * Returns a function that can be used within a + * {@link module:workbox-routing.Route} that will find a response for the + * incoming request against the precache. + * + * If for an unexpected reason there is a cache miss for the request, + * this will fall back to retrieving the `Response` via `fetch()` when + * `fallbackToNetwork` is `true`. + * + * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the + * response from the network if there's a precache miss. + * @return {module:workbox-routing~handlerCallback} + */ + + + createHandler(fallbackToNetwork = true) { + return async ({ + request + }) => { + try { + const response = await this.matchPrecache(request); + + if (response) { + return response; + } // This shouldn't normally happen, but there are edge cases: + // https://github.com/GoogleChrome/workbox/issues/1441 + + + throw new WorkboxError_js.WorkboxError('missing-precache-entry', { + cacheName: this._cacheName, + url: request instanceof Request ? request.url : request + }); + } catch (error) { + if (fallbackToNetwork) { + { + logger_js.logger.debug(`Unable to respond with precached response. ` + `Falling back to network.`, error); + } + + return fetch(request); + } + + throw error; + } + }; + } + /** + * Returns a function that looks up `url` in the precache (taking into + * account revision information), and returns the corresponding `Response`. + * + * If for an unexpected reason there is a cache miss when looking up `url`, + * this will fall back to retrieving the `Response` via `fetch()` when + * `fallbackToNetwork` is `true`. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the + * response from the network if there's a precache miss. + * @return {module:workbox-routing~handlerCallback} + */ + + + createHandlerBoundToURL(url, fallbackToNetwork = true) { + const cacheKey = this.getCacheKeyForURL(url); + + if (!cacheKey) { + throw new WorkboxError_js.WorkboxError('non-precached-url', { + url + }); + } + + const handler = this.createHandler(fallbackToNetwork); + const request = new Request(url); + return () => handler({ + request + }); + } + + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let precacheController; + /** + * @return {PrecacheController} + * @private + */ + + const getOrCreatePrecacheController = () => { + if (!precacheController) { + precacheController = new PrecacheController(); + } + + return precacheController; + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Removes any URL search parameters that should be ignored. + * + * @param {URL} urlObject The original URL. + * @param {Array} ignoreURLParametersMatching RegExps to test against + * each search parameter name. Matches mean that the search parameter should be + * ignored. + * @return {URL} The URL with any ignored search parameters removed. + * + * @private + * @memberof module:workbox-precaching + */ + + function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) { + // Convert the iterable into an array at the start of the loop to make sure + // deletion doesn't mess up iteration. + for (const paramName of [...urlObject.searchParams.keys()]) { + if (ignoreURLParametersMatching.some(regExp => regExp.test(paramName))) { + urlObject.searchParams.delete(paramName); + } + } + + return urlObject; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Generator function that yields possible variations on the original URL to + * check, one at a time. + * + * @param {string} url + * @param {Object} options + * + * @private + * @memberof module:workbox-precaching + */ + + function* generateURLVariations(url, { + ignoreURLParametersMatching, + directoryIndex, + cleanURLs, + urlManipulation + } = {}) { + const urlObject = new URL(url, location.href); + urlObject.hash = ''; + yield urlObject.href; + const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching); + yield urlWithoutIgnoredParams.href; + + if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith('/')) { + const directoryURL = new URL(urlWithoutIgnoredParams.href); + directoryURL.pathname += directoryIndex; + yield directoryURL.href; + } + + if (cleanURLs) { + const cleanURL = new URL(urlWithoutIgnoredParams.href); + cleanURL.pathname += '.html'; + yield cleanURL.href; + } + + if (urlManipulation) { + const additionalURLs = urlManipulation({ + url: urlObject + }); + + for (const urlToAttempt of additionalURLs) { + yield urlToAttempt.href; + } + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This function will take the request URL and manipulate it based on the + * configuration options. + * + * @param {string} url + * @param {Object} options + * @return {string} Returns the URL in the cache that matches the request, + * if possible. + * + * @private + */ + + const getCacheKeyForURL = (url, options) => { + const precacheController = getOrCreatePrecacheController(); + const urlsToCacheKeys = precacheController.getURLsToCacheKeys(); + + for (const possibleURL of generateURLVariations(url, options)) { + const possibleCacheKey = urlsToCacheKeys.get(possibleURL); + + if (possibleCacheKey) { + return possibleCacheKey; + } + } + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds a `fetch` listener to the service worker that will + * respond to + * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests} + * with precached assets. + * + * Requests for assets that aren't precached, the `FetchEvent` will not be + * responded to, allowing the event to fall through to other `fetch` event + * listeners. + * + * NOTE: when called more than once this method will replace the previously set + * configuration options. Calling it more than once is not recommended outside + * of tests. + * + * @private + * @param {Object} [options] + * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will + * check cache entries for a URLs ending with '/' to see if there is a hit when + * appending the `directoryIndex` value. + * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/]] An + * array of regex's to remove search params when looking for a cache match. + * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will + * check the cache for the URL with a `.html` added to the end of the end. + * @param {workbox.precaching~urlManipulation} [options.urlManipulation] + * This is a function that should take a URL and return an array of + * alternative URLs that should be checked for precache matches. + */ + + const addFetchListener = ({ + ignoreURLParametersMatching = [/^utm_/], + directoryIndex = 'index.html', + cleanURLs = true, + urlManipulation + } = {}) => { + const cacheName = cacheNames_js.cacheNames.getPrecacheName(); // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + + self.addEventListener('fetch', event => { + const precachedURL = getCacheKeyForURL(event.request.url, { + cleanURLs, + directoryIndex, + ignoreURLParametersMatching, + urlManipulation + }); + + if (!precachedURL) { + { + logger_js.logger.debug(`Precaching did not find a match for ` + getFriendlyURL_js.getFriendlyURL(event.request.url)); + } + + return; + } + + let responsePromise = self.caches.open(cacheName).then(cache => { + return cache.match(precachedURL); + }).then(cachedResponse => { + if (cachedResponse) { + return cachedResponse; + } // Fall back to the network if we don't have a cached response + // (perhaps due to manual cache cleanup). + + + { + logger_js.logger.warn(`The precached response for ` + `${getFriendlyURL_js.getFriendlyURL(precachedURL)} in ${cacheName} was not found. ` + `Falling back to the network instead.`); + } + + return fetch(precachedURL); + }); + + { + responsePromise = responsePromise.then(response => { + // Workbox is going to handle the route. + // print the routing details to the console. + logger_js.logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL_js.getFriendlyURL(event.request.url)); + logger_js.logger.log(`Serving the precached url: ${precachedURL}`); + logger_js.logger.groupCollapsed(`View request details here.`); + logger_js.logger.log(event.request); + logger_js.logger.groupEnd(); + logger_js.logger.groupCollapsed(`View response details here.`); + logger_js.logger.log(response); + logger_js.logger.groupEnd(); + logger_js.logger.groupEnd(); + return response; + }); + } + + event.respondWith(responsePromise); + }); + }; + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let listenerAdded = false; + /** + * Add a `fetch` listener to the service worker that will + * respond to + * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests} + * with precached assets. + * + * Requests for assets that aren't precached, the `FetchEvent` will not be + * responded to, allowing the event to fall through to other `fetch` event + * listeners. + * + * @param {Object} [options] + * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will + * check cache entries for a URLs ending with '/' to see if there is a hit when + * appending the `directoryIndex` value. + * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/]] An + * array of regex's to remove search params when looking for a cache match. + * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will + * check the cache for the URL with a `.html` added to the end of the end. + * @param {module:workbox-precaching~urlManipulation} [options.urlManipulation] + * This is a function that should take a URL and return an array of + * alternative URLs that should be checked for precache matches. + * + * @memberof module:workbox-precaching + */ + + function addRoute(options) { + if (!listenerAdded) { + addFetchListener(options); + listenerAdded = true; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const SUBSTRING_TO_FIND = '-precache-'; + /** + * Cleans up incompatible precaches that were created by older versions of + * Workbox, by a service worker registered under the current scope. + * + * This is meant to be called as part of the `activate` event. + * + * This should be safe to use as long as you don't include `substringToFind` + * (defaulting to `-precache-`) in your non-precache cache names. + * + * @param {string} currentPrecacheName The cache name currently in use for + * precaching. This cache won't be deleted. + * @param {string} [substringToFind='-precache-'] Cache names which include this + * substring will be deleted (excluding `currentPrecacheName`). + * @return {Array} A list of all the cache names that were deleted. + * + * @private + * @memberof module:workbox-precaching + */ + + const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => { + const cacheNames = await self.caches.keys(); + const cacheNamesToDelete = cacheNames.filter(cacheName => { + return cacheName.includes(substringToFind) && cacheName.includes(self.registration.scope) && cacheName !== currentPrecacheName; + }); + await Promise.all(cacheNamesToDelete.map(cacheName => self.caches.delete(cacheName))); + return cacheNamesToDelete; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds an `activate` event listener which will clean up incompatible + * precaches that were created by older versions of Workbox. + * + * @memberof module:workbox-precaching + */ + + function cleanupOutdatedCaches() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('activate', event => { + const cacheName = cacheNames_js.cacheNames.getPrecacheName(); + event.waitUntil(deleteOutdatedCaches(cacheName).then(cachesDeleted => { + { + if (cachesDeleted.length > 0) { + logger_js.logger.log(`The following out-of-date precaches were cleaned up ` + `automatically:`, cachesDeleted); + } + } + })); + }); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Helper function that calls + * {@link PrecacheController#createHandler} on the default + * {@link PrecacheController} instance. + * + * If you are creating your own {@link PrecacheController}, then call the + * {@link PrecacheController#createHandler} on that instance, + * instead of using this function. + * + * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the + * response from the network if there's a precache miss. + * @return {module:workbox-routing~handlerCallback} + * + * @memberof module:workbox-precaching + */ + + function createHandler(fallbackToNetwork = true) { + const precacheController = getOrCreatePrecacheController(); + return precacheController.createHandler(fallbackToNetwork); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Helper function that calls + * {@link PrecacheController#createHandlerBoundToURL} on the default + * {@link PrecacheController} instance. + * + * If you are creating your own {@link PrecacheController}, then call the + * {@link PrecacheController#createHandlerBoundToURL} on that instance, + * instead of using this function. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the + * response from the network if there's a precache miss. + * @return {module:workbox-routing~handlerCallback} + * + * @memberof module:workbox-precaching + */ + + function createHandlerBoundToURL(url) { + const precacheController = getOrCreatePrecacheController(); + return precacheController.createHandlerBoundToURL(url); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Takes in a URL, and returns the corresponding URL that could be used to + * lookup the entry in the precache. + * + * If a relative URL is provided, the location of the service worker file will + * be used as the base. + * + * For precached entries without revision information, the cache key will be the + * same as the original URL. + * + * For precached entries with revision information, the cache key will be the + * original URL with the addition of a query parameter used for keeping track of + * the revision info. + * + * @param {string} url The URL whose cache key to look up. + * @return {string} The cache key that corresponds to that URL. + * + * @memberof module:workbox-precaching + */ + + function getCacheKeyForURL$1(url) { + const precacheController = getOrCreatePrecacheController(); + return precacheController.getCacheKeyForURL(url); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Helper function that calls + * {@link PrecacheController#matchPrecache} on the default + * {@link PrecacheController} instance. + * + * If you are creating your own {@link PrecacheController}, then call + * {@link PrecacheController#matchPrecache} on that instance, + * instead of using this function. + * + * @param {string|Request} request The key (without revisioning parameters) + * to look up in the precache. + * @return {Promise} + * + * @memberof module:workbox-precaching + */ + + function matchPrecache(request) { + const precacheController = getOrCreatePrecacheController(); + return precacheController.matchPrecache(request); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + const installListener = event => { + const precacheController = getOrCreatePrecacheController(); + const plugins = precachePlugins.get(); + event.waitUntil(precacheController.install({ + event, + plugins + }).catch(error => { + { + logger_js.logger.error(`Service worker installation failed. It will ` + `be retried automatically during the next navigation.`); + } // Re-throw the error to ensure installation fails. + + + throw error; + })); + }; + + const activateListener = event => { + const precacheController = getOrCreatePrecacheController(); + event.waitUntil(precacheController.activate()); + }; + /** + * Adds items to the precache list, removing any duplicates and + * stores the files in the + * ["precache cache"]{@link module:workbox-core.cacheNames} when the service + * worker installs. + * + * This method can be called multiple times. + * + * Please note: This method **will not** serve any of the cached files for you. + * It only precaches files. To respond to a network request you call + * [addRoute()]{@link module:workbox-precaching.addRoute}. + * + * If you have a single array of files to precache, you can just call + * [precacheAndRoute()]{@link module:workbox-precaching.precacheAndRoute}. + * + * @param {Array} [entries=[]] Array of entries to precache. + * + * @memberof module:workbox-precaching + */ + + + function precache(entries) { + const precacheController = getOrCreatePrecacheController(); + precacheController.addToCacheList(entries); + + if (entries.length > 0) { + // NOTE: these listeners will only be added once (even if the `precache()` + // method is called multiple times) because event listeners are implemented + // as a set, where each listener must be unique. + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('install', installListener); + self.addEventListener('activate', activateListener); + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This method will add entries to the precache list and add a route to + * respond to fetch events. + * + * This is a convenience method that will call + * [precache()]{@link module:workbox-precaching.precache} and + * [addRoute()]{@link module:workbox-precaching.addRoute} in a single call. + * + * @param {Array} entries Array of entries to precache. + * @param {Object} [options] See + * [addRoute() options]{@link module:workbox-precaching.addRoute}. + * + * @memberof module:workbox-precaching + */ + + function precacheAndRoute(entries, options) { + precache(entries); + addRoute(options); + } + + exports.PrecacheController = PrecacheController; + exports.addPlugins = addPlugins; + exports.addRoute = addRoute; + exports.cleanupOutdatedCaches = cleanupOutdatedCaches; + exports.createHandler = createHandler; + exports.createHandlerBoundToURL = createHandlerBoundToURL; + exports.getCacheKeyForURL = getCacheKeyForURL$1; + exports.matchPrecache = matchPrecache; + exports.precache = precache; + exports.precacheAndRoute = precacheAndRoute; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core)); + diff --git a/source/js/workbox/workbox-precaching.prod.js b/source/js/workbox/workbox-precaching.prod.js new file mode 100644 index 0000000..c095eb3 --- /dev/null +++ b/source/js/workbox/workbox-precaching.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.precaching=function(t,e,n,i,s,c){"use strict";try{self["workbox:precaching:5.1.4"]&&_()}catch(t){}const r=[],o={get:()=>r,add(t){r.push(...t)}};function a(t){if(!t)throw new s.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),c=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:c.href}}class h{constructor(t){this.t=e.cacheNames.getPrecacheName(t),this.i=new Map,this.s=new Map,this.o=new Map}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=a(n),c="string"!=typeof n&&n.revision?"reload":"default";if(this.i.has(i)&&this.i.get(i)!==t)throw new s.WorkboxError("add-to-cache-list-conflicting-entries",{firstEntry:this.i.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.o.has(t)&&this.o.get(t)!==n.integrity)throw new s.WorkboxError("add-to-cache-list-conflicting-integrities",{url:i});this.o.set(t,n.integrity)}if(this.i.set(i,t),this.s.set(i,c),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}async install({event:t,plugins:e}={}){const n=[],i=[],s=await self.caches.open(this.t),c=await s.keys(),r=new Set(c.map(t=>t.url));for(const[t,e]of this.i)r.has(e)?i.push(t):n.push({cacheKey:e,url:t});const o=n.map(({cacheKey:n,url:i})=>{const s=this.o.get(n),c=this.s.get(i);return this.h({cacheKey:n,cacheMode:c,event:t,integrity:s,plugins:e,url:i})});await Promise.all(o);return{updatedURLs:n.map(t=>t.url),notUpdatedURLs:i}}async activate(){const t=await self.caches.open(this.t),e=await t.keys(),n=new Set(this.i.values()),i=[];for(const s of e)n.has(s.url)||(await t.delete(s),i.push(s.url));return{deletedURLs:i}}async h({cacheKey:t,url:e,cacheMode:r,event:o,plugins:a,integrity:h}){const u=new Request(e,{integrity:h,cache:r,credentials:"same-origin"});let l,f=await i.fetchWrapper.fetch({event:o,plugins:a,request:u});for(const t of a||[])"cacheWillUpdate"in t&&(l=t);if(!(l?await l.cacheWillUpdate({event:o,request:u,response:f}):f.status<400))throw new s.WorkboxError("bad-precaching-response",{url:e,status:f.status});f.redirected&&(f=await c.copyResponse(f)),await n.cacheWrapper.put({event:o,plugins:a,response:f,request:t===e?u:new Request(t),cacheName:this.t,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.i}getCachedURLs(){return[...this.i.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.i.get(e.href)}async matchPrecache(t){const e=t instanceof Request?t.url:t,n=this.getCacheKeyForURL(e);if(n){return(await self.caches.open(this.t)).match(n)}}createHandler(t=!0){return async({request:e})=>{try{const t=await this.matchPrecache(e);if(t)return t;throw new s.WorkboxError("missing-precache-entry",{cacheName:this.t,url:e instanceof Request?e.url:e})}catch(n){if(t)return fetch(e);throw n}}}createHandlerBoundToURL(t,e=!0){if(!this.getCacheKeyForURL(t))throw new s.WorkboxError("non-precached-url",{url:t});const n=this.createHandler(e),i=new Request(t);return()=>n({request:i})}}let u;const l=()=>(u||(u=new h),u);const f=(t,e)=>{const n=l().getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e,directoryIndex:n,cleanURLs:i,urlManipulation:s}={}){const c=new URL(t,location.href);c.hash="",yield c.href;const r=function(t,e=[]){for(const n of[...t.searchParams.keys()])e.some(t=>t.test(n))&&t.searchParams.delete(n);return t}(c,e);if(yield r.href,n&&r.pathname.endsWith("/")){const t=new URL(r.href);t.pathname+=n,yield t.href}if(i){const t=new URL(r.href);t.pathname+=".html",yield t.href}if(s){const t=s({url:c});for(const e of t)yield e.href}}(t,e)){const t=n.get(i);if(t)return t}};let w=!1;function d(t){w||((({ignoreURLParametersMatching:t=[/^utm_/],directoryIndex:n="index.html",cleanURLs:i=!0,urlManipulation:s}={})=>{const c=e.cacheNames.getPrecacheName();self.addEventListener("fetch",e=>{const r=f(e.request.url,{cleanURLs:i,directoryIndex:n,ignoreURLParametersMatching:t,urlManipulation:s});if(!r)return;let o=self.caches.open(c).then(t=>t.match(r)).then(t=>t||fetch(r));e.respondWith(o)})})(t),w=!0)}const y=t=>{const e=l(),n=o.get();t.waitUntil(e.install({event:t,plugins:n}).catch(t=>{throw t}))},p=t=>{const e=l();t.waitUntil(e.activate())};function g(t){l().addToCacheList(t),t.length>0&&(self.addEventListener("install",y),self.addEventListener("activate",p))}return t.PrecacheController=h,t.addPlugins=function(t){o.add(t)},t.addRoute=d,t.cleanupOutdatedCaches=function(){self.addEventListener("activate",t=>{const n=e.cacheNames.getPrecacheName();t.waitUntil((async(t,e="-precache-")=>{const n=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==t);return await Promise.all(n.map(t=>self.caches.delete(t))),n})(n).then(t=>{}))})},t.createHandler=function(t=!0){return l().createHandler(t)},t.createHandlerBoundToURL=function(t){return l().createHandlerBoundToURL(t)},t.getCacheKeyForURL=function(t){return l().getCacheKeyForURL(t)},t.matchPrecache=function(t){return l().matchPrecache(t)},t.precache=g,t.precacheAndRoute=function(t,e){g(t),d(e)},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core); + diff --git a/source/js/workbox/workbox-range-requests.dev.js b/source/js/workbox/workbox-range-requests.dev.js new file mode 100644 index 0000000..2375cdd --- /dev/null +++ b/source/js/workbox/workbox-range-requests.dev.js @@ -0,0 +1,262 @@ +this.workbox = this.workbox || {}; +this.workbox.rangeRequests = (function (exports, WorkboxError_js, assert_js, logger_js) { + 'use strict'; + + try { + self['workbox:range-requests:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {Blob} blob A source blob. + * @param {number} [start] The offset to use as the start of the + * slice. + * @param {number} [end] The offset to use as the end of the slice. + * @return {Object} An object with `start` and `end` properties, reflecting + * the effective boundaries to use given the size of the blob. + * + * @private + */ + + function calculateEffectiveBoundaries(blob, start, end) { + { + assert_js.assert.isInstance(blob, Blob, { + moduleName: 'workbox-range-requests', + funcName: 'calculateEffectiveBoundaries', + paramName: 'blob' + }); + } + + const blobSize = blob.size; + + if (end && end > blobSize || start && start < 0) { + throw new WorkboxError_js.WorkboxError('range-not-satisfiable', { + size: blobSize, + end, + start + }); + } + + let effectiveStart; + let effectiveEnd; + + if (start !== undefined && end !== undefined) { + effectiveStart = start; // Range values are inclusive, so add 1 to the value. + + effectiveEnd = end + 1; + } else if (start !== undefined && end === undefined) { + effectiveStart = start; + effectiveEnd = blobSize; + } else if (end !== undefined && start === undefined) { + effectiveStart = blobSize - end; + effectiveEnd = blobSize; + } + + return { + start: effectiveStart, + end: effectiveEnd + }; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} rangeHeader A Range: header value. + * @return {Object} An object with `start` and `end` properties, reflecting + * the parsed value of the Range: header. If either the `start` or `end` are + * omitted, then `null` will be returned. + * + * @private + */ + + function parseRangeHeader(rangeHeader) { + { + assert_js.assert.isType(rangeHeader, 'string', { + moduleName: 'workbox-range-requests', + funcName: 'parseRangeHeader', + paramName: 'rangeHeader' + }); + } + + const normalizedRangeHeader = rangeHeader.trim().toLowerCase(); + + if (!normalizedRangeHeader.startsWith('bytes=')) { + throw new WorkboxError_js.WorkboxError('unit-must-be-bytes', { + normalizedRangeHeader + }); + } // Specifying multiple ranges separate by commas is valid syntax, but this + // library only attempts to handle a single, contiguous sequence of bytes. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#Syntax + + + if (normalizedRangeHeader.includes(',')) { + throw new WorkboxError_js.WorkboxError('single-range-only', { + normalizedRangeHeader + }); + } + + const rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader); // We need either at least one of the start or end values. + + if (!rangeParts || !(rangeParts[1] || rangeParts[2])) { + throw new WorkboxError_js.WorkboxError('invalid-range-values', { + normalizedRangeHeader + }); + } + + return { + start: rangeParts[1] === '' ? undefined : Number(rangeParts[1]), + end: rangeParts[2] === '' ? undefined : Number(rangeParts[2]) + }; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Given a `Request` and `Response` objects as input, this will return a + * promise for a new `Response`. + * + * If the original `Response` already contains partial content (i.e. it has + * a status of 206), then this assumes it already fulfills the `Range:` + * requirements, and will return it as-is. + * + * @param {Request} request A request, which should contain a Range: + * header. + * @param {Response} originalResponse A response. + * @return {Promise} Either a `206 Partial Content` response, with + * the response body set to the slice of content specified by the request's + * `Range:` header, or a `416 Range Not Satisfiable` response if the + * conditions of the `Range:` header can't be met. + * + * @memberof module:workbox-range-requests + */ + + async function createPartialResponse(request, originalResponse) { + try { + if ("dev" !== 'production') { + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-range-requests', + funcName: 'createPartialResponse', + paramName: 'request' + }); + assert_js.assert.isInstance(originalResponse, Response, { + moduleName: 'workbox-range-requests', + funcName: 'createPartialResponse', + paramName: 'originalResponse' + }); + } + + if (originalResponse.status === 206) { + // If we already have a 206, then just pass it through as-is; + // see https://github.com/GoogleChrome/workbox/issues/1720 + return originalResponse; + } + + const rangeHeader = request.headers.get('range'); + + if (!rangeHeader) { + throw new WorkboxError_js.WorkboxError('no-range-header'); + } + + const boundaries = parseRangeHeader(rangeHeader); + const originalBlob = await originalResponse.blob(); + const effectiveBoundaries = calculateEffectiveBoundaries(originalBlob, boundaries.start, boundaries.end); + const slicedBlob = originalBlob.slice(effectiveBoundaries.start, effectiveBoundaries.end); + const slicedBlobSize = slicedBlob.size; + const slicedResponse = new Response(slicedBlob, { + // Status code 206 is for a Partial Content response. + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 + status: 206, + statusText: 'Partial Content', + headers: originalResponse.headers + }); + slicedResponse.headers.set('Content-Length', String(slicedBlobSize)); + slicedResponse.headers.set('Content-Range', `bytes ${effectiveBoundaries.start}-${effectiveBoundaries.end - 1}/` + originalBlob.size); + return slicedResponse; + } catch (error) { + { + logger_js.logger.warn(`Unable to construct a partial response; returning a ` + `416 Range Not Satisfiable response instead.`); + logger_js.logger.groupCollapsed(`View details here.`); + logger_js.logger.log(error); + logger_js.logger.log(request); + logger_js.logger.log(originalResponse); + logger_js.logger.groupEnd(); + } + + return new Response('', { + status: 416, + statusText: 'Range Not Satisfiable' + }); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The range request plugin makes it easy for a request with a 'Range' header to + * be fulfilled by a cached response. + * + * It does this by intercepting the `cachedResponseWillBeUsed` plugin callback + * and returning the appropriate subset of the cached response body. + * + * @memberof module:workbox-range-requests + */ + + class RangeRequestsPlugin { + constructor() { + /** + * @param {Object} options + * @param {Request} options.request The original request, which may or may not + * contain a Range: header. + * @param {Response} options.cachedResponse The complete cached response. + * @return {Promise} If request contains a 'Range' header, then a + * new response with status 206 whose body is a subset of `cachedResponse` is + * returned. Otherwise, `cachedResponse` is returned as-is. + * + * @private + */ + this.cachedResponseWillBeUsed = async ({ + request, + cachedResponse + }) => { + // Only return a sliced response if there's something valid in the cache, + // and there's a Range: header in the request. + if (cachedResponse && request.headers.has('range')) { + return await createPartialResponse(request, cachedResponse); + } // If there was no Range: header, or if cachedResponse wasn't valid, just + // pass it through as-is. + + + return cachedResponse; + }; + } + + } + + exports.RangeRequestsPlugin = RangeRequestsPlugin; + exports.createPartialResponse = createPartialResponse; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private)); + diff --git a/source/js/workbox/workbox-range-requests.prod.js b/source/js/workbox/workbox-range-requests.prod.js new file mode 100644 index 0000000..0d24dd8 --- /dev/null +++ b/source/js/workbox/workbox-range-requests.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.rangeRequests=function(t,e,n){"use strict";try{self["workbox:range-requests:5.1.4"]&&_()}catch(t){}async function r(t,n){try{if(206===n.status)return n;const r=t.headers.get("range");if(!r)throw new e.WorkboxError("no-range-header");const s=function(t){const n=t.trim().toLowerCase();if(!n.startsWith("bytes="))throw new e.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:n});if(n.includes(","))throw new e.WorkboxError("single-range-only",{normalizedRangeHeader:n});const r=/(\d*)-(\d*)/.exec(n);if(!r||!r[1]&&!r[2])throw new e.WorkboxError("invalid-range-values",{normalizedRangeHeader:n});return{start:""===r[1]?void 0:Number(r[1]),end:""===r[2]?void 0:Number(r[2])}}(r),a=await n.blob(),o=function(t,n,r){const s=t.size;if(r&&r>s||n&&n<0)throw new e.WorkboxError("range-not-satisfiable",{size:s,end:r,start:n});let a,o;return void 0!==n&&void 0!==r?(a=n,o=r+1):void 0!==n&&void 0===r?(a=n,o=s):void 0!==r&&void 0===n&&(a=s-r,o=s),{start:a,end:o}}(a,s.start,s.end),i=a.slice(o.start,o.end),d=i.size,u=new Response(i,{status:206,statusText:"Partial Content",headers:n.headers});return u.headers.set("Content-Length",String(d)),u.headers.set("Content-Range",`bytes ${o.start}-${o.end-1}/`+a.size),u}catch(t){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return t.RangeRequestsPlugin=class{constructor(){this.cachedResponseWillBeUsed=async({request:t,cachedResponse:e})=>e&&t.headers.has("range")?await r(t,e):e}},t.createPartialResponse=r,t}({},workbox.core._private,workbox.core._private); + diff --git a/source/js/workbox/workbox-routing.dev.js b/source/js/workbox/workbox-routing.dev.js new file mode 100644 index 0000000..1c862cf --- /dev/null +++ b/source/js/workbox/workbox-routing.dev.js @@ -0,0 +1,923 @@ +this.workbox = this.workbox || {}; +this.workbox.routing = (function (exports, assert_js, logger_js, WorkboxError_js, getFriendlyURL_js) { + 'use strict'; + + try { + self['workbox:routing:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The default HTTP method, 'GET', used when there's no specific method + * configured for a route. + * + * @type {string} + * + * @private + */ + + const defaultMethod = 'GET'; + /** + * The list of valid HTTP methods associated with requests that could be routed. + * + * @type {Array} + * + * @private + */ + + const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {function()|Object} handler Either a function, or an object with a + * 'handle' method. + * @return {Object} An object with a handle method. + * + * @private + */ + + const normalizeHandler = handler => { + if (handler && typeof handler === 'object') { + { + assert_js.assert.hasMethod(handler, 'handle', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'handler' + }); + } + + return handler; + } else { + { + assert_js.assert.isType(handler, 'function', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'handler' + }); + } + + return { + handle: handler + }; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A `Route` consists of a pair of callback functions, "match" and "handler". + * The "match" callback determine if a route should be used to "handle" a + * request by returning a non-falsy value if it can. The "handler" callback + * is called when there is a match and should return a Promise that resolves + * to a `Response`. + * + * @memberof module:workbox-routing + */ + + class Route { + /** + * Constructor for Route class. + * + * @param {module:workbox-routing~matchCallback} match + * A callback function that determines whether the route matches a given + * `fetch` event by returning a non-falsy value. + * @param {module:workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resolving to a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(match, handler, method = defaultMethod) { + { + assert_js.assert.isType(match, 'function', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'match' + }); + + if (method) { + assert_js.assert.isOneOf(method, validMethods, { + paramName: 'method' + }); + } + } // These values are referenced directly by Router so cannot be + // altered by minificaton. + + + this.handler = normalizeHandler(handler); + this.match = match; + this.method = method; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * NavigationRoute makes it easy to create a + * [Route]{@link module:workbox-routing.Route} that matches for browser + * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}. + * + * It will only match incoming Requests whose + * [`mode`]{@link https://fetch.spec.whatwg.org/#concept-request-mode} + * is set to `navigate`. + * + * You can optionally only apply this route to a subset of navigation requests + * by using one or both of the `denylist` and `allowlist` parameters. + * + * @memberof module:workbox-routing + * @extends module:workbox-routing.Route + */ + + class NavigationRoute extends Route { + /** + * If both `denylist` and `allowlist` are provided, the `denylist` will + * take precedence and the request will not match this route. + * + * The regular expressions in `allowlist` and `denylist` + * are matched against the concatenated + * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname} + * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search} + * portions of the requested URL. + * + * @param {module:workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {Object} options + * @param {Array} [options.denylist] If any of these patterns match, + * the route will not handle the request (even if a allowlist RegExp matches). + * @param {Array} [options.allowlist=[/./]] If any of these patterns + * match the URL's pathname and search parameter, the route will handle the + * request (assuming the denylist doesn't match). + */ + constructor(handler, { + allowlist = [/./], + denylist = [] + } = {}) { + { + assert_js.assert.isArrayOfClass(allowlist, RegExp, { + moduleName: 'workbox-routing', + className: 'NavigationRoute', + funcName: 'constructor', + paramName: 'options.allowlist' + }); + assert_js.assert.isArrayOfClass(denylist, RegExp, { + moduleName: 'workbox-routing', + className: 'NavigationRoute', + funcName: 'constructor', + paramName: 'options.denylist' + }); + } + + super(options => this._match(options), handler); + this._allowlist = allowlist; + this._denylist = denylist; + } + /** + * Routes match handler. + * + * @param {Object} options + * @param {URL} options.url + * @param {Request} options.request + * @return {boolean} + * + * @private + */ + + + _match({ + url, + request + }) { + if (request && request.mode !== 'navigate') { + return false; + } + + const pathnameAndSearch = url.pathname + url.search; + + for (const regExp of this._denylist) { + if (regExp.test(pathnameAndSearch)) { + { + logger_js.logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL matches this denylist pattern: ` + `${regExp}`); + } + + return false; + } + } + + if (this._allowlist.some(regExp => regExp.test(pathnameAndSearch))) { + { + logger_js.logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`); + } + + return true; + } + + { + logger_js.logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL being navigated to doesn't ` + `match the allowlist.`); + } + + return false; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * RegExpRoute makes it easy to create a regular expression based + * [Route]{@link module:workbox-routing.Route}. + * + * For same-origin requests the RegExp only needs to match part of the URL. For + * requests against third-party servers, you must define a RegExp that matches + * the start of the URL. + * + * [See the module docs for info.]{@link https://developers.google.com/web/tools/workbox/modules/workbox-routing} + * + * @memberof module:workbox-routing + * @extends module:workbox-routing.Route + */ + + class RegExpRoute extends Route { + /** + * If the regular expression contains + * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, + * the captured values will be passed to the + * [handler's]{@link module:workbox-routing~handlerCallback} `params` + * argument. + * + * @param {RegExp} regExp The regular expression to match against URLs. + * @param {module:workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(regExp, handler, method) { + { + assert_js.assert.isInstance(regExp, RegExp, { + moduleName: 'workbox-routing', + className: 'RegExpRoute', + funcName: 'constructor', + paramName: 'pattern' + }); + } + + const match = ({ + url + }) => { + const result = regExp.exec(url.href); // Return immediately if there's no match. + + if (!result) { + return; + } // Require that the match start at the first character in the URL string + // if it's a cross-origin request. + // See https://github.com/GoogleChrome/workbox/issues/281 for the context + // behind this behavior. + + + if (url.origin !== location.origin && result.index !== 0) { + { + logger_js.logger.debug(`The regular expression '${regExp}' only partially matched ` + `against the cross-origin URL '${url}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`); + } + + return; + } // If the route matches, but there aren't any capture groups defined, then + // this will return [], which is truthy and therefore sufficient to + // indicate a match. + // If there are capture groups, then it will return their values. + + + return result.slice(1); + }; + + super(match, handler, method); + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Router can be used to process a FetchEvent through one or more + * [Routes]{@link module:workbox-routing.Route} responding with a Request if + * a matching route exists. + * + * If no route matches a given a request, the Router will use a "default" + * handler if one is defined. + * + * Should the matching Route throw an error, the Router will use a "catch" + * handler if one is defined to gracefully deal with issues and respond with a + * Request. + * + * If a request matches multiple routes, the **earliest** registered route will + * be used to respond to the request. + * + * @memberof module:workbox-routing + */ + + class Router { + /** + * Initializes a new Router. + */ + constructor() { + this._routes = new Map(); + } + /** + * @return {Map>} routes A `Map` of HTTP + * method name ('GET', etc.) to an array of all the corresponding `Route` + * instances that are registered. + */ + + + get routes() { + return this._routes; + } + /** + * Adds a fetch event listener to respond to events when a route matches + * the event's request. + */ + + + addFetchListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('fetch', event => { + const { + request + } = event; + const responsePromise = this.handleRequest({ + request, + event + }); + + if (responsePromise) { + event.respondWith(responsePromise); + } + }); + } + /** + * Adds a message event listener for URLs to cache from the window. + * This is useful to cache resources loaded on the page prior to when the + * service worker started controlling it. + * + * The format of the message data sent from the window should be as follows. + * Where the `urlsToCache` array may consist of URL strings or an array of + * URL string + `requestInit` object (the same as you'd pass to `fetch()`). + * + * ``` + * { + * type: 'CACHE_URLS', + * payload: { + * urlsToCache: [ + * './script1.js', + * './script2.js', + * ['./script3.js', {mode: 'no-cors'}], + * ], + * }, + * } + * ``` + */ + + + addCacheListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('message', event => { + if (event.data && event.data.type === 'CACHE_URLS') { + const { + payload + } = event.data; + + { + logger_js.logger.debug(`Caching URLs from the window`, payload.urlsToCache); + } + + const requestPromises = Promise.all(payload.urlsToCache.map(entry => { + if (typeof entry === 'string') { + entry = [entry]; + } + + const request = new Request(...entry); + return this.handleRequest({ + request + }); // TODO(philipwalton): TypeScript errors without this typecast for + // some reason (probably a bug). The real type here should work but + // doesn't: `Array | undefined>`. + })); // TypeScript + + event.waitUntil(requestPromises); // If a MessageChannel was used, reply to the message on success. + + if (event.ports && event.ports[0]) { + requestPromises.then(() => event.ports[0].postMessage(true)); + } + } + }); + } + /** + * Apply the routing rules to a FetchEvent object to get a Response from an + * appropriate Route's handler. + * + * @param {Object} options + * @param {Request} options.request The request to handle (this is usually + * from a fetch event, but it does not have to be). + * @param {FetchEvent} [options.event] The event that triggered the request, + * if applicable. + * @return {Promise|undefined} A promise is returned if a + * registered route can handle the request. If there is no matching + * route and there's no `defaultHandler`, `undefined` is returned. + */ + + + handleRequest({ + request, + event + }) { + { + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'handleRequest', + paramName: 'options.request' + }); + } + + const url = new URL(request.url, location.href); + + if (!url.protocol.startsWith('http')) { + { + logger_js.logger.debug(`Workbox Router only supports URLs that start with 'http'.`); + } + + return; + } + + const { + params, + route + } = this.findMatchingRoute({ + url, + request, + event + }); + let handler = route && route.handler; + const debugMessages = []; + + { + if (handler) { + debugMessages.push([`Found a route to handle this request:`, route]); + + if (params) { + debugMessages.push([`Passing the following params to the route's handler:`, params]); + } + } + } // If we don't have a handler because there was no matching route, then + // fall back to defaultHandler if that's defined. + + + if (!handler && this._defaultHandler) { + { + debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler.`); + } + + handler = this._defaultHandler; + } + + if (!handler) { + { + // No handler so Workbox will do nothing. If logs is set of debug + // i.e. verbose, we should print out this information. + logger_js.logger.debug(`No route found for: ${getFriendlyURL_js.getFriendlyURL(url)}`); + } + + return; + } + + { + // We have a handler, meaning Workbox is going to handle the route. + // print the routing details to the console. + logger_js.logger.groupCollapsed(`Router is responding to: ${getFriendlyURL_js.getFriendlyURL(url)}`); + debugMessages.forEach(msg => { + if (Array.isArray(msg)) { + logger_js.logger.log(...msg); + } else { + logger_js.logger.log(msg); + } + }); + logger_js.logger.groupEnd(); + } // Wrap in try and catch in case the handle method throws a synchronous + // error. It should still callback to the catch handler. + + + let responsePromise; + + try { + responsePromise = handler.handle({ + url, + request, + event, + params + }); + } catch (err) { + responsePromise = Promise.reject(err); + } + + if (responsePromise instanceof Promise && this._catchHandler) { + responsePromise = responsePromise.catch(err => { + { + // Still include URL here as it will be async from the console group + // and may not make sense without the URL + logger_js.logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL_js.getFriendlyURL(url)}. Falling back to Catch Handler.`); + logger_js.logger.error(`Error thrown by:`, route); + logger_js.logger.error(err); + logger_js.logger.groupEnd(); + } + + return this._catchHandler.handle({ + url, + request, + event + }); + }); + } + + return responsePromise; + } + /** + * Checks a request and URL (and optionally an event) against the list of + * registered routes, and if there's a match, returns the corresponding + * route along with any params generated by the match. + * + * @param {Object} options + * @param {URL} options.url + * @param {Request} options.request The request to match. + * @param {Event} [options.event] The corresponding event (unless N/A). + * @return {Object} An object with `route` and `params` properties. + * They are populated if a matching route was found or `undefined` + * otherwise. + */ + + + findMatchingRoute({ + url, + request, + event + }) { + { + assert_js.assert.isInstance(url, URL, { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'findMatchingRoute', + paramName: 'options.url' + }); + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'findMatchingRoute', + paramName: 'options.request' + }); + } + + const routes = this._routes.get(request.method) || []; + + for (const route of routes) { + let params; + const matchResult = route.match({ + url, + request, + event + }); + + if (matchResult) { + // See https://github.com/GoogleChrome/workbox/issues/2079 + params = matchResult; + + if (Array.isArray(matchResult) && matchResult.length === 0) { + // Instead of passing an empty array in as params, use undefined. + params = undefined; + } else if (matchResult.constructor === Object && Object.keys(matchResult).length === 0) { + // Instead of passing an empty object in as params, use undefined. + params = undefined; + } else if (typeof matchResult === 'boolean') { + // For the boolean value true (rather than just something truth-y), + // don't set params. + // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 + params = undefined; + } // Return early if have a match. + + + return { + route, + params + }; + } + } // If no match was found above, return and empty object. + + + return {}; + } + /** + * Define a default `handler` that's called when no routes explicitly + * match the incoming request. + * + * Without a default handler, unmatched requests will go against the + * network as if there were no service worker present. + * + * @param {module:workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + */ + + + setDefaultHandler(handler) { + this._defaultHandler = normalizeHandler(handler); + } + /** + * If a Route throws an error while handling a request, this `handler` + * will be called and given a chance to provide a response. + * + * @param {module:workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + */ + + + setCatchHandler(handler) { + this._catchHandler = normalizeHandler(handler); + } + /** + * Registers a route with the router. + * + * @param {module:workbox-routing.Route} route The route to register. + */ + + + registerRoute(route) { + { + assert_js.assert.isType(route, 'object', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + assert_js.assert.hasMethod(route, 'match', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + assert_js.assert.isType(route.handler, 'object', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + assert_js.assert.hasMethod(route.handler, 'handle', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route.handler' + }); + assert_js.assert.isType(route.method, 'string', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route.method' + }); + } + + if (!this._routes.has(route.method)) { + this._routes.set(route.method, []); + } // Give precedence to all of the earlier routes by adding this additional + // route to the end of the array. + + + this._routes.get(route.method).push(route); + } + /** + * Unregisters a route with the router. + * + * @param {module:workbox-routing.Route} route The route to unregister. + */ + + + unregisterRoute(route) { + if (!this._routes.has(route.method)) { + throw new WorkboxError_js.WorkboxError('unregister-route-but-not-found-with-method', { + method: route.method + }); + } + + const routeIndex = this._routes.get(route.method).indexOf(route); + + if (routeIndex > -1) { + this._routes.get(route.method).splice(routeIndex, 1); + } else { + throw new WorkboxError_js.WorkboxError('unregister-route-route-not-registered'); + } + } + + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let defaultRouter; + /** + * Creates a new, singleton Router instance if one does not exist. If one + * does already exist, that instance is returned. + * + * @private + * @return {Router} + */ + + const getOrCreateDefaultRouter = () => { + if (!defaultRouter) { + defaultRouter = new Router(); // The helpers that use the default Router assume these listeners exist. + + defaultRouter.addFetchListener(); + defaultRouter.addCacheListener(); + } + + return defaultRouter; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Easily register a RegExp, string, or function with a caching + * strategy to a singleton Router instance. + * + * This method will generate a Route for you if needed and + * call [registerRoute()]{@link module:workbox-routing.Router#registerRoute}. + * + * @param {RegExp|string|module:workbox-routing.Route~matchCallback|module:workbox-routing.Route} capture + * If the capture param is a `Route`, all other arguments will be ignored. + * @param {module:workbox-routing~handlerCallback} [handler] A callback + * function that returns a Promise resulting in a Response. This parameter + * is required if `capture` is not a `Route` object. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + * @return {module:workbox-routing.Route} The generated `Route`(Useful for + * unregistering). + * + * @memberof module:workbox-routing + */ + + function registerRoute(capture, handler, method) { + let route; + + if (typeof capture === 'string') { + const captureUrl = new URL(capture, location.href); + + { + if (!(capture.startsWith('/') || capture.startsWith('http'))) { + throw new WorkboxError_js.WorkboxError('invalid-string', { + moduleName: 'workbox-routing', + funcName: 'registerRoute', + paramName: 'capture' + }); + } // We want to check if Express-style wildcards are in the pathname only. + // TODO: Remove this log message in v4. + + + const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture; // See https://github.com/pillarjs/path-to-regexp#parameters + + const wildcards = '[*:?+]'; + + if (new RegExp(`${wildcards}`).exec(valueToCheck)) { + logger_js.logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`); + } + } + + const matchCallback = ({ + url + }) => { + { + if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { + logger_js.logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url}. This route will only handle cross-origin requests ` + `if they match the entire URL.`); + } + } + + return url.href === captureUrl.href; + }; // If `capture` is a string then `handler` and `method` must be present. + + + route = new Route(matchCallback, handler, method); + } else if (capture instanceof RegExp) { + // If `capture` is a `RegExp` then `handler` and `method` must be present. + route = new RegExpRoute(capture, handler, method); + } else if (typeof capture === 'function') { + // If `capture` is a function then `handler` and `method` must be present. + route = new Route(capture, handler, method); + } else if (capture instanceof Route) { + route = capture; + } else { + throw new WorkboxError_js.WorkboxError('unsupported-route-type', { + moduleName: 'workbox-routing', + funcName: 'registerRoute', + paramName: 'capture' + }); + } + + const defaultRouter = getOrCreateDefaultRouter(); + defaultRouter.registerRoute(route); + return route; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * If a Route throws an error while handling a request, this `handler` + * will be called and given a chance to provide a response. + * + * @param {module:workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * + * @memberof module:workbox-routing + */ + + function setCatchHandler(handler) { + const defaultRouter = getOrCreateDefaultRouter(); + defaultRouter.setCatchHandler(handler); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Define a default `handler` that's called when no routes explicitly + * match the incoming request. + * + * Without a default handler, unmatched requests will go against the + * network as if there were no service worker present. + * + * @param {module:workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * + * @memberof module:workbox-routing + */ + + function setDefaultHandler(handler) { + const defaultRouter = getOrCreateDefaultRouter(); + defaultRouter.setDefaultHandler(handler); + } + + exports.NavigationRoute = NavigationRoute; + exports.RegExpRoute = RegExpRoute; + exports.Route = Route; + exports.Router = Router; + exports.registerRoute = registerRoute; + exports.setCatchHandler = setCatchHandler; + exports.setDefaultHandler = setDefaultHandler; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); + diff --git a/source/js/workbox/workbox-routing.prod.js b/source/js/workbox/workbox-routing.prod.js new file mode 100644 index 0000000..d1559da --- /dev/null +++ b/source/js/workbox/workbox-routing.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.routing=function(t,e){"use strict";try{self["workbox:routing:5.1.4"]&&_()}catch(t){}const s=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,r="GET"){this.handler=s(e),this.match=t,this.method=r}}class n extends r{constructor(t,e,s){super(({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)},e,s)}}class o{constructor(){this.t=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)})}addCacheListener(){self.addEventListener("message",t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map(t=>{"string"==typeof t&&(t=[t]);const e=new Request(...t);return this.handleRequest({request:e})}));t.waitUntil(s),t.ports&&t.ports[0]&&s.then(()=>t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const{params:r,route:n}=this.findMatchingRoute({url:s,request:t,event:e});let o,i=n&&n.handler;if(!i&&this.s&&(i=this.s),i){try{o=i.handle({url:s,request:t,event:e,params:r})}catch(t){o=Promise.reject(t)}return o instanceof Promise&&this.o&&(o=o.catch(r=>this.o.handle({url:s,request:t,event:e}))),o}}findMatchingRoute({url:t,request:e,event:s}){const r=this.t.get(e.method)||[];for(const n of r){let r;const o=n.match({url:t,request:e,event:s});if(o)return r=o,(Array.isArray(o)&&0===o.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(r=void 0),{route:n,params:r}}return{}}setDefaultHandler(t){this.s=s(t)}setCatchHandler(t){this.o=s(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new e.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const s=this.t.get(t.method).indexOf(t);if(!(s>-1))throw new e.WorkboxError("unregister-route-route-not-registered");this.t.get(t.method).splice(s,1)}}let i;const u=()=>(i||(i=new o,i.addFetchListener(),i.addCacheListener()),i);return t.NavigationRoute=class extends r{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super(t=>this.i(t),t),this.u=e,this.h=s}i({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.h)if(t.test(s))return!1;return!!this.u.some(t=>t.test(s))}},t.RegExpRoute=n,t.Route=r,t.Router=o,t.registerRoute=function(t,s,o){let i;if("string"==typeof t){const e=new URL(t,location.href);i=new r(({url:t})=>t.href===e.href,s,o)}else if(t instanceof RegExp)i=new n(t,s,o);else if("function"==typeof t)i=new r(t,s,o);else{if(!(t instanceof r))throw new e.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});i=t}return u().registerRoute(i),i},t.setCatchHandler=function(t){u().setCatchHandler(t)},t.setDefaultHandler=function(t){u().setDefaultHandler(t)},t}({},workbox.core._private); + diff --git a/source/js/workbox/workbox-strategies.dev.js b/source/js/workbox/workbox-strategies.dev.js new file mode 100644 index 0000000..b35a5e0 --- /dev/null +++ b/source/js/workbox/workbox-strategies.dev.js @@ -0,0 +1,923 @@ +this.workbox = this.workbox || {}; +this.workbox.strategies = (function (exports, assert_js, cacheNames_js, cacheWrapper_js, fetchWrapper_js, getFriendlyURL_js, logger_js, WorkboxError_js) { + 'use strict'; + + try { + self['workbox:strategies:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const messages = { + strategyStart: (strategyName, request) => `Using ${strategyName} to respond to '${getFriendlyURL_js.getFriendlyURL(request.url)}'`, + printFinalResponse: response => { + if (response) { + logger_js.logger.groupCollapsed(`View the final response here.`); + logger_js.logger.log(response || '[No response returned]'); + logger_js.logger.groupEnd(); + } + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a [cache-first]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-falling-back-to-network} + * request strategy. + * + * A cache first strategy is useful for assets that have been revisioned, + * such as URLs like `/styles/example.a8f5f1.css`, since they + * can be cached for long periods of time. + * + * If the network request fails, and there is no cache match, this will throw + * a `WorkboxError` exception. + * + * @memberof module:workbox-strategies + */ + + class CacheFirst { + /** + * @param {Object} options + * @param {string} options.cacheName Cache name to store and retrieve + * requests. Defaults to cache names provided by + * [workbox-core]{@link module:workbox-core.cacheNames}. + * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} options.fetchOptions Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of all fetch() requests made by this strategy. + * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) + */ + constructor(options = {}) { + this._cacheName = cacheNames_js.cacheNames.getRuntimeName(options.cacheName); + this._plugins = options.plugins || []; + this._fetchOptions = options.fetchOptions; + this._matchOptions = options.matchOptions; + } + /** + * This method will perform a request strategy and follows an API that + * will work with the + * [Workbox Router]{@link module:workbox-routing.Router}. + * + * @param {Object} options + * @param {Request|string} options.request A request to run this strategy for. + * @param {Event} [options.event] The event that triggered the request. + * @return {Promise} + */ + + + async handle({ + event, + request + }) { + const logs = []; + + if (typeof request === 'string') { + request = new Request(request); + } + + { + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: 'CacheFirst', + funcName: 'makeRequest', + paramName: 'request' + }); + } + + let response = await cacheWrapper_js.cacheWrapper.match({ + cacheName: this._cacheName, + request, + event, + matchOptions: this._matchOptions, + plugins: this._plugins + }); + let error; + + if (!response) { + { + logs.push(`No response found in the '${this._cacheName}' cache. ` + `Will respond with a network request.`); + } + + try { + response = await this._getFromNetwork(request, event); + } catch (err) { + error = err; + } + + { + if (response) { + logs.push(`Got response from network.`); + } else { + logs.push(`Unable to get a response from the network.`); + } + } + } else { + { + logs.push(`Found a cached response in the '${this._cacheName}' cache.`); + } + } + + { + logger_js.logger.groupCollapsed(messages.strategyStart('CacheFirst', request)); + + for (const log of logs) { + logger_js.logger.log(log); + } + + messages.printFinalResponse(response); + logger_js.logger.groupEnd(); + } + + if (!response) { + throw new WorkboxError_js.WorkboxError('no-response', { + url: request.url, + error + }); + } + + return response; + } + /** + * Handles the network and cache part of CacheFirst. + * + * @param {Request} request + * @param {Event} [event] + * @return {Promise} + * + * @private + */ + + + async _getFromNetwork(request, event) { + const response = await fetchWrapper_js.fetchWrapper.fetch({ + request, + event, + fetchOptions: this._fetchOptions, + plugins: this._plugins + }); // Keep the service worker while we put the request to the cache + + const responseClone = response.clone(); + const cachePutPromise = cacheWrapper_js.cacheWrapper.put({ + cacheName: this._cacheName, + request, + response: responseClone, + event, + plugins: this._plugins + }); + + if (event) { + try { + event.waitUntil(cachePutPromise); + } catch (error) { + { + logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_js.getFriendlyURL(request.url)}'.`); + } + } + } + + return response; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a + * [cache-only]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-only} + * request strategy. + * + * This class is useful if you want to take advantage of any + * [Workbox plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}. + * + * If there is no cache match, this will throw a `WorkboxError` exception. + * + * @memberof module:workbox-strategies + */ + + class CacheOnly { + /** + * @param {Object} options + * @param {string} options.cacheName Cache name to store and retrieve + * requests. Defaults to cache names provided by + * [workbox-core]{@link module:workbox-core.cacheNames}. + * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) + */ + constructor(options = {}) { + this._cacheName = cacheNames_js.cacheNames.getRuntimeName(options.cacheName); + this._plugins = options.plugins || []; + this._matchOptions = options.matchOptions; + } + /** + * This method will perform a request strategy and follows an API that + * will work with the + * [Workbox Router]{@link module:workbox-routing.Router}. + * + * @param {Object} options + * @param {Request|string} options.request A request to run this strategy for. + * @param {Event} [options.event] The event that triggered the request. + * @return {Promise} + */ + + + async handle({ + event, + request + }) { + if (typeof request === 'string') { + request = new Request(request); + } + + { + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: 'CacheOnly', + funcName: 'makeRequest', + paramName: 'request' + }); + } + + const response = await cacheWrapper_js.cacheWrapper.match({ + cacheName: this._cacheName, + request, + event, + matchOptions: this._matchOptions, + plugins: this._plugins + }); + + { + logger_js.logger.groupCollapsed(messages.strategyStart('CacheOnly', request)); + + if (response) { + logger_js.logger.log(`Found a cached response in the '${this._cacheName}'` + ` cache.`); + messages.printFinalResponse(response); + } else { + logger_js.logger.log(`No response found in the '${this._cacheName}' cache.`); + } + + logger_js.logger.groupEnd(); + } + + if (!response) { + throw new WorkboxError_js.WorkboxError('no-response', { + url: request.url + }); + } + + return response; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const cacheOkAndOpaquePlugin = { + /** + * Returns a valid response (to allow caching) if the status is 200 (OK) or + * 0 (opaque). + * + * @param {Object} options + * @param {Response} options.response + * @return {Response|null} + * + * @private + */ + cacheWillUpdate: async ({ + response + }) => { + if (response.status === 200 || response.status === 0) { + return response; + } + + return null; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a + * [network first]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#network-falling-back-to-cache} + * request strategy. + * + * By default, this strategy will cache responses with a 200 status code as + * well as [opaque responses]{@link https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests}. + * Opaque responses are are cross-origin requests where the response doesn't + * support [CORS]{@link https://enable-cors.org/}. + * + * If the network request fails, and there is no cache match, this will throw + * a `WorkboxError` exception. + * + * @memberof module:workbox-strategies + */ + + class NetworkFirst { + /** + * @param {Object} options + * @param {string} options.cacheName Cache name to store and retrieve + * requests. Defaults to cache names provided by + * [workbox-core]{@link module:workbox-core.cacheNames}. + * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} options.fetchOptions Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of all fetch() requests made by this strategy. + * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) + * @param {number} options.networkTimeoutSeconds If set, any network requests + * that fail to respond within the timeout will fallback to the cache. + * + * This option can be used to combat + * "[lie-fi]{@link https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi}" + * scenarios. + */ + constructor(options = {}) { + this._cacheName = cacheNames_js.cacheNames.getRuntimeName(options.cacheName); + + if (options.plugins) { + const isUsingCacheWillUpdate = options.plugins.some(plugin => !!plugin.cacheWillUpdate); + this._plugins = isUsingCacheWillUpdate ? options.plugins : [cacheOkAndOpaquePlugin, ...options.plugins]; + } else { + // No plugins passed in, use the default plugin. + this._plugins = [cacheOkAndOpaquePlugin]; + } + + this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0; + + { + if (this._networkTimeoutSeconds) { + assert_js.assert.isType(this._networkTimeoutSeconds, 'number', { + moduleName: 'workbox-strategies', + className: 'NetworkFirst', + funcName: 'constructor', + paramName: 'networkTimeoutSeconds' + }); + } + } + + this._fetchOptions = options.fetchOptions; + this._matchOptions = options.matchOptions; + } + /** + * This method will perform a request strategy and follows an API that + * will work with the + * [Workbox Router]{@link module:workbox-routing.Router}. + * + * @param {Object} options + * @param {Request|string} options.request A request to run this strategy for. + * @param {Event} [options.event] The event that triggered the request. + * @return {Promise} + */ + + + async handle({ + event, + request + }) { + const logs = []; + + if (typeof request === 'string') { + request = new Request(request); + } + + { + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: 'NetworkFirst', + funcName: 'handle', + paramName: 'makeRequest' + }); + } + + const promises = []; + let timeoutId; + + if (this._networkTimeoutSeconds) { + const { + id, + promise + } = this._getTimeoutPromise({ + request, + event, + logs + }); + + timeoutId = id; + promises.push(promise); + } + + const networkPromise = this._getNetworkPromise({ + timeoutId, + request, + event, + logs + }); + + promises.push(networkPromise); // Promise.race() will resolve as soon as the first promise resolves. + + let response = await Promise.race(promises); // If Promise.race() resolved with null, it might be due to a network + // timeout + a cache miss. If that were to happen, we'd rather wait until + // the networkPromise resolves instead of returning null. + // Note that it's fine to await an already-resolved promise, so we don't + // have to check to see if it's still "in flight". + + if (!response) { + response = await networkPromise; + } + + { + logger_js.logger.groupCollapsed(messages.strategyStart('NetworkFirst', request)); + + for (const log of logs) { + logger_js.logger.log(log); + } + + messages.printFinalResponse(response); + logger_js.logger.groupEnd(); + } + + if (!response) { + throw new WorkboxError_js.WorkboxError('no-response', { + url: request.url + }); + } + + return response; + } + /** + * @param {Object} options + * @param {Request} options.request + * @param {Array} options.logs A reference to the logs array + * @param {Event} [options.event] + * @return {Promise} + * + * @private + */ + + + _getTimeoutPromise({ + request, + logs, + event + }) { + let timeoutId; + const timeoutPromise = new Promise(resolve => { + const onNetworkTimeout = async () => { + { + logs.push(`Timing out the network response at ` + `${this._networkTimeoutSeconds} seconds.`); + } + + resolve(await this._respondFromCache({ + request, + event + })); + }; + + timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000); + }); + return { + promise: timeoutPromise, + id: timeoutId + }; + } + /** + * @param {Object} options + * @param {number|undefined} options.timeoutId + * @param {Request} options.request + * @param {Array} options.logs A reference to the logs Array. + * @param {Event} [options.event] + * @return {Promise} + * + * @private + */ + + + async _getNetworkPromise({ + timeoutId, + request, + logs, + event + }) { + let error; + let response; + + try { + response = await fetchWrapper_js.fetchWrapper.fetch({ + request, + event, + fetchOptions: this._fetchOptions, + plugins: this._plugins + }); + } catch (err) { + error = err; + } + + if (timeoutId) { + clearTimeout(timeoutId); + } + + { + if (response) { + logs.push(`Got response from network.`); + } else { + logs.push(`Unable to get a response from the network. Will respond ` + `with a cached response.`); + } + } + + if (error || !response) { + response = await this._respondFromCache({ + request, + event + }); + + { + if (response) { + logs.push(`Found a cached response in the '${this._cacheName}'` + ` cache.`); + } else { + logs.push(`No response found in the '${this._cacheName}' cache.`); + } + } + } else { + // Keep the service worker alive while we put the request in the cache + const responseClone = response.clone(); + const cachePut = cacheWrapper_js.cacheWrapper.put({ + cacheName: this._cacheName, + request, + response: responseClone, + event, + plugins: this._plugins + }); + + if (event) { + try { + // The event has been responded to so we can keep the SW alive to + // respond to the request + event.waitUntil(cachePut); + } catch (err) { + { + logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_js.getFriendlyURL(request.url)}'.`); + } + } + } + } + + return response; + } + /** + * Used if the network timeouts or fails to make the request. + * + * @param {Object} options + * @param {Request} request The request to match in the cache + * @param {Event} [options.event] + * @return {Promise} + * + * @private + */ + + + _respondFromCache({ + event, + request + }) { + return cacheWrapper_js.cacheWrapper.match({ + cacheName: this._cacheName, + request, + event, + matchOptions: this._matchOptions, + plugins: this._plugins + }); + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a + * [network-only]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#network-only} + * request strategy. + * + * This class is useful if you want to take advantage of any + * [Workbox plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}. + * + * If the network request fails, this will throw a `WorkboxError` exception. + * + * @memberof module:workbox-strategies + */ + + class NetworkOnly { + /** + * @param {Object} options + * @param {string} options.cacheName Cache name to store and retrieve + * requests. Defaults to cache names provided by + * [workbox-core]{@link module:workbox-core.cacheNames}. + * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} options.fetchOptions Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of all fetch() requests made by this strategy. + */ + constructor(options = {}) { + this._plugins = options.plugins || []; + this._fetchOptions = options.fetchOptions; + } + /** + * This method will perform a request strategy and follows an API that + * will work with the + * [Workbox Router]{@link module:workbox-routing.Router}. + * + * @param {Object} options + * @param {Request|string} options.request The request to run this strategy for. + * @param {Event} [options.event] The event that triggered the request. + * @return {Promise} + */ + + + async handle({ + event, + request + }) { + if (typeof request === 'string') { + request = new Request(request); + } + + { + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: 'NetworkOnly', + funcName: 'handle', + paramName: 'request' + }); + } + + let error; + let response; + + try { + response = await fetchWrapper_js.fetchWrapper.fetch({ + request, + event, + fetchOptions: this._fetchOptions, + plugins: this._plugins + }); + } catch (err) { + error = err; + } + + { + logger_js.logger.groupCollapsed(messages.strategyStart('NetworkOnly', request)); + + if (response) { + logger_js.logger.log(`Got response from network.`); + } else { + logger_js.logger.log(`Unable to get a response from the network.`); + } + + messages.printFinalResponse(response); + logger_js.logger.groupEnd(); + } + + if (!response) { + throw new WorkboxError_js.WorkboxError('no-response', { + url: request.url, + error + }); + } + + return response; + } + + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a + * [stale-while-revalidate]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate} + * request strategy. + * + * Resources are requested from both the cache and the network in parallel. + * The strategy will respond with the cached version if available, otherwise + * wait for the network response. The cache is updated with the network response + * with each successful request. + * + * By default, this strategy will cache responses with a 200 status code as + * well as [opaque responses]{@link https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests}. + * Opaque responses are cross-origin requests where the response doesn't + * support [CORS]{@link https://enable-cors.org/}. + * + * If the network request fails, and there is no cache match, this will throw + * a `WorkboxError` exception. + * + * @memberof module:workbox-strategies + */ + + class StaleWhileRevalidate { + /** + * @param {Object} options + * @param {string} options.cacheName Cache name to store and retrieve + * requests. Defaults to cache names provided by + * [workbox-core]{@link module:workbox-core.cacheNames}. + * @param {Array} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} options.fetchOptions Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of all fetch() requests made by this strategy. + * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) + */ + constructor(options = {}) { + this._cacheName = cacheNames_js.cacheNames.getRuntimeName(options.cacheName); + this._plugins = options.plugins || []; + + if (options.plugins) { + const isUsingCacheWillUpdate = options.plugins.some(plugin => !!plugin.cacheWillUpdate); + this._plugins = isUsingCacheWillUpdate ? options.plugins : [cacheOkAndOpaquePlugin, ...options.plugins]; + } else { + // No plugins passed in, use the default plugin. + this._plugins = [cacheOkAndOpaquePlugin]; + } + + this._fetchOptions = options.fetchOptions; + this._matchOptions = options.matchOptions; + } + /** + * This method will perform a request strategy and follows an API that + * will work with the + * [Workbox Router]{@link module:workbox-routing.Router}. + * + * @param {Object} options + * @param {Request|string} options.request A request to run this strategy for. + * @param {Event} [options.event] The event that triggered the request. + * @return {Promise} + */ + + + async handle({ + event, + request + }) { + const logs = []; + + if (typeof request === 'string') { + request = new Request(request); + } + + { + assert_js.assert.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: 'StaleWhileRevalidate', + funcName: 'handle', + paramName: 'request' + }); + } + + const fetchAndCachePromise = this._getFromNetwork({ + request, + event + }); + + let response = await cacheWrapper_js.cacheWrapper.match({ + cacheName: this._cacheName, + request, + event, + matchOptions: this._matchOptions, + plugins: this._plugins + }); + let error; + + if (response) { + { + logs.push(`Found a cached response in the '${this._cacheName}'` + ` cache. Will update with the network response in the background.`); + } + + if (event) { + try { + event.waitUntil(fetchAndCachePromise); + } catch (error) { + { + logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_js.getFriendlyURL(request.url)}'.`); + } + } + } + } else { + { + logs.push(`No response found in the '${this._cacheName}' cache. ` + `Will wait for the network response.`); + } + + try { + response = await fetchAndCachePromise; + } catch (err) { + error = err; + } + } + + { + logger_js.logger.groupCollapsed(messages.strategyStart('StaleWhileRevalidate', request)); + + for (const log of logs) { + logger_js.logger.log(log); + } + + messages.printFinalResponse(response); + logger_js.logger.groupEnd(); + } + + if (!response) { + throw new WorkboxError_js.WorkboxError('no-response', { + url: request.url, + error + }); + } + + return response; + } + /** + * @param {Object} options + * @param {Request} options.request + * @param {Event} [options.event] + * @return {Promise} + * + * @private + */ + + + async _getFromNetwork({ + request, + event + }) { + const response = await fetchWrapper_js.fetchWrapper.fetch({ + request, + event, + fetchOptions: this._fetchOptions, + plugins: this._plugins + }); + const cachePutPromise = cacheWrapper_js.cacheWrapper.put({ + cacheName: this._cacheName, + request, + response: response.clone(), + event, + plugins: this._plugins + }); + + if (event) { + try { + event.waitUntil(cachePutPromise); + } catch (error) { + { + logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache for '${getFriendlyURL_js.getFriendlyURL(request.url)}'.`); + } + } + } + + return response; + } + + } + + exports.CacheFirst = CacheFirst; + exports.CacheOnly = CacheOnly; + exports.NetworkFirst = NetworkFirst; + exports.NetworkOnly = NetworkOnly; + exports.StaleWhileRevalidate = StaleWhileRevalidate; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); + diff --git a/source/js/workbox/workbox-strategies.prod.js b/source/js/workbox/workbox-strategies.prod.js new file mode 100644 index 0000000..0b391da --- /dev/null +++ b/source/js/workbox/workbox-strategies.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.strategies=function(t,e,s,i,n){"use strict";try{self["workbox:strategies:5.1.4"]&&_()}catch(t){}const r={cacheWillUpdate:async({response:t})=>200===t.status||0===t.status?t:null};return t.CacheFirst=class{constructor(t={}){this.t=e.cacheNames.getRuntimeName(t.cacheName),this.s=t.plugins||[],this.i=t.fetchOptions,this.h=t.matchOptions}async handle({event:t,request:e}){"string"==typeof e&&(e=new Request(e));let i,r=await s.cacheWrapper.match({cacheName:this.t,request:e,event:t,matchOptions:this.h,plugins:this.s});if(!r)try{r=await this.o(e,t)}catch(t){i=t}if(!r)throw new n.WorkboxError("no-response",{url:e.url,error:i});return r}async o(t,e){const n=await i.fetchWrapper.fetch({request:t,event:e,fetchOptions:this.i,plugins:this.s}),r=n.clone(),h=s.cacheWrapper.put({cacheName:this.t,request:t,response:r,event:e,plugins:this.s});if(e)try{e.waitUntil(h)}catch(t){}return n}},t.CacheOnly=class{constructor(t={}){this.t=e.cacheNames.getRuntimeName(t.cacheName),this.s=t.plugins||[],this.h=t.matchOptions}async handle({event:t,request:e}){"string"==typeof e&&(e=new Request(e));const i=await s.cacheWrapper.match({cacheName:this.t,request:e,event:t,matchOptions:this.h,plugins:this.s});if(!i)throw new n.WorkboxError("no-response",{url:e.url});return i}},t.NetworkFirst=class{constructor(t={}){if(this.t=e.cacheNames.getRuntimeName(t.cacheName),t.plugins){const e=t.plugins.some(t=>!!t.cacheWillUpdate);this.s=e?t.plugins:[r,...t.plugins]}else this.s=[r];this.u=t.networkTimeoutSeconds||0,this.i=t.fetchOptions,this.h=t.matchOptions}async handle({event:t,request:e}){const s=[];"string"==typeof e&&(e=new Request(e));const i=[];let r;if(this.u){const{id:n,promise:h}=this.l({request:e,event:t,logs:s});r=n,i.push(h)}const h=this.p({timeoutId:r,request:e,event:t,logs:s});i.push(h);let o=await Promise.race(i);if(o||(o=await h),!o)throw new n.WorkboxError("no-response",{url:e.url});return o}l({request:t,logs:e,event:s}){let i;return{promise:new Promise(e=>{i=setTimeout(async()=>{e(await this.q({request:t,event:s}))},1e3*this.u)}),id:i}}async p({timeoutId:t,request:e,logs:n,event:r}){let h,o;try{o=await i.fetchWrapper.fetch({request:e,event:r,fetchOptions:this.i,plugins:this.s})}catch(t){h=t}if(t&&clearTimeout(t),h||!o)o=await this.q({request:e,event:r});else{const t=o.clone(),i=s.cacheWrapper.put({cacheName:this.t,request:e,response:t,event:r,plugins:this.s});if(r)try{r.waitUntil(i)}catch(t){}}return o}q({event:t,request:e}){return s.cacheWrapper.match({cacheName:this.t,request:e,event:t,matchOptions:this.h,plugins:this.s})}},t.NetworkOnly=class{constructor(t={}){this.s=t.plugins||[],this.i=t.fetchOptions}async handle({event:t,request:e}){let s,r;"string"==typeof e&&(e=new Request(e));try{r=await i.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s})}catch(t){s=t}if(!r)throw new n.WorkboxError("no-response",{url:e.url,error:s});return r}},t.StaleWhileRevalidate=class{constructor(t={}){if(this.t=e.cacheNames.getRuntimeName(t.cacheName),this.s=t.plugins||[],t.plugins){const e=t.plugins.some(t=>!!t.cacheWillUpdate);this.s=e?t.plugins:[r,...t.plugins]}else this.s=[r];this.i=t.fetchOptions,this.h=t.matchOptions}async handle({event:t,request:e}){"string"==typeof e&&(e=new Request(e));const i=this.o({request:e,event:t});let r,h=await s.cacheWrapper.match({cacheName:this.t,request:e,event:t,matchOptions:this.h,plugins:this.s});if(h){if(t)try{t.waitUntil(i)}catch(r){}}else try{h=await i}catch(t){r=t}if(!h)throw new n.WorkboxError("no-response",{url:e.url,error:r});return h}async o({request:t,event:e}){const n=await i.fetchWrapper.fetch({request:t,event:e,fetchOptions:this.i,plugins:this.s}),r=s.cacheWrapper.put({cacheName:this.t,request:t,response:n.clone(),event:e,plugins:this.s});if(e)try{e.waitUntil(r)}catch(t){}return n}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private); + diff --git a/source/js/workbox/workbox-streams.dev.js b/source/js/workbox/workbox-streams.dev.js new file mode 100644 index 0000000..11db538 --- /dev/null +++ b/source/js/workbox/workbox-streams.dev.js @@ -0,0 +1,318 @@ +this.workbox = this.workbox || {}; +this.workbox.streams = (function (exports, logger_js, assert_js, Deferred_js, canConstructReadableStream_js) { + 'use strict'; + + try { + self['workbox:streams:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Takes either a Response, a ReadableStream, or a + * [BodyInit](https://fetch.spec.whatwg.org/#bodyinit) and returns the + * ReadableStreamReader object associated with it. + * + * @param {module:workbox-streams.StreamSource} source + * @return {ReadableStreamReader} + * @private + */ + + function _getReaderFromSource(source) { + if (source instanceof Response) { + return source.body.getReader(); + } + + if (source instanceof ReadableStream) { + return source.getReader(); + } + + return new Response(source).body.getReader(); + } + /** + * Takes multiple source Promises, each of which could resolve to a Response, a + * ReadableStream, or a [BodyInit](https://fetch.spec.whatwg.org/#bodyinit). + * + * Returns an object exposing a ReadableStream with each individual stream's + * data returned in sequence, along with a Promise which signals when the + * stream is finished (useful for passing to a FetchEvent's waitUntil()). + * + * @param {Array>} sourcePromises + * @return {Object<{done: Promise, stream: ReadableStream}>} + * + * @memberof module:workbox-streams + */ + + + function concatenate(sourcePromises) { + { + assert_js.assert.isArray(sourcePromises, { + moduleName: 'workbox-streams', + funcName: 'concatenate', + paramName: 'sourcePromises' + }); + } + + const readerPromises = sourcePromises.map(sourcePromise => { + return Promise.resolve(sourcePromise).then(source => { + return _getReaderFromSource(source); + }); + }); + const streamDeferred = new Deferred_js.Deferred(); + let i = 0; + const logMessages = []; + const stream = new ReadableStream({ + pull(controller) { + return readerPromises[i].then(reader => reader.read()).then(result => { + if (result.done) { + { + logMessages.push(['Reached the end of source:', sourcePromises[i]]); + } + + i++; + + if (i >= readerPromises.length) { + // Log all the messages in the group at once in a single group. + { + logger_js.logger.groupCollapsed(`Concatenating ${readerPromises.length} sources.`); + + for (const message of logMessages) { + if (Array.isArray(message)) { + logger_js.logger.log(...message); + } else { + logger_js.logger.log(message); + } + } + + logger_js.logger.log('Finished reading all sources.'); + logger_js.logger.groupEnd(); + } + + controller.close(); + streamDeferred.resolve(); + return; + } // The `pull` method is defined because we're inside it. + + + return this.pull(controller); + } else { + controller.enqueue(result.value); + } + }).catch(error => { + { + logger_js.logger.error('An error occurred:', error); + } + + streamDeferred.reject(error); + throw error; + }); + }, + + cancel() { + { + logger_js.logger.warn('The ReadableStream was cancelled.'); + } + + streamDeferred.resolve(); + } + + }); + return { + done: streamDeferred.promise, + stream + }; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This is a utility method that determines whether the current browser supports + * the features required to create streamed responses. Currently, it checks if + * [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream) + * is available. + * + * @private + * @param {HeadersInit} [headersInit] If there's no `Content-Type` specified, + * `'text/html'` will be used by default. + * @return {boolean} `true`, if the current browser meets the requirements for + * streaming responses, and `false` otherwise. + * + * @memberof module:workbox-streams + */ + + function createHeaders(headersInit = {}) { + // See https://github.com/GoogleChrome/workbox/issues/1461 + const headers = new Headers(headersInit); + + if (!headers.has('content-type')) { + headers.set('content-type', 'text/html'); + } + + return headers; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Takes multiple source Promises, each of which could resolve to a Response, a + * ReadableStream, or a [BodyInit](https://fetch.spec.whatwg.org/#bodyinit), + * along with a + * [HeadersInit](https://fetch.spec.whatwg.org/#typedefdef-headersinit). + * + * Returns an object exposing a Response whose body consists of each individual + * stream's data returned in sequence, along with a Promise which signals when + * the stream is finished (useful for passing to a FetchEvent's waitUntil()). + * + * @param {Array>} sourcePromises + * @param {HeadersInit} [headersInit] If there's no `Content-Type` specified, + * `'text/html'` will be used by default. + * @return {Object<{done: Promise, response: Response}>} + * + * @memberof module:workbox-streams + */ + + function concatenateToResponse(sourcePromises, headersInit) { + const { + done, + stream + } = concatenate(sourcePromises); + const headers = createHeaders(headersInit); + const response = new Response(stream, { + headers + }); + return { + done, + response + }; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This is a utility method that determines whether the current browser supports + * the features required to create streamed responses. Currently, it checks if + * [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream) + * can be created. + * + * @return {boolean} `true`, if the current browser meets the requirements for + * streaming responses, and `false` otherwise. + * + * @memberof module:workbox-streams + */ + + function isSupported() { + return canConstructReadableStream_js.canConstructReadableStream(); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A shortcut to create a strategy that could be dropped-in to Workbox's router. + * + * On browsers that do not support constructing new `ReadableStream`s, this + * strategy will automatically wait for all the `sourceFunctions` to complete, + * and create a final response that concatenates their values together. + * + * @param {Array} sourceFunctions + * An array of functions similar to {@link module:workbox-routing~handlerCallback} + * but that instead return a {@link module:workbox-streams.StreamSource} (or a + * Promise which resolves to one). + * @param {HeadersInit} [headersInit] If there's no `Content-Type` specified, + * `'text/html'` will be used by default. + * @return {module:workbox-routing~handlerCallback} + * @memberof module:workbox-streams + */ + + function strategy(sourceFunctions, headersInit) { + return async ({ + event, + request, + url, + params + }) => { + const sourcePromises = sourceFunctions.map(fn => { + // Ensure the return value of the function is always a promise. + return Promise.resolve(fn({ + event, + request, + url, + params + })); + }); + + if (isSupported()) { + const { + done, + response + } = concatenateToResponse(sourcePromises, headersInit); + + if (event) { + event.waitUntil(done); + } + + return response; + } + + { + logger_js.logger.log(`The current browser doesn't support creating response ` + `streams. Falling back to non-streaming response instead.`); + } // Fallback to waiting for everything to finish, and concatenating the + // responses. + + + const blobPartsPromises = sourcePromises.map(async sourcePromise => { + const source = await sourcePromise; + + if (source instanceof Response) { + return source.blob(); + } else { + // Technically, a `StreamSource` object can include any valid + // `BodyInit` type, including `FormData` and `URLSearchParams`, which + // cannot be passed to the Blob constructor directly, so we have to + // convert them to actual Blobs first. + return new Response(source).blob(); + } + }); + const blobParts = await Promise.all(blobPartsPromises); + const headers = createHeaders(headersInit); // Constructing a new Response from a Blob source is well-supported. + // So is constructing a new Blob from multiple source Blobs or strings. + + return new Response(new Blob(blobParts), { + headers + }); + }; + } + + exports.concatenate = concatenate; + exports.concatenateToResponse = concatenateToResponse; + exports.isSupported = isSupported; + exports.strategy = strategy; + + return exports; + +}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private)); + diff --git a/source/js/workbox/workbox-streams.prod.js b/source/js/workbox/workbox-streams.prod.js new file mode 100644 index 0000000..540dfc7 --- /dev/null +++ b/source/js/workbox/workbox-streams.prod.js @@ -0,0 +1,2 @@ +this.workbox=this.workbox||{},this.workbox.streams=function(e,n,t){"use strict";try{self["workbox:streams:5.1.4"]&&_()}catch(e){}function s(e){const t=e.map(e=>Promise.resolve(e).then(e=>function(e){return e instanceof Response?e.body.getReader():e instanceof ReadableStream?e.getReader():new Response(e).body.getReader()}(e))),s=new n.Deferred;let r=0;const o=new ReadableStream({pull(e){return t[r].then(e=>e.read()).then(n=>{if(n.done)return r++,r>=t.length?(e.close(),void s.resolve()):this.pull(e);e.enqueue(n.value)}).catch(e=>{throw s.reject(e),e})},cancel(){s.resolve()}});return{done:s.promise,stream:o}}function r(e={}){const n=new Headers(e);return n.has("content-type")||n.set("content-type","text/html"),n}function o(e,n){const{done:t,stream:o}=s(e),c=r(n);return{done:t,response:new Response(o,{headers:c})}}function c(){return t.canConstructReadableStream()}return e.concatenate=s,e.concatenateToResponse=o,e.isSupported=c,e.strategy=function(e,n){return async({event:t,request:s,url:a,params:u})=>{const i=e.map(e=>Promise.resolve(e({event:t,request:s,url:a,params:u})));if(c()){const{done:e,response:s}=o(i,n);return t&&t.waitUntil(e),s}const f=i.map(async e=>{const n=await e;return n instanceof Response?n.blob():new Response(n).blob()}),p=await Promise.all(f),w=r(n);return new Response(new Blob(p),{headers:w})}},e}({},workbox.core._private,workbox.core._private); + diff --git a/source/js/workbox/workbox-sw.js b/source/js/workbox/workbox-sw.js new file mode 100644 index 0000000..4a3f808 --- /dev/null +++ b/source/js/workbox/workbox-sw.js @@ -0,0 +1,2 @@ +!function(){"use strict";try{self["workbox:sw:5.1.4"]&&_()}catch(t){}const t={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(e,s){if(e[s])return e[s];const o=t[s];return o&&e.loadModule("workbox-"+o),e[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(t){if(this.t.modulePathCb)return this.t.modulePathCb(t,this.t.debug);let e=["https://cdn.jsdelivr.net/npm/workbox-cdn@5.1.4/workbox"];const s=`${t}.${this.s}.js`,o=this.t.modulePathPrefix;return o&&(e=o.split("/"),""===e[e.length-1]&&e.splice(e.length-1,1)),e.push(s),e.join("/")}}}(); + diff --git a/source/js/workbox/workbox-window.dev.es5.mjs b/source/js/workbox/workbox-window.dev.es5.mjs new file mode 100644 index 0000000..ce3e35f --- /dev/null +++ b/source/js/workbox/workbox-window.dev.es5.mjs @@ -0,0 +1,1125 @@ +try { + self['workbox:window:5.1.4'] && _(); +} catch (e) {} + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * Sends a data object to a service worker via `postMessage` and resolves with + * a response (if any). + * + * A response can be set in a message handler in the service worker by + * calling `event.ports[0].postMessage(...)`, which will resolve the promise + * returned by `messageSW()`. If no response is set, the promise will not + * resolve. + * + * @param {ServiceWorker} sw The service worker to send the message to. + * @param {Object} data An object to send to the service worker. + * @return {Promise} + * @memberof module:workbox-window + */ + +function messageSW(sw, data) { + return new Promise(function (resolve) { + var messageChannel = new MessageChannel(); + + messageChannel.port1.onmessage = function (event) { + resolve(event.data); + }; + + sw.postMessage(data, [messageChannel.port2]); + }); +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + subClass.__proto__ = superClass; +} + +function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); +} + +function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; +} + +function _createForOfIteratorHelperLoose(o, allowArrayLike) { + var it; + + if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { + if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { + if (it) o = it; + var i = 0; + return function () { + if (i >= o.length) return { + done: true + }; + return { + done: false, + value: o[i++] + }; + }; + } + + throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + it = o[Symbol.iterator](); + return it.next.bind(it); +} + +try { + self['workbox:core:5.1.4'] && _(); +} catch (e) {} + +/* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * The Deferred class composes Promises in a way that allows for them to be + * resolved or rejected from outside the constructor. In most cases promises + * should be used directly, but Deferreds can be necessary when the logic to + * resolve a promise must be separate. + * + * @private + */ + +var Deferred = +/** + * Creates a promise and exposes its resolve and reject functions as methods. + */ +function Deferred() { + var _this = this; + + this.promise = new Promise(function (resolve, reject) { + _this.resolve = resolve; + _this.reject = reject; + }); +}; + +/* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * A helper function that prevents a promise from being flagged as unused. + * + * @private + **/ + +function dontWaitFor(promise) { + // Effective no-op. + promise.then(function () {}); +} + +/* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +var logger = function () { + // Don't overwrite this value if it's already set. + // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 + if (!('__WB_DISABLE_DEV_LOGS' in self)) { + self.__WB_DISABLE_DEV_LOGS = false; + } + + var inGroup = false; + var methodToColorMap = { + debug: "#7f8c8d", + log: "#2ecc71", + warn: "#f39c12", + error: "#c0392b", + groupCollapsed: "#3498db", + groupEnd: null + }; + + var print = function print(method, args) { + var _console2; + + if (self.__WB_DISABLE_DEV_LOGS) { + return; + } + + if (method === 'groupCollapsed') { + // Safari doesn't print all console.groupCollapsed() arguments: + // https://bugs.webkit.org/show_bug.cgi?id=182754 + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + var _console; + + (_console = console)[method].apply(_console, args); + + return; + } + } + + var styles = ["background: " + methodToColorMap[method], "border-radius: 0.5em", "color: white", "font-weight: bold", "padding: 2px 0.5em"]; // When in a group, the workbox prefix is not displayed. + + var logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; + + (_console2 = console)[method].apply(_console2, logPrefix.concat(args)); + + if (method === 'groupCollapsed') { + inGroup = true; + } + + if (method === 'groupEnd') { + inGroup = false; + } + }; + + var api = {}; + var loggerMethods = Object.keys(methodToColorMap); + + var _loop = function _loop() { + var key = _loggerMethods[_i]; + var method = key; + + api[method] = function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + print(method, args); + }; + }; + + for (var _i = 0, _loggerMethods = loggerMethods; _i < _loggerMethods.length; _i++) { + _loop(); + } + + return api; +}(); + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +/** + * A minimal `EventTarget` shim. + * This is necessary because not all browsers support constructable + * `EventTarget`, so using a real `EventTarget` will error. + * @private + */ +var WorkboxEventTarget = /*#__PURE__*/function () { + function WorkboxEventTarget() { + this._eventListenerRegistry = new Map(); + } + /** + * @param {string} type + * @param {Function} listener + * @private + */ + + + var _proto = WorkboxEventTarget.prototype; + + _proto.addEventListener = function addEventListener(type, listener) { + var foo = this._getEventListenersByType(type); + + foo.add(listener); + } + /** + * @param {string} type + * @param {Function} listener + * @private + */ + ; + + _proto.removeEventListener = function removeEventListener(type, listener) { + this._getEventListenersByType(type).delete(listener); + } + /** + * @param {Object} event + * @private + */ + ; + + _proto.dispatchEvent = function dispatchEvent(event) { + event.target = this; + + var listeners = this._getEventListenersByType(event.type); + + for (var _iterator = _createForOfIteratorHelperLoose(listeners), _step; !(_step = _iterator()).done;) { + var listener = _step.value; + listener(event); + } + } + /** + * Returns a Set of listeners associated with the passed event type. + * If no handlers have been registered, an empty Set is returned. + * + * @param {string} type The event type. + * @return {Set} An array of handler functions. + * @private + */ + ; + + _proto._getEventListenersByType = function _getEventListenersByType(type) { + if (!this._eventListenerRegistry.has(type)) { + this._eventListenerRegistry.set(type, new Set()); + } + + return this._eventListenerRegistry.get(type); + }; + + return WorkboxEventTarget; +}(); + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * Returns true if two URLs have the same `.href` property. The URLS can be + * relative, and if they are the current location href is used to resolve URLs. + * + * @private + * @param {string} url1 + * @param {string} url2 + * @return {boolean} + */ + +function urlsMatch(url1, url2) { + var _location = location, + href = _location.href; + return new URL(url1, href).href === new URL(url2, href).href; +} + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * A minimal `Event` subclass shim. + * This doesn't *actually* subclass `Event` because not all browsers support + * constructable `EventTarget`, and using a real `Event` will error. + * @private + */ + +var WorkboxEvent = function WorkboxEvent(type, props) { + this.type = type; + Object.assign(this, props); +}; + +// `skipWaiting()` wasn't called. This 200 amount wasn't scientifically +// chosen, but it seems to avoid false positives in my testing. + +function _await(value, then, direct) { + if (direct) { + return then ? then(value) : value; + } + + if (!value || !value.then) { + value = Promise.resolve(value); + } + + return then ? value.then(then) : value; +} + +var WAITING_TIMEOUT_DURATION = 200; // The amount of time after a registration that we can reasonably conclude +// that the registration didn't trigger an update. + +function _async(f) { + return function () { + for (var args = [], i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + try { + return Promise.resolve(f.apply(this, args)); + } catch (e) { + return Promise.reject(e); + } + }; +} + +var REGISTRATION_TIMEOUT_DURATION = 60000; +/** + * A class to aid in handling service worker registration, updates, and + * reacting to service worker lifecycle events. + * + * @fires [message]{@link module:workbox-window.Workbox#message} + * @fires [installed]{@link module:workbox-window.Workbox#installed} + * @fires [waiting]{@link module:workbox-window.Workbox#waiting} + * @fires [controlling]{@link module:workbox-window.Workbox#controlling} + * @fires [activated]{@link module:workbox-window.Workbox#activated} + * @fires [redundant]{@link module:workbox-window.Workbox#redundant} + * @fires [externalinstalled]{@link module:workbox-window.Workbox#externalinstalled} + * @fires [externalwaiting]{@link module:workbox-window.Workbox#externalwaiting} + * @fires [externalactivated]{@link module:workbox-window.Workbox#externalactivated} + * @memberof module:workbox-window + */ + +function _empty() {} + +var Workbox = /*#__PURE__*/function (_WorkboxEventTarget) { + _inheritsLoose(Workbox, _WorkboxEventTarget); + + /** + * Creates a new Workbox instance with a script URL and service worker + * options. The script URL and options are the same as those used when + * calling `navigator.serviceWorker.register(scriptURL, options)`. See: + * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register + * + * @param {string} scriptURL The service worker script associated with this + * instance. + * @param {Object} [registerOptions] The service worker options associated + * with this instance. + */ + function Workbox(scriptURL, registerOptions) { + var _this; + + if (registerOptions === void 0) { + registerOptions = {}; + } + + _this = _WorkboxEventTarget.call(this) || this; + _this._registerOptions = {}; + _this._updateFoundCount = 0; // Deferreds we can resolve later. + + _this._swDeferred = new Deferred(); + _this._activeDeferred = new Deferred(); + _this._controllingDeferred = new Deferred(); + _this._registrationTime = 0; + _this._ownSWs = new Set(); + /** + * @private + */ + + _this._onUpdateFound = function () { + // `this._registration` will never be `undefined` after an update is found. + var registration = _this._registration; + var installingSW = registration.installing; // If the script URL passed to `navigator.serviceWorker.register()` is + // different from the current controlling SW's script URL, we know any + // successful registration calls will trigger an `updatefound` event. + // But if the registered script URL is the same as the current controlling + // SW's script URL, we'll only get an `updatefound` event if the file + // changed since it was last registered. This can be a problem if the user + // opens up the same page in a different tab, and that page registers + // a SW that triggers an update. It's a problem because this page has no + // good way of knowing whether the `updatefound` event came from the SW + // script it registered or from a registration attempt made by a newer + // version of the page running in another tab. + // To minimize the possibility of a false positive, we use the logic here: + + var updateLikelyTriggeredExternally = // Since we enforce only calling `register()` once, and since we don't + // add the `updatefound` event listener until the `register()` call, if + // `_updateFoundCount` is > 0 then it means this method has already + // been called, thus this SW must be external + _this._updateFoundCount > 0 || // If the script URL of the installing SW is different from this + // instance's script URL, we know it's definitely not from our + // registration. + !urlsMatch(installingSW.scriptURL, _this._scriptURL) || // If all of the above are false, then we use a time-based heuristic: + // Any `updatefound` event that occurs long after our registration is + // assumed to be external. + performance.now() > _this._registrationTime + REGISTRATION_TIMEOUT_DURATION ? // If any of the above are not true, we assume the update was + // triggered by this instance. + true : false; + + if (updateLikelyTriggeredExternally) { + _this._externalSW = installingSW; + registration.removeEventListener('updatefound', _this._onUpdateFound); + } else { + // If the update was not triggered externally we know the installing + // SW is the one we registered, so we set it. + _this._sw = installingSW; + + _this._ownSWs.add(installingSW); + + _this._swDeferred.resolve(installingSW); // The `installing` state isn't something we have a dedicated + // callback for, but we do log messages for it in development. + + + { + if (navigator.serviceWorker.controller) { + logger.log('Updated service worker found. Installing now...'); + } else { + logger.log('Service worker is installing...'); + } + } + } // Increment the `updatefound` count, so future invocations of this + // method can be sure they were triggered externally. + + + ++_this._updateFoundCount; // Add a `statechange` listener regardless of whether this update was + // triggered externally, since we have callbacks for both. + + installingSW.addEventListener('statechange', _this._onStateChange); + }; + /** + * @private + * @param {Event} originalEvent + */ + + + _this._onStateChange = function (originalEvent) { + // `this._registration` will never be `undefined` after an update is found. + var registration = _this._registration; + var sw = originalEvent.target; + var state = sw.state; + var isExternal = sw === _this._externalSW; + var eventPrefix = isExternal ? 'external' : ''; + var eventProps = { + sw: sw, + originalEvent: originalEvent + }; + + if (!isExternal && _this._isUpdate) { + eventProps.isUpdate = true; + } + + _this.dispatchEvent(new WorkboxEvent(eventPrefix + state, eventProps)); + + if (state === 'installed') { + // This timeout is used to ignore cases where the service worker calls + // `skipWaiting()` in the install event, thus moving it directly in the + // activating state. (Since all service workers *must* go through the + // waiting phase, the only way to detect `skipWaiting()` called in the + // install event is to observe that the time spent in the waiting phase + // is very short.) + // NOTE: we don't need separate timeouts for the own and external SWs + // since they can't go through these phases at the same time. + _this._waitingTimeout = self.setTimeout(function () { + // Ensure the SW is still waiting (it may now be redundant). + if (state === 'installed' && registration.waiting === sw) { + _this.dispatchEvent(new WorkboxEvent(eventPrefix + 'waiting', eventProps)); + + { + if (isExternal) { + logger.warn('An external service worker has installed but is ' + 'waiting for this client to close before activating...'); + } else { + logger.warn('The service worker has installed but is waiting ' + 'for existing clients to close before activating...'); + } + } + } + }, WAITING_TIMEOUT_DURATION); + } else if (state === 'activating') { + clearTimeout(_this._waitingTimeout); + + if (!isExternal) { + _this._activeDeferred.resolve(sw); + } + } + + { + switch (state) { + case 'installed': + if (isExternal) { + logger.warn('An external service worker has installed. ' + 'You may want to suggest users reload this page.'); + } else { + logger.log('Registered service worker installed.'); + } + + break; + + case 'activated': + if (isExternal) { + logger.warn('An external service worker has activated.'); + } else { + logger.log('Registered service worker activated.'); + + if (sw !== navigator.serviceWorker.controller) { + logger.warn('The registered service worker is active but ' + 'not yet controlling the page. Reload or run ' + '`clients.claim()` in the service worker.'); + } + } + + break; + + case 'redundant': + if (sw === _this._compatibleControllingSW) { + logger.log('Previously controlling service worker now redundant!'); + } else if (!isExternal) { + logger.log('Registered service worker now redundant!'); + } + + break; + } + } + }; + /** + * @private + * @param {Event} originalEvent + */ + + + _this._onControllerChange = function (originalEvent) { + var sw = _this._sw; + + if (sw === navigator.serviceWorker.controller) { + _this.dispatchEvent(new WorkboxEvent('controlling', { + sw: sw, + originalEvent: originalEvent, + isUpdate: _this._isUpdate + })); + + { + logger.log('Registered service worker now controlling this page.'); + } + + _this._controllingDeferred.resolve(sw); + } + }; + /** + * @private + * @param {Event} originalEvent + */ + + + _this._onMessage = _async(function (originalEvent) { + var data = originalEvent.data, + source = originalEvent.source; // Wait until there's an "own" service worker. This is used to buffer + // `message` events that may be received prior to calling `register()`. + + return _await(_this.getSW(), function () { + if (_this._ownSWs.has(source)) { + _this.dispatchEvent(new WorkboxEvent('message', { + data: data, + sw: source, + originalEvent: originalEvent + })); + } + }); // If the service worker that sent the message is in the list of own + // service workers for this instance, dispatch a `message` event. + // NOTE: we check for all previously owned service workers rather than + // just the current one because some messages (e.g. cache updates) use + // a timeout when sent and may be delayed long enough for a service worker + // update to be found. + }); + _this._scriptURL = scriptURL; + _this._registerOptions = registerOptions; // Add a message listener immediately since messages received during + // page load are buffered only until the DOMContentLoaded event: + // https://github.com/GoogleChrome/workbox/issues/2202 + + navigator.serviceWorker.addEventListener('message', _this._onMessage); + return _this; + } + /** + * Registers a service worker for this instances script URL and service + * worker options. By default this method delays registration until after + * the window has loaded. + * + * @param {Object} [options] + * @param {Function} [options.immediate=false] Setting this to true will + * register the service worker immediately, even if the window has + * not loaded (not recommended). + */ + + + var _proto = Workbox.prototype; + + _proto.register = function register(_temp) { + var _ref = _temp === void 0 ? {} : _temp, + _ref$immediate = _ref.immediate, + immediate = _ref$immediate === void 0 ? false : _ref$immediate; + + try { + var _this3 = this; + + if ("dev" !== 'production') { + if (_this3._registrationTime) { + logger.error('Cannot re-register a Workbox instance after it has ' + 'been registered. Create a new instance instead.'); + return; + } + } + + return _invoke(function () { + if (!immediate && document.readyState !== 'complete') { + return _awaitIgnored(new Promise(function (res) { + return window.addEventListener('load', res); + })); + } + }, function () { + // Set this flag to true if any service worker was controlling the page + // at registration time. + _this3._isUpdate = Boolean(navigator.serviceWorker.controller); // Before registering, attempt to determine if a SW is already controlling + // the page, and if that SW script (and version, if specified) matches this + // instance's script. + + _this3._compatibleControllingSW = _this3._getControllingSWIfCompatible(); + return _await(_this3._registerScript(), function (_this2$_registerScrip) { + _this3._registration = _this2$_registerScrip; + + // If we have a compatible controller, store the controller as the "own" + // SW, resolve active/controlling deferreds and add necessary listeners. + if (_this3._compatibleControllingSW) { + _this3._sw = _this3._compatibleControllingSW; + + _this3._activeDeferred.resolve(_this3._compatibleControllingSW); + + _this3._controllingDeferred.resolve(_this3._compatibleControllingSW); + + _this3._compatibleControllingSW.addEventListener('statechange', _this3._onStateChange, { + once: true + }); + } // If there's a waiting service worker with a matching URL before the + // `updatefound` event fires, it likely means that this site is open + // in another tab, or the user refreshed the page (and thus the previous + // page wasn't fully unloaded before this page started loading). + // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting + + + var waitingSW = _this3._registration.waiting; + + if (waitingSW && urlsMatch(waitingSW.scriptURL, _this3._scriptURL)) { + // Store the waiting SW as the "own" Sw, even if it means overwriting + // a compatible controller. + _this3._sw = waitingSW; // Run this in the next microtask, so any code that adds an event + // listener after awaiting `register()` will get this event. + + dontWaitFor(Promise.resolve().then(function () { + _this3.dispatchEvent(new WorkboxEvent('waiting', { + sw: waitingSW, + wasWaitingBeforeRegister: true + })); + + if ("dev" !== 'production') { + logger.warn('A service worker was already waiting to activate ' + 'before this script was registered...'); + } + })); + } // If an "own" SW is already set, resolve the deferred. + + + if (_this3._sw) { + _this3._swDeferred.resolve(_this3._sw); + + _this3._ownSWs.add(_this3._sw); + } + + if ("dev" !== 'production') { + logger.log('Successfully registered service worker.', _this3._scriptURL); + + if (navigator.serviceWorker.controller) { + if (_this3._compatibleControllingSW) { + logger.debug('A service worker with the same script URL ' + 'is already controlling this page.'); + } else { + logger.debug('A service worker with a different script URL is ' + 'currently controlling the page. The browser is now fetching ' + 'the new script now...'); + } + } + + var currentPageIsOutOfScope = function currentPageIsOutOfScope() { + var scopeURL = new URL(_this3._registerOptions.scope || _this3._scriptURL, document.baseURI); + var scopeURLBasePath = new URL('./', scopeURL.href).pathname; + return !location.pathname.startsWith(scopeURLBasePath); + }; + + if (currentPageIsOutOfScope()) { + logger.warn('The current page is not in scope for the registered ' + 'service worker. Was this a mistake?'); + } + } + + _this3._registration.addEventListener('updatefound', _this3._onUpdateFound); + + navigator.serviceWorker.addEventListener('controllerchange', _this3._onControllerChange, { + once: true + }); + return _this3._registration; + }); + }); + } catch (e) { + return Promise.reject(e); + } + } + /** + * Checks for updates of the registered service worker. + */ + ; + + _proto.update = function update() { + try { + var _this5 = this; + + if (!_this5._registration) { + if ("dev" !== 'production') { + logger.error('Cannot update a Workbox instance without ' + 'being registered. Register the Workbox instance first.'); + } + + return; + } // Try to update registration + + + return _awaitIgnored(_this5._registration.update()); + } catch (e) { + return Promise.reject(e); + } + } + /** + * Resolves to the service worker registered by this instance as soon as it + * is active. If a service worker was already controlling at registration + * time then it will resolve to that if the script URLs (and optionally + * script versions) match, otherwise it will wait until an update is found + * and activates. + * + * @return {Promise} + */ + ; + + /** + * Resolves with a reference to a service worker that matches the script URL + * of this instance, as soon as it's available. + * + * If, at registration time, there's already an active or waiting service + * worker with a matching script URL, it will be used (with the waiting + * service worker taking precedence over the active service worker if both + * match, since the waiting service worker would have been registered more + * recently). + * If there's no matching active or waiting service worker at registration + * time then the promise will not resolve until an update is found and starts + * installing, at which point the installing service worker is used. + * + * @return {Promise} + */ + _proto.getSW = function getSW() { + try { + var _this7 = this; + + // If `this._sw` is set, resolve with that as we want `getSW()` to + // return the correct (new) service worker if an update is found. + return _this7._sw !== undefined ? _this7._sw : _this7._swDeferred.promise; + } catch (e) { + return Promise.reject(e); + } + } + /** + * Sends the passed data object to the service worker registered by this + * instance (via [`getSW()`]{@link module:workbox-window.Workbox#getSW}) and resolves + * with a response (if any). + * + * A response can be set in a message handler in the service worker by + * calling `event.ports[0].postMessage(...)`, which will resolve the promise + * returned by `messageSW()`. If no response is set, the promise will never + * resolve. + * + * @param {Object} data An object to send to the service worker + * @return {Promise} + */ + ; + + _proto.messageSW = function messageSW$1(data) { + try { + var _this9 = this; + + return _await(_this9.getSW(), function (sw) { + return messageSW(sw, data); + }); + } catch (e) { + return Promise.reject(e); + } + } + /** + * Checks for a service worker already controlling the page and returns + * it if its script URL matches. + * + * @private + * @return {ServiceWorker|undefined} + */ + ; + + _proto._getControllingSWIfCompatible = function _getControllingSWIfCompatible() { + var controller = navigator.serviceWorker.controller; + + if (controller && urlsMatch(controller.scriptURL, this._scriptURL)) { + return controller; + } else { + return undefined; + } + } + /** + * Registers a service worker for this instances script URL and register + * options and tracks the time registration was complete. + * + * @private + */ + ; + + _proto._registerScript = function _registerScript() { + try { + var _this11 = this; + + return _catch(function () { + return _await(navigator.serviceWorker.register(_this11._scriptURL, _this11._registerOptions), function (reg) { + // Keep track of when registration happened, so it can be used in the + // `this._onUpdateFound` heuristic. Also use the presence of this + // property as a way to see if `.register()` has been called. + _this11._registrationTime = performance.now(); + return reg; + }); + }, function (error) { + if ("dev" !== 'production') { + logger.error(error); + } // Re-throw the error. + + + throw error; + }); + } catch (e) { + return Promise.reject(e); + } + }; + + _createClass(Workbox, [{ + key: "active", + get: function get() { + return this._activeDeferred.promise; + } + /** + * Resolves to the service worker registered by this instance as soon as it + * is controlling the page. If a service worker was already controlling at + * registration time then it will resolve to that if the script URLs (and + * optionally script versions) match, otherwise it will wait until an update + * is found and starts controlling the page. + * Note: the first time a service worker is installed it will active but + * not start controlling the page unless `clients.claim()` is called in the + * service worker. + * + * @return {Promise} + */ + + }, { + key: "controlling", + get: function get() { + return this._controllingDeferred.promise; + } + }]); + + return Workbox; +}(WorkboxEventTarget); + +function _awaitIgnored(value, direct) { + if (!direct) { + return value && value.then ? value.then(_empty) : Promise.resolve(); + } +} // The jsdoc comments below outline the events this instance may dispatch: +// ----------------------------------------------------------------------- + +/** + * The `message` event is dispatched any time a `postMessage` is received. + * + * @event module:workbox-window.Workbox#message + * @type {WorkboxEvent} + * @property {*} data The `data` property from the original `message` event. + * @property {Event} originalEvent The original [`message`]{@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} + * event. + * @property {string} type `message`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `installed` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `installed`. + * + * Then can happen either the very first time a service worker is installed, + * or after an update to the current service worker is found. In the case + * of an update being found, the event's `isUpdate` property will be `true`. + * + * @event module:workbox-window.Workbox#installed + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `installed`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `waiting` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `installed` and then doesn't immediately change to `activating`. + * It may also be dispatched if a service worker with the same + * [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL} + * was already waiting when the [`register()`]{@link module:workbox-window.Workbox#register} + * method was called. + * + * @event module:workbox-window.Workbox#waiting + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event|undefined} originalEvent The original + * [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event, or `undefined` in the case where the service worker was waiting + * to before `.register()` was called. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {boolean|undefined} wasWaitingBeforeRegister True if a service worker with + * a matching `scriptURL` was already waiting when this `Workbox` + * instance called `register()`. + * @property {string} type `waiting`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `controlling` event is dispatched if a + * [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange} + * fires on the service worker [container]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer} + * and the [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL} + * of the new [controller]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/controller} + * matches the `scriptURL` of the `Workbox` instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}. + * + * @event module:workbox-window.Workbox#controlling + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this service worker was registered. + * @property {string} type `controlling`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `activated` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `activated`. + * + * @event module:workbox-window.Workbox#activated + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `activated`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `redundant` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `redundant`. + * + * @event module:workbox-window.Workbox#redundant + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `redundant`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `externalinstalled` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `installed`. + * + * @event module:workbox-window.Workbox#externalinstalled + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalinstalled`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `externalwaiting` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `waiting`. + * + * @event module:workbox-window.Workbox#externalwaiting + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalwaiting`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `externalactivated` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `activated`. + * + * @event module:workbox-window.Workbox#externalactivated + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalactivated`. + * @property {Workbox} target The `Workbox` instance. + */ + + +function _invoke(body, then) { + var result = body(); + + if (result && result.then) { + return result.then(then); + } + + return then(result); +} + +function _catch(body, recover) { + try { + var result = body(); + } catch (e) { + return recover(e); + } + + if (result && result.then) { + return result.then(void 0, recover); + } + + return result; +} + +export { Workbox, messageSW }; + diff --git a/source/js/workbox/workbox-window.dev.mjs b/source/js/workbox/workbox-window.dev.mjs new file mode 100644 index 0000000..042a230 --- /dev/null +++ b/source/js/workbox/workbox-window.dev.mjs @@ -0,0 +1,943 @@ +try { + self['workbox:window:5.1.4'] && _(); +} catch (e) {} + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * Sends a data object to a service worker via `postMessage` and resolves with + * a response (if any). + * + * A response can be set in a message handler in the service worker by + * calling `event.ports[0].postMessage(...)`, which will resolve the promise + * returned by `messageSW()`. If no response is set, the promise will not + * resolve. + * + * @param {ServiceWorker} sw The service worker to send the message to. + * @param {Object} data An object to send to the service worker. + * @return {Promise} + * @memberof module:workbox-window + */ + +function messageSW(sw, data) { + return new Promise(resolve => { + const messageChannel = new MessageChannel(); + + messageChannel.port1.onmessage = event => { + resolve(event.data); + }; + + sw.postMessage(data, [messageChannel.port2]); + }); +} + +try { + self['workbox:core:5.1.4'] && _(); +} catch (e) {} + +/* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * The Deferred class composes Promises in a way that allows for them to be + * resolved or rejected from outside the constructor. In most cases promises + * should be used directly, but Deferreds can be necessary when the logic to + * resolve a promise must be separate. + * + * @private + */ + +class Deferred { + /** + * Creates a promise and exposes its resolve and reject functions as methods. + */ + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + +} + +/* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * A helper function that prevents a promise from being flagged as unused. + * + * @private + **/ + +function dontWaitFor(promise) { + // Effective no-op. + promise.then(() => {}); +} + +/* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +const logger = (() => { + // Don't overwrite this value if it's already set. + // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 + if (!('__WB_DISABLE_DEV_LOGS' in self)) { + self.__WB_DISABLE_DEV_LOGS = false; + } + + let inGroup = false; + const methodToColorMap = { + debug: `#7f8c8d`, + log: `#2ecc71`, + warn: `#f39c12`, + error: `#c0392b`, + groupCollapsed: `#3498db`, + groupEnd: null + }; + + const print = function (method, args) { + if (self.__WB_DISABLE_DEV_LOGS) { + return; + } + + if (method === 'groupCollapsed') { + // Safari doesn't print all console.groupCollapsed() arguments: + // https://bugs.webkit.org/show_bug.cgi?id=182754 + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + console[method](...args); + return; + } + } + + const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; // When in a group, the workbox prefix is not displayed. + + const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; + console[method](...logPrefix, ...args); + + if (method === 'groupCollapsed') { + inGroup = true; + } + + if (method === 'groupEnd') { + inGroup = false; + } + }; + + const api = {}; + const loggerMethods = Object.keys(methodToColorMap); + + for (const key of loggerMethods) { + const method = key; + + api[method] = (...args) => { + print(method, args); + }; + } + + return api; +})(); + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +/** + * A minimal `EventTarget` shim. + * This is necessary because not all browsers support constructable + * `EventTarget`, so using a real `EventTarget` will error. + * @private + */ +class WorkboxEventTarget { + constructor() { + this._eventListenerRegistry = new Map(); + } + /** + * @param {string} type + * @param {Function} listener + * @private + */ + + + addEventListener(type, listener) { + const foo = this._getEventListenersByType(type); + + foo.add(listener); + } + /** + * @param {string} type + * @param {Function} listener + * @private + */ + + + removeEventListener(type, listener) { + this._getEventListenersByType(type).delete(listener); + } + /** + * @param {Object} event + * @private + */ + + + dispatchEvent(event) { + event.target = this; + + const listeners = this._getEventListenersByType(event.type); + + for (const listener of listeners) { + listener(event); + } + } + /** + * Returns a Set of listeners associated with the passed event type. + * If no handlers have been registered, an empty Set is returned. + * + * @param {string} type The event type. + * @return {Set} An array of handler functions. + * @private + */ + + + _getEventListenersByType(type) { + if (!this._eventListenerRegistry.has(type)) { + this._eventListenerRegistry.set(type, new Set()); + } + + return this._eventListenerRegistry.get(type); + } + +} + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * Returns true if two URLs have the same `.href` property. The URLS can be + * relative, and if they are the current location href is used to resolve URLs. + * + * @private + * @param {string} url1 + * @param {string} url2 + * @return {boolean} + */ + +function urlsMatch(url1, url2) { + const { + href + } = location; + return new URL(url1, href).href === new URL(url2, href).href; +} + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +/** + * A minimal `Event` subclass shim. + * This doesn't *actually* subclass `Event` because not all browsers support + * constructable `EventTarget`, and using a real `Event` will error. + * @private + */ + +class WorkboxEvent { + constructor(type, props) { + this.type = type; + Object.assign(this, props); + } + +} + +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ +// `skipWaiting()` wasn't called. This 200 amount wasn't scientifically +// chosen, but it seems to avoid false positives in my testing. + +const WAITING_TIMEOUT_DURATION = 200; // The amount of time after a registration that we can reasonably conclude +// that the registration didn't trigger an update. + +const REGISTRATION_TIMEOUT_DURATION = 60000; +/** + * A class to aid in handling service worker registration, updates, and + * reacting to service worker lifecycle events. + * + * @fires [message]{@link module:workbox-window.Workbox#message} + * @fires [installed]{@link module:workbox-window.Workbox#installed} + * @fires [waiting]{@link module:workbox-window.Workbox#waiting} + * @fires [controlling]{@link module:workbox-window.Workbox#controlling} + * @fires [activated]{@link module:workbox-window.Workbox#activated} + * @fires [redundant]{@link module:workbox-window.Workbox#redundant} + * @fires [externalinstalled]{@link module:workbox-window.Workbox#externalinstalled} + * @fires [externalwaiting]{@link module:workbox-window.Workbox#externalwaiting} + * @fires [externalactivated]{@link module:workbox-window.Workbox#externalactivated} + * @memberof module:workbox-window + */ + +class Workbox extends WorkboxEventTarget { + /** + * Creates a new Workbox instance with a script URL and service worker + * options. The script URL and options are the same as those used when + * calling `navigator.serviceWorker.register(scriptURL, options)`. See: + * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register + * + * @param {string} scriptURL The service worker script associated with this + * instance. + * @param {Object} [registerOptions] The service worker options associated + * with this instance. + */ + constructor(scriptURL, registerOptions = {}) { + super(); + this._registerOptions = {}; + this._updateFoundCount = 0; // Deferreds we can resolve later. + + this._swDeferred = new Deferred(); + this._activeDeferred = new Deferred(); + this._controllingDeferred = new Deferred(); + this._registrationTime = 0; + this._ownSWs = new Set(); + /** + * @private + */ + + this._onUpdateFound = () => { + // `this._registration` will never be `undefined` after an update is found. + const registration = this._registration; + const installingSW = registration.installing; // If the script URL passed to `navigator.serviceWorker.register()` is + // different from the current controlling SW's script URL, we know any + // successful registration calls will trigger an `updatefound` event. + // But if the registered script URL is the same as the current controlling + // SW's script URL, we'll only get an `updatefound` event if the file + // changed since it was last registered. This can be a problem if the user + // opens up the same page in a different tab, and that page registers + // a SW that triggers an update. It's a problem because this page has no + // good way of knowing whether the `updatefound` event came from the SW + // script it registered or from a registration attempt made by a newer + // version of the page running in another tab. + // To minimize the possibility of a false positive, we use the logic here: + + const updateLikelyTriggeredExternally = // Since we enforce only calling `register()` once, and since we don't + // add the `updatefound` event listener until the `register()` call, if + // `_updateFoundCount` is > 0 then it means this method has already + // been called, thus this SW must be external + this._updateFoundCount > 0 || // If the script URL of the installing SW is different from this + // instance's script URL, we know it's definitely not from our + // registration. + !urlsMatch(installingSW.scriptURL, this._scriptURL) || // If all of the above are false, then we use a time-based heuristic: + // Any `updatefound` event that occurs long after our registration is + // assumed to be external. + performance.now() > this._registrationTime + REGISTRATION_TIMEOUT_DURATION ? // If any of the above are not true, we assume the update was + // triggered by this instance. + true : false; + + if (updateLikelyTriggeredExternally) { + this._externalSW = installingSW; + registration.removeEventListener('updatefound', this._onUpdateFound); + } else { + // If the update was not triggered externally we know the installing + // SW is the one we registered, so we set it. + this._sw = installingSW; + + this._ownSWs.add(installingSW); + + this._swDeferred.resolve(installingSW); // The `installing` state isn't something we have a dedicated + // callback for, but we do log messages for it in development. + + + { + if (navigator.serviceWorker.controller) { + logger.log('Updated service worker found. Installing now...'); + } else { + logger.log('Service worker is installing...'); + } + } + } // Increment the `updatefound` count, so future invocations of this + // method can be sure they were triggered externally. + + + ++this._updateFoundCount; // Add a `statechange` listener regardless of whether this update was + // triggered externally, since we have callbacks for both. + + installingSW.addEventListener('statechange', this._onStateChange); + }; + /** + * @private + * @param {Event} originalEvent + */ + + + this._onStateChange = originalEvent => { + // `this._registration` will never be `undefined` after an update is found. + const registration = this._registration; + const sw = originalEvent.target; + const { + state + } = sw; + const isExternal = sw === this._externalSW; + const eventPrefix = isExternal ? 'external' : ''; + const eventProps = { + sw, + originalEvent + }; + + if (!isExternal && this._isUpdate) { + eventProps.isUpdate = true; + } + + this.dispatchEvent(new WorkboxEvent(eventPrefix + state, eventProps)); + + if (state === 'installed') { + // This timeout is used to ignore cases where the service worker calls + // `skipWaiting()` in the install event, thus moving it directly in the + // activating state. (Since all service workers *must* go through the + // waiting phase, the only way to detect `skipWaiting()` called in the + // install event is to observe that the time spent in the waiting phase + // is very short.) + // NOTE: we don't need separate timeouts for the own and external SWs + // since they can't go through these phases at the same time. + this._waitingTimeout = self.setTimeout(() => { + // Ensure the SW is still waiting (it may now be redundant). + if (state === 'installed' && registration.waiting === sw) { + this.dispatchEvent(new WorkboxEvent(eventPrefix + 'waiting', eventProps)); + + { + if (isExternal) { + logger.warn('An external service worker has installed but is ' + 'waiting for this client to close before activating...'); + } else { + logger.warn('The service worker has installed but is waiting ' + 'for existing clients to close before activating...'); + } + } + } + }, WAITING_TIMEOUT_DURATION); + } else if (state === 'activating') { + clearTimeout(this._waitingTimeout); + + if (!isExternal) { + this._activeDeferred.resolve(sw); + } + } + + { + switch (state) { + case 'installed': + if (isExternal) { + logger.warn('An external service worker has installed. ' + 'You may want to suggest users reload this page.'); + } else { + logger.log('Registered service worker installed.'); + } + + break; + + case 'activated': + if (isExternal) { + logger.warn('An external service worker has activated.'); + } else { + logger.log('Registered service worker activated.'); + + if (sw !== navigator.serviceWorker.controller) { + logger.warn('The registered service worker is active but ' + 'not yet controlling the page. Reload or run ' + '`clients.claim()` in the service worker.'); + } + } + + break; + + case 'redundant': + if (sw === this._compatibleControllingSW) { + logger.log('Previously controlling service worker now redundant!'); + } else if (!isExternal) { + logger.log('Registered service worker now redundant!'); + } + + break; + } + } + }; + /** + * @private + * @param {Event} originalEvent + */ + + + this._onControllerChange = originalEvent => { + const sw = this._sw; + + if (sw === navigator.serviceWorker.controller) { + this.dispatchEvent(new WorkboxEvent('controlling', { + sw, + originalEvent, + isUpdate: this._isUpdate + })); + + { + logger.log('Registered service worker now controlling this page.'); + } + + this._controllingDeferred.resolve(sw); + } + }; + /** + * @private + * @param {Event} originalEvent + */ + + + this._onMessage = async originalEvent => { + const { + data, + source + } = originalEvent; // Wait until there's an "own" service worker. This is used to buffer + // `message` events that may be received prior to calling `register()`. + + await this.getSW(); // If the service worker that sent the message is in the list of own + // service workers for this instance, dispatch a `message` event. + // NOTE: we check for all previously owned service workers rather than + // just the current one because some messages (e.g. cache updates) use + // a timeout when sent and may be delayed long enough for a service worker + // update to be found. + + if (this._ownSWs.has(source)) { + this.dispatchEvent(new WorkboxEvent('message', { + data, + sw: source, + originalEvent + })); + } + }; + + this._scriptURL = scriptURL; + this._registerOptions = registerOptions; // Add a message listener immediately since messages received during + // page load are buffered only until the DOMContentLoaded event: + // https://github.com/GoogleChrome/workbox/issues/2202 + + navigator.serviceWorker.addEventListener('message', this._onMessage); + } + /** + * Registers a service worker for this instances script URL and service + * worker options. By default this method delays registration until after + * the window has loaded. + * + * @param {Object} [options] + * @param {Function} [options.immediate=false] Setting this to true will + * register the service worker immediately, even if the window has + * not loaded (not recommended). + */ + + + async register({ + immediate = false + } = {}) { + { + if (this._registrationTime) { + logger.error('Cannot re-register a Workbox instance after it has ' + 'been registered. Create a new instance instead.'); + return; + } + } + + if (!immediate && document.readyState !== 'complete') { + await new Promise(res => window.addEventListener('load', res)); + } // Set this flag to true if any service worker was controlling the page + // at registration time. + + + this._isUpdate = Boolean(navigator.serviceWorker.controller); // Before registering, attempt to determine if a SW is already controlling + // the page, and if that SW script (and version, if specified) matches this + // instance's script. + + this._compatibleControllingSW = this._getControllingSWIfCompatible(); + this._registration = await this._registerScript(); // If we have a compatible controller, store the controller as the "own" + // SW, resolve active/controlling deferreds and add necessary listeners. + + if (this._compatibleControllingSW) { + this._sw = this._compatibleControllingSW; + + this._activeDeferred.resolve(this._compatibleControllingSW); + + this._controllingDeferred.resolve(this._compatibleControllingSW); + + this._compatibleControllingSW.addEventListener('statechange', this._onStateChange, { + once: true + }); + } // If there's a waiting service worker with a matching URL before the + // `updatefound` event fires, it likely means that this site is open + // in another tab, or the user refreshed the page (and thus the previous + // page wasn't fully unloaded before this page started loading). + // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting + + + const waitingSW = this._registration.waiting; + + if (waitingSW && urlsMatch(waitingSW.scriptURL, this._scriptURL)) { + // Store the waiting SW as the "own" Sw, even if it means overwriting + // a compatible controller. + this._sw = waitingSW; // Run this in the next microtask, so any code that adds an event + // listener after awaiting `register()` will get this event. + + dontWaitFor(Promise.resolve().then(() => { + this.dispatchEvent(new WorkboxEvent('waiting', { + sw: waitingSW, + wasWaitingBeforeRegister: true + })); + + { + logger.warn('A service worker was already waiting to activate ' + 'before this script was registered...'); + } + })); + } // If an "own" SW is already set, resolve the deferred. + + + if (this._sw) { + this._swDeferred.resolve(this._sw); + + this._ownSWs.add(this._sw); + } + + { + logger.log('Successfully registered service worker.', this._scriptURL); + + if (navigator.serviceWorker.controller) { + if (this._compatibleControllingSW) { + logger.debug('A service worker with the same script URL ' + 'is already controlling this page.'); + } else { + logger.debug('A service worker with a different script URL is ' + 'currently controlling the page. The browser is now fetching ' + 'the new script now...'); + } + } + + const currentPageIsOutOfScope = () => { + const scopeURL = new URL(this._registerOptions.scope || this._scriptURL, document.baseURI); + const scopeURLBasePath = new URL('./', scopeURL.href).pathname; + return !location.pathname.startsWith(scopeURLBasePath); + }; + + if (currentPageIsOutOfScope()) { + logger.warn('The current page is not in scope for the registered ' + 'service worker. Was this a mistake?'); + } + } + + this._registration.addEventListener('updatefound', this._onUpdateFound); + + navigator.serviceWorker.addEventListener('controllerchange', this._onControllerChange, { + once: true + }); + return this._registration; + } + /** + * Checks for updates of the registered service worker. + */ + + + async update() { + if (!this._registration) { + { + logger.error('Cannot update a Workbox instance without ' + 'being registered. Register the Workbox instance first.'); + } + + return; + } // Try to update registration + + + await this._registration.update(); + } + /** + * Resolves to the service worker registered by this instance as soon as it + * is active. If a service worker was already controlling at registration + * time then it will resolve to that if the script URLs (and optionally + * script versions) match, otherwise it will wait until an update is found + * and activates. + * + * @return {Promise} + */ + + + get active() { + return this._activeDeferred.promise; + } + /** + * Resolves to the service worker registered by this instance as soon as it + * is controlling the page. If a service worker was already controlling at + * registration time then it will resolve to that if the script URLs (and + * optionally script versions) match, otherwise it will wait until an update + * is found and starts controlling the page. + * Note: the first time a service worker is installed it will active but + * not start controlling the page unless `clients.claim()` is called in the + * service worker. + * + * @return {Promise} + */ + + + get controlling() { + return this._controllingDeferred.promise; + } + /** + * Resolves with a reference to a service worker that matches the script URL + * of this instance, as soon as it's available. + * + * If, at registration time, there's already an active or waiting service + * worker with a matching script URL, it will be used (with the waiting + * service worker taking precedence over the active service worker if both + * match, since the waiting service worker would have been registered more + * recently). + * If there's no matching active or waiting service worker at registration + * time then the promise will not resolve until an update is found and starts + * installing, at which point the installing service worker is used. + * + * @return {Promise} + */ + + + async getSW() { + // If `this._sw` is set, resolve with that as we want `getSW()` to + // return the correct (new) service worker if an update is found. + return this._sw !== undefined ? this._sw : this._swDeferred.promise; + } + /** + * Sends the passed data object to the service worker registered by this + * instance (via [`getSW()`]{@link module:workbox-window.Workbox#getSW}) and resolves + * with a response (if any). + * + * A response can be set in a message handler in the service worker by + * calling `event.ports[0].postMessage(...)`, which will resolve the promise + * returned by `messageSW()`. If no response is set, the promise will never + * resolve. + * + * @param {Object} data An object to send to the service worker + * @return {Promise} + */ + + + async messageSW(data) { + const sw = await this.getSW(); + return messageSW(sw, data); + } + /** + * Checks for a service worker already controlling the page and returns + * it if its script URL matches. + * + * @private + * @return {ServiceWorker|undefined} + */ + + + _getControllingSWIfCompatible() { + const controller = navigator.serviceWorker.controller; + + if (controller && urlsMatch(controller.scriptURL, this._scriptURL)) { + return controller; + } else { + return undefined; + } + } + /** + * Registers a service worker for this instances script URL and register + * options and tracks the time registration was complete. + * + * @private + */ + + + async _registerScript() { + try { + const reg = await navigator.serviceWorker.register(this._scriptURL, this._registerOptions); // Keep track of when registration happened, so it can be used in the + // `this._onUpdateFound` heuristic. Also use the presence of this + // property as a way to see if `.register()` has been called. + + this._registrationTime = performance.now(); + return reg; + } catch (error) { + { + logger.error(error); + } // Re-throw the error. + + + throw error; + } + } + +} +// ----------------------------------------------------------------------- + +/** + * The `message` event is dispatched any time a `postMessage` is received. + * + * @event module:workbox-window.Workbox#message + * @type {WorkboxEvent} + * @property {*} data The `data` property from the original `message` event. + * @property {Event} originalEvent The original [`message`]{@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} + * event. + * @property {string} type `message`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `installed` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `installed`. + * + * Then can happen either the very first time a service worker is installed, + * or after an update to the current service worker is found. In the case + * of an update being found, the event's `isUpdate` property will be `true`. + * + * @event module:workbox-window.Workbox#installed + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `installed`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `waiting` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `installed` and then doesn't immediately change to `activating`. + * It may also be dispatched if a service worker with the same + * [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL} + * was already waiting when the [`register()`]{@link module:workbox-window.Workbox#register} + * method was called. + * + * @event module:workbox-window.Workbox#waiting + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event|undefined} originalEvent The original + * [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event, or `undefined` in the case where the service worker was waiting + * to before `.register()` was called. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {boolean|undefined} wasWaitingBeforeRegister True if a service worker with + * a matching `scriptURL` was already waiting when this `Workbox` + * instance called `register()`. + * @property {string} type `waiting`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `controlling` event is dispatched if a + * [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange} + * fires on the service worker [container]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer} + * and the [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL} + * of the new [controller]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/controller} + * matches the `scriptURL` of the `Workbox` instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}. + * + * @event module:workbox-window.Workbox#controlling + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this service worker was registered. + * @property {string} type `controlling`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `activated` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `activated`. + * + * @event module:workbox-window.Workbox#activated + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `activated`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `redundant` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `redundant`. + * + * @event module:workbox-window.Workbox#redundant + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `redundant`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `externalinstalled` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `installed`. + * + * @event module:workbox-window.Workbox#externalinstalled + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalinstalled`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `externalwaiting` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `waiting`. + * + * @event module:workbox-window.Workbox#externalwaiting + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalwaiting`. + * @property {Workbox} target The `Workbox` instance. + */ + +/** + * The `externalactivated` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `activated`. + * + * @event module:workbox-window.Workbox#externalactivated + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalactivated`. + * @property {Workbox} target The `Workbox` instance. + */ + +export { Workbox, messageSW }; + diff --git a/source/js/workbox/workbox-window.dev.umd.js b/source/js/workbox/workbox-window.dev.umd.js new file mode 100644 index 0000000..34c2eb4 --- /dev/null +++ b/source/js/workbox/workbox-window.dev.umd.js @@ -0,0 +1,1136 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = global || self, factory(global.workbox = {})); +}(this, (function (exports) { 'use strict'; + + try { + self['workbox:window:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Sends a data object to a service worker via `postMessage` and resolves with + * a response (if any). + * + * A response can be set in a message handler in the service worker by + * calling `event.ports[0].postMessage(...)`, which will resolve the promise + * returned by `messageSW()`. If no response is set, the promise will not + * resolve. + * + * @param {ServiceWorker} sw The service worker to send the message to. + * @param {Object} data An object to send to the service worker. + * @return {Promise} + * @memberof module:workbox-window + */ + + function messageSW(sw, data) { + return new Promise(function (resolve) { + var messageChannel = new MessageChannel(); + + messageChannel.port1.onmessage = function (event) { + resolve(event.data); + }; + + sw.postMessage(data, [messageChannel.port2]); + }); + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + subClass.__proto__ = superClass; + } + + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); + } + + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; + } + + function _createForOfIteratorHelperLoose(o, allowArrayLike) { + var it; + + if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { + if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { + if (it) o = it; + var i = 0; + return function () { + if (i >= o.length) return { + done: true + }; + return { + done: false, + value: o[i++] + }; + }; + } + + throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + it = o[Symbol.iterator](); + return it.next.bind(it); + } + + try { + self['workbox:core:5.1.4'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Deferred class composes Promises in a way that allows for them to be + * resolved or rejected from outside the constructor. In most cases promises + * should be used directly, but Deferreds can be necessary when the logic to + * resolve a promise must be separate. + * + * @private + */ + + var Deferred = + /** + * Creates a promise and exposes its resolve and reject functions as methods. + */ + function Deferred() { + var _this = this; + + this.promise = new Promise(function (resolve, reject) { + _this.resolve = resolve; + _this.reject = reject; + }); + }; + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A helper function that prevents a promise from being flagged as unused. + * + * @private + **/ + + function dontWaitFor(promise) { + // Effective no-op. + promise.then(function () {}); + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + var logger = function () { + // Don't overwrite this value if it's already set. + // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 + if (!('__WB_DISABLE_DEV_LOGS' in self)) { + self.__WB_DISABLE_DEV_LOGS = false; + } + + var inGroup = false; + var methodToColorMap = { + debug: "#7f8c8d", + log: "#2ecc71", + warn: "#f39c12", + error: "#c0392b", + groupCollapsed: "#3498db", + groupEnd: null + }; + + var print = function print(method, args) { + var _console2; + + if (self.__WB_DISABLE_DEV_LOGS) { + return; + } + + if (method === 'groupCollapsed') { + // Safari doesn't print all console.groupCollapsed() arguments: + // https://bugs.webkit.org/show_bug.cgi?id=182754 + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + var _console; + + (_console = console)[method].apply(_console, args); + + return; + } + } + + var styles = ["background: " + methodToColorMap[method], "border-radius: 0.5em", "color: white", "font-weight: bold", "padding: 2px 0.5em"]; // When in a group, the workbox prefix is not displayed. + + var logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; + + (_console2 = console)[method].apply(_console2, logPrefix.concat(args)); + + if (method === 'groupCollapsed') { + inGroup = true; + } + + if (method === 'groupEnd') { + inGroup = false; + } + }; + + var api = {}; + var loggerMethods = Object.keys(methodToColorMap); + + var _loop = function _loop() { + var key = _loggerMethods[_i]; + var method = key; + + api[method] = function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + print(method, args); + }; + }; + + for (var _i = 0, _loggerMethods = loggerMethods; _i < _loggerMethods.length; _i++) { + _loop(); + } + + return api; + }(); + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + + /** + * A minimal `EventTarget` shim. + * This is necessary because not all browsers support constructable + * `EventTarget`, so using a real `EventTarget` will error. + * @private + */ + var WorkboxEventTarget = /*#__PURE__*/function () { + function WorkboxEventTarget() { + this._eventListenerRegistry = new Map(); + } + /** + * @param {string} type + * @param {Function} listener + * @private + */ + + + var _proto = WorkboxEventTarget.prototype; + + _proto.addEventListener = function addEventListener(type, listener) { + var foo = this._getEventListenersByType(type); + + foo.add(listener); + } + /** + * @param {string} type + * @param {Function} listener + * @private + */ + ; + + _proto.removeEventListener = function removeEventListener(type, listener) { + this._getEventListenersByType(type).delete(listener); + } + /** + * @param {Object} event + * @private + */ + ; + + _proto.dispatchEvent = function dispatchEvent(event) { + event.target = this; + + var listeners = this._getEventListenersByType(event.type); + + for (var _iterator = _createForOfIteratorHelperLoose(listeners), _step; !(_step = _iterator()).done;) { + var listener = _step.value; + listener(event); + } + } + /** + * Returns a Set of listeners associated with the passed event type. + * If no handlers have been registered, an empty Set is returned. + * + * @param {string} type The event type. + * @return {Set} An array of handler functions. + * @private + */ + ; + + _proto._getEventListenersByType = function _getEventListenersByType(type) { + if (!this._eventListenerRegistry.has(type)) { + this._eventListenerRegistry.set(type, new Set()); + } + + return this._eventListenerRegistry.get(type); + }; + + return WorkboxEventTarget; + }(); + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Returns true if two URLs have the same `.href` property. The URLS can be + * relative, and if they are the current location href is used to resolve URLs. + * + * @private + * @param {string} url1 + * @param {string} url2 + * @return {boolean} + */ + + function urlsMatch(url1, url2) { + var _location = location, + href = _location.href; + return new URL(url1, href).href === new URL(url2, href).href; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A minimal `Event` subclass shim. + * This doesn't *actually* subclass `Event` because not all browsers support + * constructable `EventTarget`, and using a real `Event` will error. + * @private + */ + + var WorkboxEvent = function WorkboxEvent(type, props) { + this.type = type; + Object.assign(this, props); + }; + + // `skipWaiting()` wasn't called. This 200 amount wasn't scientifically + // chosen, but it seems to avoid false positives in my testing. + + function _await(value, then, direct) { + if (direct) { + return then ? then(value) : value; + } + + if (!value || !value.then) { + value = Promise.resolve(value); + } + + return then ? value.then(then) : value; + } + + var WAITING_TIMEOUT_DURATION = 200; // The amount of time after a registration that we can reasonably conclude + // that the registration didn't trigger an update. + + function _async(f) { + return function () { + for (var args = [], i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + try { + return Promise.resolve(f.apply(this, args)); + } catch (e) { + return Promise.reject(e); + } + }; + } + + var REGISTRATION_TIMEOUT_DURATION = 60000; + /** + * A class to aid in handling service worker registration, updates, and + * reacting to service worker lifecycle events. + * + * @fires [message]{@link module:workbox-window.Workbox#message} + * @fires [installed]{@link module:workbox-window.Workbox#installed} + * @fires [waiting]{@link module:workbox-window.Workbox#waiting} + * @fires [controlling]{@link module:workbox-window.Workbox#controlling} + * @fires [activated]{@link module:workbox-window.Workbox#activated} + * @fires [redundant]{@link module:workbox-window.Workbox#redundant} + * @fires [externalinstalled]{@link module:workbox-window.Workbox#externalinstalled} + * @fires [externalwaiting]{@link module:workbox-window.Workbox#externalwaiting} + * @fires [externalactivated]{@link module:workbox-window.Workbox#externalactivated} + * @memberof module:workbox-window + */ + + function _empty() {} + + var Workbox = /*#__PURE__*/function (_WorkboxEventTarget) { + _inheritsLoose(Workbox, _WorkboxEventTarget); + + /** + * Creates a new Workbox instance with a script URL and service worker + * options. The script URL and options are the same as those used when + * calling `navigator.serviceWorker.register(scriptURL, options)`. See: + * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register + * + * @param {string} scriptURL The service worker script associated with this + * instance. + * @param {Object} [registerOptions] The service worker options associated + * with this instance. + */ + function Workbox(scriptURL, registerOptions) { + var _this; + + if (registerOptions === void 0) { + registerOptions = {}; + } + + _this = _WorkboxEventTarget.call(this) || this; + _this._registerOptions = {}; + _this._updateFoundCount = 0; // Deferreds we can resolve later. + + _this._swDeferred = new Deferred(); + _this._activeDeferred = new Deferred(); + _this._controllingDeferred = new Deferred(); + _this._registrationTime = 0; + _this._ownSWs = new Set(); + /** + * @private + */ + + _this._onUpdateFound = function () { + // `this._registration` will never be `undefined` after an update is found. + var registration = _this._registration; + var installingSW = registration.installing; // If the script URL passed to `navigator.serviceWorker.register()` is + // different from the current controlling SW's script URL, we know any + // successful registration calls will trigger an `updatefound` event. + // But if the registered script URL is the same as the current controlling + // SW's script URL, we'll only get an `updatefound` event if the file + // changed since it was last registered. This can be a problem if the user + // opens up the same page in a different tab, and that page registers + // a SW that triggers an update. It's a problem because this page has no + // good way of knowing whether the `updatefound` event came from the SW + // script it registered or from a registration attempt made by a newer + // version of the page running in another tab. + // To minimize the possibility of a false positive, we use the logic here: + + var updateLikelyTriggeredExternally = // Since we enforce only calling `register()` once, and since we don't + // add the `updatefound` event listener until the `register()` call, if + // `_updateFoundCount` is > 0 then it means this method has already + // been called, thus this SW must be external + _this._updateFoundCount > 0 || // If the script URL of the installing SW is different from this + // instance's script URL, we know it's definitely not from our + // registration. + !urlsMatch(installingSW.scriptURL, _this._scriptURL) || // If all of the above are false, then we use a time-based heuristic: + // Any `updatefound` event that occurs long after our registration is + // assumed to be external. + performance.now() > _this._registrationTime + REGISTRATION_TIMEOUT_DURATION ? // If any of the above are not true, we assume the update was + // triggered by this instance. + true : false; + + if (updateLikelyTriggeredExternally) { + _this._externalSW = installingSW; + registration.removeEventListener('updatefound', _this._onUpdateFound); + } else { + // If the update was not triggered externally we know the installing + // SW is the one we registered, so we set it. + _this._sw = installingSW; + + _this._ownSWs.add(installingSW); + + _this._swDeferred.resolve(installingSW); // The `installing` state isn't something we have a dedicated + // callback for, but we do log messages for it in development. + + + { + if (navigator.serviceWorker.controller) { + logger.log('Updated service worker found. Installing now...'); + } else { + logger.log('Service worker is installing...'); + } + } + } // Increment the `updatefound` count, so future invocations of this + // method can be sure they were triggered externally. + + + ++_this._updateFoundCount; // Add a `statechange` listener regardless of whether this update was + // triggered externally, since we have callbacks for both. + + installingSW.addEventListener('statechange', _this._onStateChange); + }; + /** + * @private + * @param {Event} originalEvent + */ + + + _this._onStateChange = function (originalEvent) { + // `this._registration` will never be `undefined` after an update is found. + var registration = _this._registration; + var sw = originalEvent.target; + var state = sw.state; + var isExternal = sw === _this._externalSW; + var eventPrefix = isExternal ? 'external' : ''; + var eventProps = { + sw: sw, + originalEvent: originalEvent + }; + + if (!isExternal && _this._isUpdate) { + eventProps.isUpdate = true; + } + + _this.dispatchEvent(new WorkboxEvent(eventPrefix + state, eventProps)); + + if (state === 'installed') { + // This timeout is used to ignore cases where the service worker calls + // `skipWaiting()` in the install event, thus moving it directly in the + // activating state. (Since all service workers *must* go through the + // waiting phase, the only way to detect `skipWaiting()` called in the + // install event is to observe that the time spent in the waiting phase + // is very short.) + // NOTE: we don't need separate timeouts for the own and external SWs + // since they can't go through these phases at the same time. + _this._waitingTimeout = self.setTimeout(function () { + // Ensure the SW is still waiting (it may now be redundant). + if (state === 'installed' && registration.waiting === sw) { + _this.dispatchEvent(new WorkboxEvent(eventPrefix + 'waiting', eventProps)); + + { + if (isExternal) { + logger.warn('An external service worker has installed but is ' + 'waiting for this client to close before activating...'); + } else { + logger.warn('The service worker has installed but is waiting ' + 'for existing clients to close before activating...'); + } + } + } + }, WAITING_TIMEOUT_DURATION); + } else if (state === 'activating') { + clearTimeout(_this._waitingTimeout); + + if (!isExternal) { + _this._activeDeferred.resolve(sw); + } + } + + { + switch (state) { + case 'installed': + if (isExternal) { + logger.warn('An external service worker has installed. ' + 'You may want to suggest users reload this page.'); + } else { + logger.log('Registered service worker installed.'); + } + + break; + + case 'activated': + if (isExternal) { + logger.warn('An external service worker has activated.'); + } else { + logger.log('Registered service worker activated.'); + + if (sw !== navigator.serviceWorker.controller) { + logger.warn('The registered service worker is active but ' + 'not yet controlling the page. Reload or run ' + '`clients.claim()` in the service worker.'); + } + } + + break; + + case 'redundant': + if (sw === _this._compatibleControllingSW) { + logger.log('Previously controlling service worker now redundant!'); + } else if (!isExternal) { + logger.log('Registered service worker now redundant!'); + } + + break; + } + } + }; + /** + * @private + * @param {Event} originalEvent + */ + + + _this._onControllerChange = function (originalEvent) { + var sw = _this._sw; + + if (sw === navigator.serviceWorker.controller) { + _this.dispatchEvent(new WorkboxEvent('controlling', { + sw: sw, + originalEvent: originalEvent, + isUpdate: _this._isUpdate + })); + + { + logger.log('Registered service worker now controlling this page.'); + } + + _this._controllingDeferred.resolve(sw); + } + }; + /** + * @private + * @param {Event} originalEvent + */ + + + _this._onMessage = _async(function (originalEvent) { + var data = originalEvent.data, + source = originalEvent.source; // Wait until there's an "own" service worker. This is used to buffer + // `message` events that may be received prior to calling `register()`. + + return _await(_this.getSW(), function () { + if (_this._ownSWs.has(source)) { + _this.dispatchEvent(new WorkboxEvent('message', { + data: data, + sw: source, + originalEvent: originalEvent + })); + } + }); // If the service worker that sent the message is in the list of own + // service workers for this instance, dispatch a `message` event. + // NOTE: we check for all previously owned service workers rather than + // just the current one because some messages (e.g. cache updates) use + // a timeout when sent and may be delayed long enough for a service worker + // update to be found. + }); + _this._scriptURL = scriptURL; + _this._registerOptions = registerOptions; // Add a message listener immediately since messages received during + // page load are buffered only until the DOMContentLoaded event: + // https://github.com/GoogleChrome/workbox/issues/2202 + + navigator.serviceWorker.addEventListener('message', _this._onMessage); + return _this; + } + /** + * Registers a service worker for this instances script URL and service + * worker options. By default this method delays registration until after + * the window has loaded. + * + * @param {Object} [options] + * @param {Function} [options.immediate=false] Setting this to true will + * register the service worker immediately, even if the window has + * not loaded (not recommended). + */ + + + var _proto = Workbox.prototype; + + _proto.register = function register(_temp) { + var _ref = _temp === void 0 ? {} : _temp, + _ref$immediate = _ref.immediate, + immediate = _ref$immediate === void 0 ? false : _ref$immediate; + + try { + var _this3 = this; + + if ("dev" !== 'production') { + if (_this3._registrationTime) { + logger.error('Cannot re-register a Workbox instance after it has ' + 'been registered. Create a new instance instead.'); + return; + } + } + + return _invoke(function () { + if (!immediate && document.readyState !== 'complete') { + return _awaitIgnored(new Promise(function (res) { + return window.addEventListener('load', res); + })); + } + }, function () { + // Set this flag to true if any service worker was controlling the page + // at registration time. + _this3._isUpdate = Boolean(navigator.serviceWorker.controller); // Before registering, attempt to determine if a SW is already controlling + // the page, and if that SW script (and version, if specified) matches this + // instance's script. + + _this3._compatibleControllingSW = _this3._getControllingSWIfCompatible(); + return _await(_this3._registerScript(), function (_this2$_registerScrip) { + _this3._registration = _this2$_registerScrip; + + // If we have a compatible controller, store the controller as the "own" + // SW, resolve active/controlling deferreds and add necessary listeners. + if (_this3._compatibleControllingSW) { + _this3._sw = _this3._compatibleControllingSW; + + _this3._activeDeferred.resolve(_this3._compatibleControllingSW); + + _this3._controllingDeferred.resolve(_this3._compatibleControllingSW); + + _this3._compatibleControllingSW.addEventListener('statechange', _this3._onStateChange, { + once: true + }); + } // If there's a waiting service worker with a matching URL before the + // `updatefound` event fires, it likely means that this site is open + // in another tab, or the user refreshed the page (and thus the previous + // page wasn't fully unloaded before this page started loading). + // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting + + + var waitingSW = _this3._registration.waiting; + + if (waitingSW && urlsMatch(waitingSW.scriptURL, _this3._scriptURL)) { + // Store the waiting SW as the "own" Sw, even if it means overwriting + // a compatible controller. + _this3._sw = waitingSW; // Run this in the next microtask, so any code that adds an event + // listener after awaiting `register()` will get this event. + + dontWaitFor(Promise.resolve().then(function () { + _this3.dispatchEvent(new WorkboxEvent('waiting', { + sw: waitingSW, + wasWaitingBeforeRegister: true + })); + + if ("dev" !== 'production') { + logger.warn('A service worker was already waiting to activate ' + 'before this script was registered...'); + } + })); + } // If an "own" SW is already set, resolve the deferred. + + + if (_this3._sw) { + _this3._swDeferred.resolve(_this3._sw); + + _this3._ownSWs.add(_this3._sw); + } + + if ("dev" !== 'production') { + logger.log('Successfully registered service worker.', _this3._scriptURL); + + if (navigator.serviceWorker.controller) { + if (_this3._compatibleControllingSW) { + logger.debug('A service worker with the same script URL ' + 'is already controlling this page.'); + } else { + logger.debug('A service worker with a different script URL is ' + 'currently controlling the page. The browser is now fetching ' + 'the new script now...'); + } + } + + var currentPageIsOutOfScope = function currentPageIsOutOfScope() { + var scopeURL = new URL(_this3._registerOptions.scope || _this3._scriptURL, document.baseURI); + var scopeURLBasePath = new URL('./', scopeURL.href).pathname; + return !location.pathname.startsWith(scopeURLBasePath); + }; + + if (currentPageIsOutOfScope()) { + logger.warn('The current page is not in scope for the registered ' + 'service worker. Was this a mistake?'); + } + } + + _this3._registration.addEventListener('updatefound', _this3._onUpdateFound); + + navigator.serviceWorker.addEventListener('controllerchange', _this3._onControllerChange, { + once: true + }); + return _this3._registration; + }); + }); + } catch (e) { + return Promise.reject(e); + } + } + /** + * Checks for updates of the registered service worker. + */ + ; + + _proto.update = function update() { + try { + var _this5 = this; + + if (!_this5._registration) { + if ("dev" !== 'production') { + logger.error('Cannot update a Workbox instance without ' + 'being registered. Register the Workbox instance first.'); + } + + return; + } // Try to update registration + + + return _awaitIgnored(_this5._registration.update()); + } catch (e) { + return Promise.reject(e); + } + } + /** + * Resolves to the service worker registered by this instance as soon as it + * is active. If a service worker was already controlling at registration + * time then it will resolve to that if the script URLs (and optionally + * script versions) match, otherwise it will wait until an update is found + * and activates. + * + * @return {Promise} + */ + ; + + /** + * Resolves with a reference to a service worker that matches the script URL + * of this instance, as soon as it's available. + * + * If, at registration time, there's already an active or waiting service + * worker with a matching script URL, it will be used (with the waiting + * service worker taking precedence over the active service worker if both + * match, since the waiting service worker would have been registered more + * recently). + * If there's no matching active or waiting service worker at registration + * time then the promise will not resolve until an update is found and starts + * installing, at which point the installing service worker is used. + * + * @return {Promise} + */ + _proto.getSW = function getSW() { + try { + var _this7 = this; + + // If `this._sw` is set, resolve with that as we want `getSW()` to + // return the correct (new) service worker if an update is found. + return _this7._sw !== undefined ? _this7._sw : _this7._swDeferred.promise; + } catch (e) { + return Promise.reject(e); + } + } + /** + * Sends the passed data object to the service worker registered by this + * instance (via [`getSW()`]{@link module:workbox-window.Workbox#getSW}) and resolves + * with a response (if any). + * + * A response can be set in a message handler in the service worker by + * calling `event.ports[0].postMessage(...)`, which will resolve the promise + * returned by `messageSW()`. If no response is set, the promise will never + * resolve. + * + * @param {Object} data An object to send to the service worker + * @return {Promise} + */ + ; + + _proto.messageSW = function messageSW$1(data) { + try { + var _this9 = this; + + return _await(_this9.getSW(), function (sw) { + return messageSW(sw, data); + }); + } catch (e) { + return Promise.reject(e); + } + } + /** + * Checks for a service worker already controlling the page and returns + * it if its script URL matches. + * + * @private + * @return {ServiceWorker|undefined} + */ + ; + + _proto._getControllingSWIfCompatible = function _getControllingSWIfCompatible() { + var controller = navigator.serviceWorker.controller; + + if (controller && urlsMatch(controller.scriptURL, this._scriptURL)) { + return controller; + } else { + return undefined; + } + } + /** + * Registers a service worker for this instances script URL and register + * options and tracks the time registration was complete. + * + * @private + */ + ; + + _proto._registerScript = function _registerScript() { + try { + var _this11 = this; + + return _catch(function () { + return _await(navigator.serviceWorker.register(_this11._scriptURL, _this11._registerOptions), function (reg) { + // Keep track of when registration happened, so it can be used in the + // `this._onUpdateFound` heuristic. Also use the presence of this + // property as a way to see if `.register()` has been called. + _this11._registrationTime = performance.now(); + return reg; + }); + }, function (error) { + if ("dev" !== 'production') { + logger.error(error); + } // Re-throw the error. + + + throw error; + }); + } catch (e) { + return Promise.reject(e); + } + }; + + _createClass(Workbox, [{ + key: "active", + get: function get() { + return this._activeDeferred.promise; + } + /** + * Resolves to the service worker registered by this instance as soon as it + * is controlling the page. If a service worker was already controlling at + * registration time then it will resolve to that if the script URLs (and + * optionally script versions) match, otherwise it will wait until an update + * is found and starts controlling the page. + * Note: the first time a service worker is installed it will active but + * not start controlling the page unless `clients.claim()` is called in the + * service worker. + * + * @return {Promise} + */ + + }, { + key: "controlling", + get: function get() { + return this._controllingDeferred.promise; + } + }]); + + return Workbox; + }(WorkboxEventTarget); + + function _awaitIgnored(value, direct) { + if (!direct) { + return value && value.then ? value.then(_empty) : Promise.resolve(); + } + } // The jsdoc comments below outline the events this instance may dispatch: + // ----------------------------------------------------------------------- + + /** + * The `message` event is dispatched any time a `postMessage` is received. + * + * @event module:workbox-window.Workbox#message + * @type {WorkboxEvent} + * @property {*} data The `data` property from the original `message` event. + * @property {Event} originalEvent The original [`message`]{@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} + * event. + * @property {string} type `message`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `installed` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `installed`. + * + * Then can happen either the very first time a service worker is installed, + * or after an update to the current service worker is found. In the case + * of an update being found, the event's `isUpdate` property will be `true`. + * + * @event module:workbox-window.Workbox#installed + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `installed`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `waiting` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `installed` and then doesn't immediately change to `activating`. + * It may also be dispatched if a service worker with the same + * [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL} + * was already waiting when the [`register()`]{@link module:workbox-window.Workbox#register} + * method was called. + * + * @event module:workbox-window.Workbox#waiting + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event|undefined} originalEvent The original + * [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event, or `undefined` in the case where the service worker was waiting + * to before `.register()` was called. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {boolean|undefined} wasWaitingBeforeRegister True if a service worker with + * a matching `scriptURL` was already waiting when this `Workbox` + * instance called `register()`. + * @property {string} type `waiting`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `controlling` event is dispatched if a + * [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange} + * fires on the service worker [container]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer} + * and the [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL} + * of the new [controller]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/controller} + * matches the `scriptURL` of the `Workbox` instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}. + * + * @event module:workbox-window.Workbox#controlling + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this service worker was registered. + * @property {string} type `controlling`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `activated` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `activated`. + * + * @event module:workbox-window.Workbox#activated + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `activated`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `redundant` event is dispatched if the state of a + * [`Workbox`]{@link module:workbox-window.Workbox} instance's + * [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw} + * changes to `redundant`. + * + * @event module:workbox-window.Workbox#redundant + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {boolean|undefined} isUpdate True if a service worker was already + * controlling when this `Workbox` instance called `register()`. + * @property {string} type `redundant`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `externalinstalled` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `installed`. + * + * @event module:workbox-window.Workbox#externalinstalled + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalinstalled`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `externalwaiting` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `waiting`. + * + * @event module:workbox-window.Workbox#externalwaiting + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalwaiting`. + * @property {Workbox} target The `Workbox` instance. + */ + + /** + * The `externalactivated` event is dispatched if the state of an + * [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-window#when_an_unexpected_version_of_the_service_worker_is_found} + * changes to `activated`. + * + * @event module:workbox-window.Workbox#externalactivated + * @type {WorkboxEvent} + * @property {ServiceWorker} sw The service worker instance. + * @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange} + * event. + * @property {string} type `externalactivated`. + * @property {Workbox} target The `Workbox` instance. + */ + + + function _invoke(body, then) { + var result = body(); + + if (result && result.then) { + return result.then(then); + } + + return then(result); + } + + function _catch(body, recover) { + try { + var result = body(); + } catch (e) { + return recover(e); + } + + if (result && result.then) { + return result.then(void 0, recover); + } + + return result; + } + + exports.Workbox = Workbox; + exports.messageSW = messageSW; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); + diff --git a/source/js/workbox/workbox-window.prod.es5.mjs b/source/js/workbox/workbox-window.prod.es5.mjs new file mode 100644 index 0000000..6dd6977 --- /dev/null +++ b/source/js/workbox/workbox-window.prod.es5.mjs @@ -0,0 +1,2 @@ +try{self["workbox:window:5.1.4"]&&_()}catch(n){}function n(n,t){return new Promise((function(r){var e=new MessageChannel;e.port1.onmessage=function(n){r(n.data)},n.postMessage(t,[e.port2])}))}function t(n,t){for(var r=0;rn.length)&&(t=n.length);for(var r=0,e=new Array(t);r=n.length?{done:!0}:{done:!1,value:n[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}return(e=n[Symbol.iterator]()).next.bind(e)}try{self["workbox:core:5.1.4"]&&_()}catch(n){}var i=function(){var n=this;this.promise=new Promise((function(t,r){n.resolve=t,n.reject=r}))};function o(n,t){var r=location.href;return new URL(n,r).href===new URL(t,r).href}var u=function(n,t){this.type=n,Object.assign(this,t)};function a(n,t,r){return r?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function c(){}var f=function(r){var e,c;function f(n,t){var e,c;return void 0===t&&(t={}),(e=r.call(this)||this).t={},e.i=0,e.o=new i,e.u=new i,e.s=new i,e.v=0,e.h=new Set,e.l=function(){var n=e.m,t=n.installing;e.i>0||!o(t.scriptURL,e.g)||performance.now()>e.v+6e4?(e.p=t,n.removeEventListener("updatefound",e.l)):(e.P=t,e.h.add(t),e.o.resolve(t)),++e.i,t.addEventListener("statechange",e.S)},e.S=function(n){var t=e.m,r=n.target,i=r.state,o=r===e.p,a=o?"external":"",c={sw:r,originalEvent:n};!o&&e.j&&(c.isUpdate=!0),e.dispatchEvent(new u(a+i,c)),"installed"===i?e.A=self.setTimeout((function(){"installed"===i&&t.waiting===r&&e.dispatchEvent(new u(a+"waiting",c))}),200):"activating"===i&&(clearTimeout(e.A),o||e.u.resolve(r))},e.O=function(n){var t=e.P;t===navigator.serviceWorker.controller&&(e.dispatchEvent(new u("controlling",{sw:t,originalEvent:n,isUpdate:e.j})),e.s.resolve(t))},e.U=(c=function(n){var t=n.data,r=n.source;return a(e.getSW(),(function(){e.h.has(r)&&e.dispatchEvent(new u("message",{data:t,sw:r,originalEvent:n}))}))},function(){for(var n=[],t=0;t{const e=new MessageChannel;e.port1.onmessage=t=>{i(t.data)},t.postMessage(s,[e.port2])})}try{self["workbox:core:5.1.4"]&&_()}catch(t){}class s{constructor(){this.promise=new Promise((t,s)=>{this.resolve=t,this.reject=s})}}function i(t,s){const{href:i}=location;return new URL(t,i).href===new URL(s,i).href}class e{constructor(t,s){this.type=t,Object.assign(this,s)}}class h extends class{constructor(){this.t=new Map}addEventListener(t,s){this.s(t).add(s)}removeEventListener(t,s){this.s(t).delete(s)}dispatchEvent(t){t.target=this;const s=this.s(t.type);for(const i of s)i(t)}s(t){return this.t.has(t)||this.t.set(t,new Set),this.t.get(t)}}{constructor(t,h={}){super(),this.i={},this.h=0,this.o=new s,this.g=new s,this.l=new s,this.u=0,this.v=new Set,this.m=()=>{const t=this.p,s=t.installing;this.h>0||!i(s.scriptURL,this.S)||performance.now()>this.u+6e4?(this.L=s,t.removeEventListener("updatefound",this.m)):(this._=s,this.v.add(s),this.o.resolve(s)),++this.h,s.addEventListener("statechange",this.P)},this.P=t=>{const s=this.p,i=t.target,{state:h}=i,n=i===this.L,a=n?"external":"",r={sw:i,originalEvent:t};!n&&this.W&&(r.isUpdate=!0),this.dispatchEvent(new e(a+h,r)),"installed"===h?this.B=self.setTimeout(()=>{"installed"===h&&s.waiting===i&&this.dispatchEvent(new e(a+"waiting",r))},200):"activating"===h&&(clearTimeout(this.B),n||this.g.resolve(i))},this.C=t=>{const s=this._;s===navigator.serviceWorker.controller&&(this.dispatchEvent(new e("controlling",{sw:s,originalEvent:t,isUpdate:this.W})),this.l.resolve(s))},this.R=async t=>{const{data:s,source:i}=t;await this.getSW(),this.v.has(i)&&this.dispatchEvent(new e("message",{data:s,sw:i,originalEvent:t}))},this.S=t,this.i=h,navigator.serviceWorker.addEventListener("message",this.R)}async register({immediate:t=!1}={}){t||"complete"===document.readyState||await new Promise(t=>window.addEventListener("load",t)),this.W=Boolean(navigator.serviceWorker.controller),this.U=this.M(),this.p=await this.T(),this.U&&(this._=this.U,this.g.resolve(this.U),this.l.resolve(this.U),this.U.addEventListener("statechange",this.P,{once:!0}));const s=this.p.waiting;return s&&i(s.scriptURL,this.S)&&(this._=s,Promise.resolve().then(()=>{this.dispatchEvent(new e("waiting",{sw:s,wasWaitingBeforeRegister:!0}))}).then(()=>{})),this._&&(this.o.resolve(this._),this.v.add(this._)),this.p.addEventListener("updatefound",this.m),navigator.serviceWorker.addEventListener("controllerchange",this.C,{once:!0}),this.p}async update(){this.p&&await this.p.update()}get active(){return this.g.promise}get controlling(){return this.l.promise}async getSW(){return void 0!==this._?this._:this.o.promise}async messageSW(s){return t(await this.getSW(),s)}M(){const t=navigator.serviceWorker.controller;return t&&i(t.scriptURL,this.S)?t:void 0}async T(){try{const t=await navigator.serviceWorker.register(this.S,this.i);return this.u=performance.now(),t}catch(t){throw t}}}export{h as Workbox,t as messageSW}; + diff --git a/source/js/workbox/workbox-window.prod.umd.js b/source/js/workbox/workbox-window.prod.umd.js new file mode 100644 index 0000000..f8448ed --- /dev/null +++ b/source/js/workbox/workbox-window.prod.umd.js @@ -0,0 +1,2 @@ +!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).workbox={})}(this,(function(n){"use strict";try{self["workbox:window:5.1.4"]&&_()}catch(n){}function t(n,t){return new Promise((function(r){var e=new MessageChannel;e.port1.onmessage=function(n){r(n.data)},n.postMessage(t,[e.port2])}))}function r(n,t){for(var r=0;rn.length)&&(t=n.length);for(var r=0,e=new Array(t);r=n.length?{done:!0}:{done:!1,value:n[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}return(r=n[Symbol.iterator]()).next.bind(r)}try{self["workbox:core:5.1.4"]&&_()}catch(n){}var o=function(){var n=this;this.promise=new Promise((function(t,r){n.resolve=t,n.reject=r}))};function u(n,t){var r=location.href;return new URL(n,r).href===new URL(t,r).href}var a=function(n,t){this.type=n,Object.assign(this,t)};function c(n,t,r){return r?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function f(){}var s=function(n){var e,i;function f(t,r){var e,i;return void 0===r&&(r={}),(e=n.call(this)||this).t={},e.i=0,e.o=new o,e.u=new o,e.s=new o,e.v=0,e.h=new Set,e.l=function(){var n=e.m,t=n.installing;e.i>0||!u(t.scriptURL,e.g)||performance.now()>e.v+6e4?(e.p=t,n.removeEventListener("updatefound",e.l)):(e.P=t,e.h.add(t),e.o.resolve(t)),++e.i,t.addEventListener("statechange",e.j)},e.j=function(n){var t=e.m,r=n.target,i=r.state,o=r===e.p,u=o?"external":"",c={sw:r,originalEvent:n};!o&&e.S&&(c.isUpdate=!0),e.dispatchEvent(new a(u+i,c)),"installed"===i?e.O=self.setTimeout((function(){"installed"===i&&t.waiting===r&&e.dispatchEvent(new a(u+"waiting",c))}),200):"activating"===i&&(clearTimeout(e.O),o||e.u.resolve(r))},e.A=function(n){var t=e.P;t===navigator.serviceWorker.controller&&(e.dispatchEvent(new a("controlling",{sw:t,originalEvent:n,isUpdate:e.S})),e.s.resolve(t))},e.M=(i=function(n){var t=n.data,r=n.source;return c(e.getSW(),(function(){e.h.has(r)&&e.dispatchEvent(new a("message",{data:t,sw:r,originalEvent:n}))}))},function(){for(var n=[],t=0;t