Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

250 строки
8.4 KiB

  1. /*
  2. Copyright 2017 Google Inc. All Rights Reserved.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. import {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';
  14. import {logger} from 'workbox-core/_private/logger.mjs';
  15. import {assert} from 'workbox-core/_private/assert.mjs';
  16. import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.mjs';
  17. import {QueueStore} from './models/QueueStore.mjs';
  18. import StorableRequest from './models/StorableRequest.mjs';
  19. import {TAG_PREFIX, MAX_RETENTION_TIME} from './utils/constants.mjs';
  20. import './_version.mjs';
  21. const queueNames = new Set();
  22. /**
  23. * A class to manage storing failed requests in IndexedDB and retrying them
  24. * later. All parts of the storing and replaying process are observable via
  25. * callbacks.
  26. *
  27. * @memberof workbox.backgroundSync
  28. */
  29. class Queue {
  30. /**
  31. * Creates an instance of Queue with the given options
  32. *
  33. * @param {string} name The unique name for this queue. This name must be
  34. * unique as it's used to register sync events and store requests
  35. * in IndexedDB specific to this instance. An error will be thrown if
  36. * a duplicate name is detected.
  37. * @param {Object} [options]
  38. * @param {Object} [options.callbacks] Callbacks to observe the lifecycle of
  39. * queued requests. Use these to respond to or modify the requests
  40. * during the replay process.
  41. * @param {function(StorableRequest):undefined}
  42. * [options.callbacks.requestWillEnqueue]
  43. * Invoked immediately before the request is stored to IndexedDB. Use
  44. * this callback to modify request data at store time.
  45. * @param {function(StorableRequest):undefined}
  46. * [options.callbacks.requestWillReplay]
  47. * Invoked immediately before the request is re-fetched. Use this
  48. * callback to modify request data at fetch time.
  49. * @param {function(Array<StorableRequest>):undefined}
  50. * [options.callbacks.queueDidReplay]
  51. * Invoked after all requests in the queue have successfully replayed.
  52. * @param {number} [options.maxRetentionTime = 7 days] The amount of time (in
  53. * minutes) a request may be retried. After this amount of time has
  54. * passed, the request will be deleted from the queue.
  55. */
  56. constructor(name, {
  57. callbacks = {},
  58. maxRetentionTime = MAX_RETENTION_TIME,
  59. } = {}) {
  60. // Ensure the store name is not already being used
  61. if (queueNames.has(name)) {
  62. throw new WorkboxError('duplicate-queue-name', {name});
  63. } else {
  64. queueNames.add(name);
  65. }
  66. this._name = name;
  67. this._callbacks = callbacks;
  68. this._maxRetentionTime = maxRetentionTime;
  69. this._queueStore = new QueueStore(this);
  70. this._addSyncListener();
  71. }
  72. /**
  73. * @return {string}
  74. */
  75. get name() {
  76. return this._name;
  77. }
  78. /**
  79. * Stores the passed request into IndexedDB. The database used is
  80. * `workbox-background-sync` and the object store name is the same as
  81. * the name this instance was created with (to guarantee it's unique).
  82. *
  83. * @param {Request} request The request object to store.
  84. */
  85. async addRequest(request) {
  86. if (process.env.NODE_ENV !== 'production') {
  87. assert.isInstance(request, Request, {
  88. moduleName: 'workbox-background-sync',
  89. className: 'Queue',
  90. funcName: 'addRequest',
  91. paramName: 'request',
  92. });
  93. }
  94. const storableRequest = await StorableRequest.fromRequest(request.clone());
  95. await this._runCallback('requestWillEnqueue', storableRequest);
  96. await this._queueStore.addEntry(storableRequest);
  97. await this._registerSync();
  98. if (process.env.NODE_ENV !== 'production') {
  99. logger.log(`Request for '${getFriendlyURL(storableRequest.url)}' has been
  100. added to background sync queue '${this._name}'.`);
  101. }
  102. }
  103. /**
  104. * Retrieves all stored requests in IndexedDB and retries them. If the
  105. * queue contained requests that were successfully replayed, the
  106. * `queueDidReplay` callback is invoked (which implies the queue is
  107. * now empty). If any of the requests fail, a new sync registration is
  108. * created to retry again later.
  109. */
  110. async replayRequests() {
  111. const now = Date.now();
  112. const replayedRequests = [];
  113. const failedRequests = [];
  114. let storableRequest;
  115. while (storableRequest = await this._queueStore.getAndRemoveOldestEntry()) {
  116. // Make a copy so the unmodified request can be stored
  117. // in the event of a replay failure.
  118. const storableRequestClone = storableRequest.clone();
  119. // Ignore requests older than maxRetentionTime.
  120. const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
  121. if (now - storableRequest.timestamp > maxRetentionTimeInMs) {
  122. continue;
  123. }
  124. await this._runCallback('requestWillReplay', storableRequest);
  125. const replay = {request: storableRequest.toRequest()};
  126. try {
  127. // Clone the request before fetching so callbacks get an unused one.
  128. replay.response = await fetch(replay.request.clone());
  129. if (process.env.NODE_ENV !== 'production') {
  130. logger.log(`Request for '${getFriendlyURL(storableRequest.url)}'
  131. has been replayed`);
  132. }
  133. } catch (err) {
  134. if (process.env.NODE_ENV !== 'production') {
  135. logger.log(`Request for '${getFriendlyURL(storableRequest.url)}'
  136. failed to replay`);
  137. }
  138. replay.error = err;
  139. failedRequests.push(storableRequestClone);
  140. }
  141. replayedRequests.push(replay);
  142. }
  143. await this._runCallback('queueDidReplay', replayedRequests);
  144. // If any requests failed, put the failed requests back in the queue
  145. // and rethrow the failed requests count.
  146. if (failedRequests.length) {
  147. await Promise.all(failedRequests.map((storableRequest) => {
  148. return this._queueStore.addEntry(storableRequest);
  149. }));
  150. throw new WorkboxError('queue-replay-failed',
  151. {name: this._name, count: failedRequests.length});
  152. }
  153. }
  154. /**
  155. * Runs the passed callback if it exists.
  156. *
  157. * @private
  158. * @param {string} name The name of the callback on this._callbacks.
  159. * @param {...*} args The arguments to invoke the callback with.
  160. */
  161. async _runCallback(name, ...args) {
  162. if (typeof this._callbacks[name] === 'function') {
  163. await this._callbacks[name].apply(null, args);
  164. }
  165. }
  166. /**
  167. * In sync-supporting browsers, this adds a listener for the sync event.
  168. * In non-sync-supporting browsers, this will retry the queue on service
  169. * worker startup.
  170. *
  171. * @private
  172. */
  173. _addSyncListener() {
  174. if ('sync' in registration) {
  175. self.addEventListener('sync', (event) => {
  176. if (event.tag === `${TAG_PREFIX}:${this._name}`) {
  177. if (process.env.NODE_ENV !== 'production') {
  178. logger.log(`Background sync for tag '${event.tag}'
  179. has been received, starting replay now`);
  180. }
  181. event.waitUntil(this.replayRequests());
  182. }
  183. });
  184. } else {
  185. if (process.env.NODE_ENV !== 'production') {
  186. logger.log(`Background sync replaying without background sync event`);
  187. }
  188. // If the browser doesn't support background sync, retry
  189. // every time the service worker starts up as a fallback.
  190. this.replayRequests();
  191. }
  192. }
  193. /**
  194. * Registers a sync event with a tag unique to this instance.
  195. *
  196. * @private
  197. */
  198. async _registerSync() {
  199. if ('sync' in registration) {
  200. try {
  201. await registration.sync.register(`${TAG_PREFIX}:${this._name}`);
  202. } catch (err) {
  203. // This means the registration failed for some reason, possibly due to
  204. // the user disabling it.
  205. if (process.env.NODE_ENV !== 'production') {
  206. logger.warn(
  207. `Unable to register sync event for '${this._name}'.`, err);
  208. }
  209. }
  210. }
  211. }
  212. /**
  213. * Returns the set of queue names. This is primarily used to reset the list
  214. * of queue names in tests.
  215. *
  216. * @return {Set}
  217. *
  218. * @private
  219. */
  220. static get _queueNames() {
  221. return queueNames;
  222. }
  223. }
  224. export {Queue};