workbox-background-sync.dev.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250
  1. this.workbox = this.workbox || {};
  2. this.workbox.backgroundSync = (function (exports, WorkboxError_js, logger_js, assert_js, getFriendlyURL_js) {
  3. 'use strict';
  4. function _extends() {
  5. _extends = Object.assign || function (target) {
  6. for (var i = 1; i < arguments.length; i++) {
  7. var source = arguments[i];
  8. for (var key in source) {
  9. if (Object.prototype.hasOwnProperty.call(source, key)) {
  10. target[key] = source[key];
  11. }
  12. }
  13. }
  14. return target;
  15. };
  16. return _extends.apply(this, arguments);
  17. }
  18. const instanceOfAny = (object, constructors) => constructors.some(c => object instanceof c);
  19. let idbProxyableTypes;
  20. let cursorAdvanceMethods; // This is a function to prevent it throwing up in node environments.
  21. function getIdbProxyableTypes() {
  22. return idbProxyableTypes || (idbProxyableTypes = [IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction]);
  23. } // This is a function to prevent it throwing up in node environments.
  24. function getCursorAdvanceMethods() {
  25. return cursorAdvanceMethods || (cursorAdvanceMethods = [IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey]);
  26. }
  27. const cursorRequestMap = new WeakMap();
  28. const transactionDoneMap = new WeakMap();
  29. const transactionStoreNamesMap = new WeakMap();
  30. const transformCache = new WeakMap();
  31. const reverseTransformCache = new WeakMap();
  32. function promisifyRequest(request) {
  33. const promise = new Promise((resolve, reject) => {
  34. const unlisten = () => {
  35. request.removeEventListener('success', success);
  36. request.removeEventListener('error', error);
  37. };
  38. const success = () => {
  39. resolve(wrap(request.result));
  40. unlisten();
  41. };
  42. const error = () => {
  43. reject(request.error);
  44. unlisten();
  45. };
  46. request.addEventListener('success', success);
  47. request.addEventListener('error', error);
  48. });
  49. promise.then(value => {
  50. // Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval
  51. // (see wrapFunction).
  52. if (value instanceof IDBCursor) {
  53. cursorRequestMap.set(value, request);
  54. } // Catching to avoid "Uncaught Promise exceptions"
  55. }).catch(() => {}); // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This
  56. // is because we create many promises from a single IDBRequest.
  57. reverseTransformCache.set(promise, request);
  58. return promise;
  59. }
  60. function cacheDonePromiseForTransaction(tx) {
  61. // Early bail if we've already created a done promise for this transaction.
  62. if (transactionDoneMap.has(tx)) return;
  63. const done = new Promise((resolve, reject) => {
  64. const unlisten = () => {
  65. tx.removeEventListener('complete', complete);
  66. tx.removeEventListener('error', error);
  67. tx.removeEventListener('abort', error);
  68. };
  69. const complete = () => {
  70. resolve();
  71. unlisten();
  72. };
  73. const error = () => {
  74. reject(tx.error || new DOMException('AbortError', 'AbortError'));
  75. unlisten();
  76. };
  77. tx.addEventListener('complete', complete);
  78. tx.addEventListener('error', error);
  79. tx.addEventListener('abort', error);
  80. }); // Cache it for later retrieval.
  81. transactionDoneMap.set(tx, done);
  82. }
  83. let idbProxyTraps = {
  84. get(target, prop, receiver) {
  85. if (target instanceof IDBTransaction) {
  86. // Special handling for transaction.done.
  87. if (prop === 'done') return transactionDoneMap.get(target); // Polyfill for objectStoreNames because of Edge.
  88. if (prop === 'objectStoreNames') {
  89. return target.objectStoreNames || transactionStoreNamesMap.get(target);
  90. } // Make tx.store return the only store in the transaction, or undefined if there are many.
  91. if (prop === 'store') {
  92. return receiver.objectStoreNames[1] ? undefined : receiver.objectStore(receiver.objectStoreNames[0]);
  93. }
  94. } // Else transform whatever we get back.
  95. return wrap(target[prop]);
  96. },
  97. set(target, prop, value) {
  98. target[prop] = value;
  99. return true;
  100. },
  101. has(target, prop) {
  102. if (target instanceof IDBTransaction && (prop === 'done' || prop === 'store')) {
  103. return true;
  104. }
  105. return prop in target;
  106. }
  107. };
  108. function replaceTraps(callback) {
  109. idbProxyTraps = callback(idbProxyTraps);
  110. }
  111. function wrapFunction(func) {
  112. // Due to expected object equality (which is enforced by the caching in `wrap`), we
  113. // only create one new func per func.
  114. // Edge doesn't support objectStoreNames (booo), so we polyfill it here.
  115. if (func === IDBDatabase.prototype.transaction && !('objectStoreNames' in IDBTransaction.prototype)) {
  116. return function (storeNames, ...args) {
  117. const tx = func.call(unwrap(this), storeNames, ...args);
  118. transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]);
  119. return wrap(tx);
  120. };
  121. } // Cursor methods are special, as the behaviour is a little more different to standard IDB. In
  122. // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
  123. // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
  124. // with real promises, so each advance methods returns a new promise for the cursor object, or
  125. // undefined if the end of the cursor has been reached.
  126. if (getCursorAdvanceMethods().includes(func)) {
  127. return function (...args) {
  128. // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
  129. // the original object.
  130. func.apply(unwrap(this), args);
  131. return wrap(cursorRequestMap.get(this));
  132. };
  133. }
  134. return function (...args) {
  135. // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
  136. // the original object.
  137. return wrap(func.apply(unwrap(this), args));
  138. };
  139. }
  140. function transformCachableValue(value) {
  141. if (typeof value === 'function') return wrapFunction(value); // This doesn't return, it just creates a 'done' promise for the transaction,
  142. // which is later returned for transaction.done (see idbObjectHandler).
  143. if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value);
  144. if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps); // Return the same value back if we're not going to transform it.
  145. return value;
  146. }
  147. function wrap(value) {
  148. // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
  149. // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
  150. if (value instanceof IDBRequest) return promisifyRequest(value); // If we've already transformed this value before, reuse the transformed value.
  151. // This is faster, but it also provides object equality.
  152. if (transformCache.has(value)) return transformCache.get(value);
  153. const newValue = transformCachableValue(value); // Not all types are transformed.
  154. // These may be primitive types, so they can't be WeakMap keys.
  155. if (newValue !== value) {
  156. transformCache.set(value, newValue);
  157. reverseTransformCache.set(newValue, value);
  158. }
  159. return newValue;
  160. }
  161. const unwrap = value => reverseTransformCache.get(value);
  162. /**
  163. * Open a database.
  164. *
  165. * @param name Name of the database.
  166. * @param version Schema version.
  167. * @param callbacks Additional callbacks.
  168. */
  169. function openDB(name, version, {
  170. blocked,
  171. upgrade,
  172. blocking,
  173. terminated
  174. } = {}) {
  175. const request = indexedDB.open(name, version);
  176. const openPromise = wrap(request);
  177. if (upgrade) {
  178. request.addEventListener('upgradeneeded', event => {
  179. upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction));
  180. });
  181. }
  182. if (blocked) request.addEventListener('blocked', () => blocked());
  183. openPromise.then(db => {
  184. if (terminated) db.addEventListener('close', () => terminated());
  185. if (blocking) db.addEventListener('versionchange', () => blocking());
  186. }).catch(() => {});
  187. return openPromise;
  188. }
  189. const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'];
  190. const writeMethods = ['put', 'add', 'delete', 'clear'];
  191. const cachedMethods = new Map();
  192. function getMethod(target, prop) {
  193. if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === 'string')) {
  194. return;
  195. }
  196. if (cachedMethods.get(prop)) return cachedMethods.get(prop);
  197. const targetFuncName = prop.replace(/FromIndex$/, '');
  198. const useIndex = prop !== targetFuncName;
  199. const isWrite = writeMethods.includes(targetFuncName);
  200. if ( // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
  201. !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) {
  202. return;
  203. }
  204. const method = async function (storeName, ...args) {
  205. // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :(
  206. const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly');
  207. let target = tx.store;
  208. if (useIndex) target = target.index(args.shift()); // Must reject if op rejects.
  209. // If it's a write operation, must reject if tx.done rejects.
  210. // Must reject with op rejection first.
  211. // Must resolve with op value.
  212. // Must handle both promises (no unhandled rejections)
  213. return (await Promise.all([target[targetFuncName](...args), isWrite && tx.done]))[0];
  214. };
  215. cachedMethods.set(prop, method);
  216. return method;
  217. }
  218. replaceTraps(oldTraps => _extends({}, oldTraps, {
  219. get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
  220. has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
  221. }));
  222. try {
  223. self['workbox:background-sync:6.6.0'] && _();
  224. } catch (e) {}
  225. /*
  226. Copyright 2021 Google LLC
  227. Use of this source code is governed by an MIT-style
  228. license that can be found in the LICENSE file or at
  229. https://opensource.org/licenses/MIT.
  230. */
  231. const DB_VERSION = 3;
  232. const DB_NAME = 'workbox-background-sync';
  233. const REQUEST_OBJECT_STORE_NAME = 'requests';
  234. const QUEUE_NAME_INDEX = 'queueName';
  235. /**
  236. * A class to interact directly an IndexedDB created specifically to save and
  237. * retrieve QueueStoreEntries. This class encapsulates all the schema details
  238. * to store the representation of a Queue.
  239. *
  240. * @private
  241. */
  242. class QueueDb {
  243. constructor() {
  244. this._db = null;
  245. }
  246. /**
  247. * Add QueueStoreEntry to underlying db.
  248. *
  249. * @param {UnidentifiedQueueStoreEntry} entry
  250. */
  251. async addEntry(entry) {
  252. const db = await this.getDb();
  253. const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, 'readwrite', {
  254. durability: 'relaxed'
  255. });
  256. await tx.store.add(entry);
  257. await tx.done;
  258. }
  259. /**
  260. * Returns the first entry id in the ObjectStore.
  261. *
  262. * @return {number | undefined}
  263. */
  264. async getFirstEntryId() {
  265. const db = await this.getDb();
  266. const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.openCursor();
  267. return cursor === null || cursor === void 0 ? void 0 : cursor.value.id;
  268. }
  269. /**
  270. * Get all the entries filtered by index
  271. *
  272. * @param queueName
  273. * @return {Promise<QueueStoreEntry[]>}
  274. */
  275. async getAllEntriesByQueueName(queueName) {
  276. const db = await this.getDb();
  277. const results = await db.getAllFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
  278. return results ? results : new Array();
  279. }
  280. /**
  281. * Returns the number of entries filtered by index
  282. *
  283. * @param queueName
  284. * @return {Promise<number>}
  285. */
  286. async getEntryCountByQueueName(queueName) {
  287. const db = await this.getDb();
  288. return db.countFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
  289. }
  290. /**
  291. * Deletes a single entry by id.
  292. *
  293. * @param {number} id the id of the entry to be deleted
  294. */
  295. async deleteEntry(id) {
  296. const db = await this.getDb();
  297. await db.delete(REQUEST_OBJECT_STORE_NAME, id);
  298. }
  299. /**
  300. *
  301. * @param queueName
  302. * @returns {Promise<QueueStoreEntry | undefined>}
  303. */
  304. async getFirstEntryByQueueName(queueName) {
  305. return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'next');
  306. }
  307. /**
  308. *
  309. * @param queueName
  310. * @returns {Promise<QueueStoreEntry | undefined>}
  311. */
  312. async getLastEntryByQueueName(queueName) {
  313. return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'prev');
  314. }
  315. /**
  316. * Returns either the first or the last entries, depending on direction.
  317. * Filtered by index.
  318. *
  319. * @param {IDBCursorDirection} direction
  320. * @param {IDBKeyRange} query
  321. * @return {Promise<QueueStoreEntry | undefined>}
  322. * @private
  323. */
  324. async getEndEntryFromIndex(query, direction) {
  325. const db = await this.getDb();
  326. const cursor = await db.transaction(REQUEST_OBJECT_STORE_NAME).store.index(QUEUE_NAME_INDEX).openCursor(query, direction);
  327. return cursor === null || cursor === void 0 ? void 0 : cursor.value;
  328. }
  329. /**
  330. * Returns an open connection to the database.
  331. *
  332. * @private
  333. */
  334. async getDb() {
  335. if (!this._db) {
  336. this._db = await openDB(DB_NAME, DB_VERSION, {
  337. upgrade: this._upgradeDb
  338. });
  339. }
  340. return this._db;
  341. }
  342. /**
  343. * Upgrades QueueDB
  344. *
  345. * @param {IDBPDatabase<QueueDBSchema>} db
  346. * @param {number} oldVersion
  347. * @private
  348. */
  349. _upgradeDb(db, oldVersion) {
  350. if (oldVersion > 0 && oldVersion < DB_VERSION) {
  351. if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
  352. db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
  353. }
  354. }
  355. const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
  356. autoIncrement: true,
  357. keyPath: 'id'
  358. });
  359. objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, {
  360. unique: false
  361. });
  362. }
  363. }
  364. /*
  365. Copyright 2018 Google LLC
  366. Use of this source code is governed by an MIT-style
  367. license that can be found in the LICENSE file or at
  368. https://opensource.org/licenses/MIT.
  369. */
  370. /**
  371. * A class to manage storing requests from a Queue in IndexedDB,
  372. * indexed by their queue name for easier access.
  373. *
  374. * Most developers will not need to access this class directly;
  375. * it is exposed for advanced use cases.
  376. */
  377. class QueueStore {
  378. /**
  379. * Associates this instance with a Queue instance, so entries added can be
  380. * identified by their queue name.
  381. *
  382. * @param {string} queueName
  383. */
  384. constructor(queueName) {
  385. this._queueName = queueName;
  386. this._queueDb = new QueueDb();
  387. }
  388. /**
  389. * Append an entry last in the queue.
  390. *
  391. * @param {Object} entry
  392. * @param {Object} entry.requestData
  393. * @param {number} [entry.timestamp]
  394. * @param {Object} [entry.metadata]
  395. */
  396. async pushEntry(entry) {
  397. {
  398. assert_js.assert.isType(entry, 'object', {
  399. moduleName: 'workbox-background-sync',
  400. className: 'QueueStore',
  401. funcName: 'pushEntry',
  402. paramName: 'entry'
  403. });
  404. assert_js.assert.isType(entry.requestData, 'object', {
  405. moduleName: 'workbox-background-sync',
  406. className: 'QueueStore',
  407. funcName: 'pushEntry',
  408. paramName: 'entry.requestData'
  409. });
  410. } // Don't specify an ID since one is automatically generated.
  411. delete entry.id;
  412. entry.queueName = this._queueName;
  413. await this._queueDb.addEntry(entry);
  414. }
  415. /**
  416. * Prepend an entry first in the queue.
  417. *
  418. * @param {Object} entry
  419. * @param {Object} entry.requestData
  420. * @param {number} [entry.timestamp]
  421. * @param {Object} [entry.metadata]
  422. */
  423. async unshiftEntry(entry) {
  424. {
  425. assert_js.assert.isType(entry, 'object', {
  426. moduleName: 'workbox-background-sync',
  427. className: 'QueueStore',
  428. funcName: 'unshiftEntry',
  429. paramName: 'entry'
  430. });
  431. assert_js.assert.isType(entry.requestData, 'object', {
  432. moduleName: 'workbox-background-sync',
  433. className: 'QueueStore',
  434. funcName: 'unshiftEntry',
  435. paramName: 'entry.requestData'
  436. });
  437. }
  438. const firstId = await this._queueDb.getFirstEntryId();
  439. if (firstId) {
  440. // Pick an ID one less than the lowest ID in the object store.
  441. entry.id = firstId - 1;
  442. } else {
  443. // Otherwise let the auto-incrementor assign the ID.
  444. delete entry.id;
  445. }
  446. entry.queueName = this._queueName;
  447. await this._queueDb.addEntry(entry);
  448. }
  449. /**
  450. * Removes and returns the last entry in the queue matching the `queueName`.
  451. *
  452. * @return {Promise<QueueStoreEntry|undefined>}
  453. */
  454. async popEntry() {
  455. return this._removeEntry(await this._queueDb.getLastEntryByQueueName(this._queueName));
  456. }
  457. /**
  458. * Removes and returns the first entry in the queue matching the `queueName`.
  459. *
  460. * @return {Promise<QueueStoreEntry|undefined>}
  461. */
  462. async shiftEntry() {
  463. return this._removeEntry(await this._queueDb.getFirstEntryByQueueName(this._queueName));
  464. }
  465. /**
  466. * Returns all entries in the store matching the `queueName`.
  467. *
  468. * @param {Object} options See {@link workbox-background-sync.Queue~getAll}
  469. * @return {Promise<Array<Object>>}
  470. */
  471. async getAll() {
  472. return await this._queueDb.getAllEntriesByQueueName(this._queueName);
  473. }
  474. /**
  475. * Returns the number of entries in the store matching the `queueName`.
  476. *
  477. * @param {Object} options See {@link workbox-background-sync.Queue~size}
  478. * @return {Promise<number>}
  479. */
  480. async size() {
  481. return await this._queueDb.getEntryCountByQueueName(this._queueName);
  482. }
  483. /**
  484. * Deletes the entry for the given ID.
  485. *
  486. * WARNING: this method does not ensure the deleted entry belongs to this
  487. * queue (i.e. matches the `queueName`). But this limitation is acceptable
  488. * as this class is not publicly exposed. An additional check would make
  489. * this method slower than it needs to be.
  490. *
  491. * @param {number} id
  492. */
  493. async deleteEntry(id) {
  494. await this._queueDb.deleteEntry(id);
  495. }
  496. /**
  497. * Removes and returns the first or last entry in the queue (based on the
  498. * `direction` argument) matching the `queueName`.
  499. *
  500. * @return {Promise<QueueStoreEntry|undefined>}
  501. * @private
  502. */
  503. async _removeEntry(entry) {
  504. if (entry) {
  505. await this.deleteEntry(entry.id);
  506. }
  507. return entry;
  508. }
  509. }
  510. /*
  511. Copyright 2018 Google LLC
  512. Use of this source code is governed by an MIT-style
  513. license that can be found in the LICENSE file or at
  514. https://opensource.org/licenses/MIT.
  515. */
  516. const serializableProperties = ['method', 'referrer', 'referrerPolicy', 'mode', 'credentials', 'cache', 'redirect', 'integrity', 'keepalive'];
  517. /**
  518. * A class to make it easier to serialize and de-serialize requests so they
  519. * can be stored in IndexedDB.
  520. *
  521. * Most developers will not need to access this class directly;
  522. * it is exposed for advanced use cases.
  523. */
  524. class StorableRequest {
  525. /**
  526. * Converts a Request object to a plain object that can be structured
  527. * cloned or JSON-stringified.
  528. *
  529. * @param {Request} request
  530. * @return {Promise<StorableRequest>}
  531. */
  532. static async fromRequest(request) {
  533. const requestData = {
  534. url: request.url,
  535. headers: {}
  536. }; // Set the body if present.
  537. if (request.method !== 'GET') {
  538. // Use ArrayBuffer to support non-text request bodies.
  539. // NOTE: we can't use Blobs becuse Safari doesn't support storing
  540. // Blobs in IndexedDB in some cases:
  541. // https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457
  542. requestData.body = await request.clone().arrayBuffer();
  543. } // Convert the headers from an iterable to an object.
  544. for (const [key, value] of request.headers.entries()) {
  545. requestData.headers[key] = value;
  546. } // Add all other serializable request properties
  547. for (const prop of serializableProperties) {
  548. if (request[prop] !== undefined) {
  549. requestData[prop] = request[prop];
  550. }
  551. }
  552. return new StorableRequest(requestData);
  553. }
  554. /**
  555. * Accepts an object of request data that can be used to construct a
  556. * `Request` but can also be stored in IndexedDB.
  557. *
  558. * @param {Object} requestData An object of request data that includes the
  559. * `url` plus any relevant properties of
  560. * [requestInit]{@link https://fetch.spec.whatwg.org/#requestinit}.
  561. */
  562. constructor(requestData) {
  563. {
  564. assert_js.assert.isType(requestData, 'object', {
  565. moduleName: 'workbox-background-sync',
  566. className: 'StorableRequest',
  567. funcName: 'constructor',
  568. paramName: 'requestData'
  569. });
  570. assert_js.assert.isType(requestData.url, 'string', {
  571. moduleName: 'workbox-background-sync',
  572. className: 'StorableRequest',
  573. funcName: 'constructor',
  574. paramName: 'requestData.url'
  575. });
  576. } // If the request's mode is `navigate`, convert it to `same-origin` since
  577. // navigation requests can't be constructed via script.
  578. if (requestData['mode'] === 'navigate') {
  579. requestData['mode'] = 'same-origin';
  580. }
  581. this._requestData = requestData;
  582. }
  583. /**
  584. * Returns a deep clone of the instances `_requestData` object.
  585. *
  586. * @return {Object}
  587. */
  588. toObject() {
  589. const requestData = Object.assign({}, this._requestData);
  590. requestData.headers = Object.assign({}, this._requestData.headers);
  591. if (requestData.body) {
  592. requestData.body = requestData.body.slice(0);
  593. }
  594. return requestData;
  595. }
  596. /**
  597. * Converts this instance to a Request.
  598. *
  599. * @return {Request}
  600. */
  601. toRequest() {
  602. return new Request(this._requestData.url, this._requestData);
  603. }
  604. /**
  605. * Creates and returns a deep clone of the instance.
  606. *
  607. * @return {StorableRequest}
  608. */
  609. clone() {
  610. return new StorableRequest(this.toObject());
  611. }
  612. }
  613. /*
  614. Copyright 2018 Google LLC
  615. Use of this source code is governed by an MIT-style
  616. license that can be found in the LICENSE file or at
  617. https://opensource.org/licenses/MIT.
  618. */
  619. const TAG_PREFIX = 'workbox-background-sync';
  620. const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
  621. const queueNames = new Set();
  622. /**
  623. * Converts a QueueStore entry into the format exposed by Queue. This entails
  624. * converting the request data into a real request and omitting the `id` and
  625. * `queueName` properties.
  626. *
  627. * @param {UnidentifiedQueueStoreEntry} queueStoreEntry
  628. * @return {Queue}
  629. * @private
  630. */
  631. const convertEntry = queueStoreEntry => {
  632. const queueEntry = {
  633. request: new StorableRequest(queueStoreEntry.requestData).toRequest(),
  634. timestamp: queueStoreEntry.timestamp
  635. };
  636. if (queueStoreEntry.metadata) {
  637. queueEntry.metadata = queueStoreEntry.metadata;
  638. }
  639. return queueEntry;
  640. };
  641. /**
  642. * A class to manage storing failed requests in IndexedDB and retrying them
  643. * later. All parts of the storing and replaying process are observable via
  644. * callbacks.
  645. *
  646. * @memberof workbox-background-sync
  647. */
  648. class Queue {
  649. /**
  650. * Creates an instance of Queue with the given options
  651. *
  652. * @param {string} name The unique name for this queue. This name must be
  653. * unique as it's used to register sync events and store requests
  654. * in IndexedDB specific to this instance. An error will be thrown if
  655. * a duplicate name is detected.
  656. * @param {Object} [options]
  657. * @param {Function} [options.onSync] A function that gets invoked whenever
  658. * the 'sync' event fires. The function is invoked with an object
  659. * containing the `queue` property (referencing this instance), and you
  660. * can use the callback to customize the replay behavior of the queue.
  661. * When not set the `replayRequests()` method is called.
  662. * Note: if the replay fails after a sync event, make sure you throw an
  663. * error, so the browser knows to retry the sync event later.
  664. * @param {number} [options.maxRetentionTime=7 days] The amount of time (in
  665. * minutes) a request may be retried. After this amount of time has
  666. * passed, the request will be deleted from the queue.
  667. * @param {boolean} [options.forceSyncFallback=false] If `true`, instead
  668. * of attempting to use background sync events, always attempt to replay
  669. * queued request at service worker startup. Most folks will not need
  670. * this, unless you explicitly target a runtime like Electron that
  671. * exposes the interfaces for background sync, but does not have a working
  672. * implementation.
  673. */
  674. constructor(name, {
  675. forceSyncFallback,
  676. onSync,
  677. maxRetentionTime
  678. } = {}) {
  679. this._syncInProgress = false;
  680. this._requestsAddedDuringSync = false; // Ensure the store name is not already being used
  681. if (queueNames.has(name)) {
  682. throw new WorkboxError_js.WorkboxError('duplicate-queue-name', {
  683. name
  684. });
  685. } else {
  686. queueNames.add(name);
  687. }
  688. this._name = name;
  689. this._onSync = onSync || this.replayRequests;
  690. this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
  691. this._forceSyncFallback = Boolean(forceSyncFallback);
  692. this._queueStore = new QueueStore(this._name);
  693. this._addSyncListener();
  694. }
  695. /**
  696. * @return {string}
  697. */
  698. get name() {
  699. return this._name;
  700. }
  701. /**
  702. * Stores the passed request in IndexedDB (with its timestamp and any
  703. * metadata) at the end of the queue.
  704. *
  705. * @param {QueueEntry} entry
  706. * @param {Request} entry.request The request to store in the queue.
  707. * @param {Object} [entry.metadata] Any metadata you want associated with the
  708. * stored request. When requests are replayed you'll have access to this
  709. * metadata object in case you need to modify the request beforehand.
  710. * @param {number} [entry.timestamp] The timestamp (Epoch time in
  711. * milliseconds) when the request was first added to the queue. This is
  712. * used along with `maxRetentionTime` to remove outdated requests. In
  713. * general you don't need to set this value, as it's automatically set
  714. * for you (defaulting to `Date.now()`), but you can update it if you
  715. * don't want particular requests to expire.
  716. */
  717. async pushRequest(entry) {
  718. {
  719. assert_js.assert.isType(entry, 'object', {
  720. moduleName: 'workbox-background-sync',
  721. className: 'Queue',
  722. funcName: 'pushRequest',
  723. paramName: 'entry'
  724. });
  725. assert_js.assert.isInstance(entry.request, Request, {
  726. moduleName: 'workbox-background-sync',
  727. className: 'Queue',
  728. funcName: 'pushRequest',
  729. paramName: 'entry.request'
  730. });
  731. }
  732. await this._addRequest(entry, 'push');
  733. }
  734. /**
  735. * Stores the passed request in IndexedDB (with its timestamp and any
  736. * metadata) at the beginning of the queue.
  737. *
  738. * @param {QueueEntry} entry
  739. * @param {Request} entry.request The request to store in the queue.
  740. * @param {Object} [entry.metadata] Any metadata you want associated with the
  741. * stored request. When requests are replayed you'll have access to this
  742. * metadata object in case you need to modify the request beforehand.
  743. * @param {number} [entry.timestamp] The timestamp (Epoch time in
  744. * milliseconds) when the request was first added to the queue. This is
  745. * used along with `maxRetentionTime` to remove outdated requests. In
  746. * general you don't need to set this value, as it's automatically set
  747. * for you (defaulting to `Date.now()`), but you can update it if you
  748. * don't want particular requests to expire.
  749. */
  750. async unshiftRequest(entry) {
  751. {
  752. assert_js.assert.isType(entry, 'object', {
  753. moduleName: 'workbox-background-sync',
  754. className: 'Queue',
  755. funcName: 'unshiftRequest',
  756. paramName: 'entry'
  757. });
  758. assert_js.assert.isInstance(entry.request, Request, {
  759. moduleName: 'workbox-background-sync',
  760. className: 'Queue',
  761. funcName: 'unshiftRequest',
  762. paramName: 'entry.request'
  763. });
  764. }
  765. await this._addRequest(entry, 'unshift');
  766. }
  767. /**
  768. * Removes and returns the last request in the queue (along with its
  769. * timestamp and any metadata). The returned object takes the form:
  770. * `{request, timestamp, metadata}`.
  771. *
  772. * @return {Promise<QueueEntry | undefined>}
  773. */
  774. async popRequest() {
  775. return this._removeRequest('pop');
  776. }
  777. /**
  778. * Removes and returns the first request in the queue (along with its
  779. * timestamp and any metadata). The returned object takes the form:
  780. * `{request, timestamp, metadata}`.
  781. *
  782. * @return {Promise<QueueEntry | undefined>}
  783. */
  784. async shiftRequest() {
  785. return this._removeRequest('shift');
  786. }
  787. /**
  788. * Returns all the entries that have not expired (per `maxRetentionTime`).
  789. * Any expired entries are removed from the queue.
  790. *
  791. * @return {Promise<Array<QueueEntry>>}
  792. */
  793. async getAll() {
  794. const allEntries = await this._queueStore.getAll();
  795. const now = Date.now();
  796. const unexpiredEntries = [];
  797. for (const entry of allEntries) {
  798. // Ignore requests older than maxRetentionTime. Call this function
  799. // recursively until an unexpired request is found.
  800. const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
  801. if (now - entry.timestamp > maxRetentionTimeInMs) {
  802. await this._queueStore.deleteEntry(entry.id);
  803. } else {
  804. unexpiredEntries.push(convertEntry(entry));
  805. }
  806. }
  807. return unexpiredEntries;
  808. }
  809. /**
  810. * Returns the number of entries present in the queue.
  811. * Note that expired entries (per `maxRetentionTime`) are also included in this count.
  812. *
  813. * @return {Promise<number>}
  814. */
  815. async size() {
  816. return await this._queueStore.size();
  817. }
  818. /**
  819. * Adds the entry to the QueueStore and registers for a sync event.
  820. *
  821. * @param {Object} entry
  822. * @param {Request} entry.request
  823. * @param {Object} [entry.metadata]
  824. * @param {number} [entry.timestamp=Date.now()]
  825. * @param {string} operation ('push' or 'unshift')
  826. * @private
  827. */
  828. async _addRequest({
  829. request,
  830. metadata,
  831. timestamp = Date.now()
  832. }, operation) {
  833. const storableRequest = await StorableRequest.fromRequest(request.clone());
  834. const entry = {
  835. requestData: storableRequest.toObject(),
  836. timestamp
  837. }; // Only include metadata if it's present.
  838. if (metadata) {
  839. entry.metadata = metadata;
  840. }
  841. switch (operation) {
  842. case 'push':
  843. await this._queueStore.pushEntry(entry);
  844. break;
  845. case 'unshift':
  846. await this._queueStore.unshiftEntry(entry);
  847. break;
  848. }
  849. {
  850. logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(request.url)}' has ` + `been added to background sync queue '${this._name}'.`);
  851. } // Don't register for a sync if we're in the middle of a sync. Instead,
  852. // we wait until the sync is complete and call register if
  853. // `this._requestsAddedDuringSync` is true.
  854. if (this._syncInProgress) {
  855. this._requestsAddedDuringSync = true;
  856. } else {
  857. await this.registerSync();
  858. }
  859. }
  860. /**
  861. * Removes and returns the first or last (depending on `operation`) entry
  862. * from the QueueStore that's not older than the `maxRetentionTime`.
  863. *
  864. * @param {string} operation ('pop' or 'shift')
  865. * @return {Object|undefined}
  866. * @private
  867. */
  868. async _removeRequest(operation) {
  869. const now = Date.now();
  870. let entry;
  871. switch (operation) {
  872. case 'pop':
  873. entry = await this._queueStore.popEntry();
  874. break;
  875. case 'shift':
  876. entry = await this._queueStore.shiftEntry();
  877. break;
  878. }
  879. if (entry) {
  880. // Ignore requests older than maxRetentionTime. Call this function
  881. // recursively until an unexpired request is found.
  882. const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
  883. if (now - entry.timestamp > maxRetentionTimeInMs) {
  884. return this._removeRequest(operation);
  885. }
  886. return convertEntry(entry);
  887. } else {
  888. return undefined;
  889. }
  890. }
  891. /**
  892. * Loops through each request in the queue and attempts to re-fetch it.
  893. * If any request fails to re-fetch, it's put back in the same position in
  894. * the queue (which registers a retry for the next sync event).
  895. */
  896. async replayRequests() {
  897. let entry;
  898. while (entry = await this.shiftRequest()) {
  899. try {
  900. await fetch(entry.request.clone());
  901. if ("dev" !== 'production') {
  902. logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}' ` + `has been replayed in queue '${this._name}'`);
  903. }
  904. } catch (error) {
  905. await this.unshiftRequest(entry);
  906. {
  907. logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}' ` + `failed to replay, putting it back in queue '${this._name}'`);
  908. }
  909. throw new WorkboxError_js.WorkboxError('queue-replay-failed', {
  910. name: this._name
  911. });
  912. }
  913. }
  914. {
  915. logger_js.logger.log(`All requests in queue '${this.name}' have successfully ` + `replayed; the queue is now empty!`);
  916. }
  917. }
  918. /**
  919. * Registers a sync event with a tag unique to this instance.
  920. */
  921. async registerSync() {
  922. // See https://github.com/GoogleChrome/workbox/issues/2393
  923. if ('sync' in self.registration && !this._forceSyncFallback) {
  924. try {
  925. await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`);
  926. } catch (err) {
  927. // This means the registration failed for some reason, possibly due to
  928. // the user disabling it.
  929. {
  930. logger_js.logger.warn(`Unable to register sync event for '${this._name}'.`, err);
  931. }
  932. }
  933. }
  934. }
  935. /**
  936. * In sync-supporting browsers, this adds a listener for the sync event.
  937. * In non-sync-supporting browsers, or if _forceSyncFallback is true, this
  938. * will retry the queue on service worker startup.
  939. *
  940. * @private
  941. */
  942. _addSyncListener() {
  943. // See https://github.com/GoogleChrome/workbox/issues/2393
  944. if ('sync' in self.registration && !this._forceSyncFallback) {
  945. self.addEventListener('sync', event => {
  946. if (event.tag === `${TAG_PREFIX}:${this._name}`) {
  947. {
  948. logger_js.logger.log(`Background sync for tag '${event.tag}' ` + `has been received`);
  949. }
  950. const syncComplete = async () => {
  951. this._syncInProgress = true;
  952. let syncError;
  953. try {
  954. await this._onSync({
  955. queue: this
  956. });
  957. } catch (error) {
  958. if (error instanceof Error) {
  959. syncError = error; // Rethrow the error. Note: the logic in the finally clause
  960. // will run before this gets rethrown.
  961. throw syncError;
  962. }
  963. } finally {
  964. // New items may have been added to the queue during the sync,
  965. // so we need to register for a new sync if that's happened...
  966. // Unless there was an error during the sync, in which
  967. // case the browser will automatically retry later, as long
  968. // as `event.lastChance` is not true.
  969. if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) {
  970. await this.registerSync();
  971. }
  972. this._syncInProgress = false;
  973. this._requestsAddedDuringSync = false;
  974. }
  975. };
  976. event.waitUntil(syncComplete());
  977. }
  978. });
  979. } else {
  980. {
  981. logger_js.logger.log(`Background sync replaying without background sync event`);
  982. } // If the browser doesn't support background sync, or the developer has
  983. // opted-in to not using it, retry every time the service worker starts up
  984. // as a fallback.
  985. void this._onSync({
  986. queue: this
  987. });
  988. }
  989. }
  990. /**
  991. * Returns the set of queue names. This is primarily used to reset the list
  992. * of queue names in tests.
  993. *
  994. * @return {Set<string>}
  995. *
  996. * @private
  997. */
  998. static get _queueNames() {
  999. return queueNames;
  1000. }
  1001. }
  1002. /*
  1003. Copyright 2018 Google LLC
  1004. Use of this source code is governed by an MIT-style
  1005. license that can be found in the LICENSE file or at
  1006. https://opensource.org/licenses/MIT.
  1007. */
  1008. /**
  1009. * A class implementing the `fetchDidFail` lifecycle callback. This makes it
  1010. * easier to add failed requests to a background sync Queue.
  1011. *
  1012. * @memberof workbox-background-sync
  1013. */
  1014. class BackgroundSyncPlugin {
  1015. /**
  1016. * @param {string} name See the {@link workbox-background-sync.Queue}
  1017. * documentation for parameter details.
  1018. * @param {Object} [options] See the
  1019. * {@link workbox-background-sync.Queue} documentation for
  1020. * parameter details.
  1021. */
  1022. constructor(name, options) {
  1023. /**
  1024. * @param {Object} options
  1025. * @param {Request} options.request
  1026. * @private
  1027. */
  1028. this.fetchDidFail = async ({
  1029. request
  1030. }) => {
  1031. await this._queue.pushRequest({
  1032. request
  1033. });
  1034. };
  1035. this._queue = new Queue(name, options);
  1036. }
  1037. }
  1038. exports.BackgroundSyncPlugin = BackgroundSyncPlugin;
  1039. exports.Queue = Queue;
  1040. exports.QueueStore = QueueStore;
  1041. exports.StorableRequest = StorableRequest;
  1042. return exports;
  1043. }({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
  1044. //# sourceMappingURL=workbox-background-sync.dev.js.map