source: frontend/node_modules/workbox-background-sync/Queue.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 16.2 KB
Line 
1/*
2 Copyright 2018 Google LLC
3
4 Use of this source code is governed by an MIT-style
5 license that can be found in the LICENSE file or at
6 https://opensource.org/licenses/MIT.
7*/
8import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
9import { logger } from 'workbox-core/_private/logger.js';
10import { assert } from 'workbox-core/_private/assert.js';
11import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
12import { QueueStore } from './lib/QueueStore.js';
13import { StorableRequest } from './lib/StorableRequest.js';
14import './_version.js';
15const TAG_PREFIX = 'workbox-background-sync';
16const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
17const queueNames = new Set();
18/**
19 * Converts a QueueStore entry into the format exposed by Queue. This entails
20 * converting the request data into a real request and omitting the `id` and
21 * `queueName` properties.
22 *
23 * @param {UnidentifiedQueueStoreEntry} queueStoreEntry
24 * @return {Queue}
25 * @private
26 */
27const convertEntry = (queueStoreEntry) => {
28 const queueEntry = {
29 request: new StorableRequest(queueStoreEntry.requestData).toRequest(),
30 timestamp: queueStoreEntry.timestamp,
31 };
32 if (queueStoreEntry.metadata) {
33 queueEntry.metadata = queueStoreEntry.metadata;
34 }
35 return queueEntry;
36};
37/**
38 * A class to manage storing failed requests in IndexedDB and retrying them
39 * later. All parts of the storing and replaying process are observable via
40 * callbacks.
41 *
42 * @memberof workbox-background-sync
43 */
44class Queue {
45 /**
46 * Creates an instance of Queue with the given options
47 *
48 * @param {string} name The unique name for this queue. This name must be
49 * unique as it's used to register sync events and store requests
50 * in IndexedDB specific to this instance. An error will be thrown if
51 * a duplicate name is detected.
52 * @param {Object} [options]
53 * @param {Function} [options.onSync] A function that gets invoked whenever
54 * the 'sync' event fires. The function is invoked with an object
55 * containing the `queue` property (referencing this instance), and you
56 * can use the callback to customize the replay behavior of the queue.
57 * When not set the `replayRequests()` method is called.
58 * Note: if the replay fails after a sync event, make sure you throw an
59 * error, so the browser knows to retry the sync event later.
60 * @param {number} [options.maxRetentionTime=7 days] The amount of time (in
61 * minutes) a request may be retried. After this amount of time has
62 * passed, the request will be deleted from the queue.
63 * @param {boolean} [options.forceSyncFallback=false] If `true`, instead
64 * of attempting to use background sync events, always attempt to replay
65 * queued request at service worker startup. Most folks will not need
66 * this, unless you explicitly target a runtime like Electron that
67 * exposes the interfaces for background sync, but does not have a working
68 * implementation.
69 */
70 constructor(name, { forceSyncFallback, onSync, maxRetentionTime } = {}) {
71 this._syncInProgress = false;
72 this._requestsAddedDuringSync = false;
73 // Ensure the store name is not already being used
74 if (queueNames.has(name)) {
75 throw new WorkboxError('duplicate-queue-name', { name });
76 }
77 else {
78 queueNames.add(name);
79 }
80 this._name = name;
81 this._onSync = onSync || this.replayRequests;
82 this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
83 this._forceSyncFallback = Boolean(forceSyncFallback);
84 this._queueStore = new QueueStore(this._name);
85 this._addSyncListener();
86 }
87 /**
88 * @return {string}
89 */
90 get name() {
91 return this._name;
92 }
93 /**
94 * Stores the passed request in IndexedDB (with its timestamp and any
95 * metadata) at the end of the queue.
96 *
97 * @param {QueueEntry} entry
98 * @param {Request} entry.request The request to store in the queue.
99 * @param {Object} [entry.metadata] Any metadata you want associated with the
100 * stored request. When requests are replayed you'll have access to this
101 * metadata object in case you need to modify the request beforehand.
102 * @param {number} [entry.timestamp] The timestamp (Epoch time in
103 * milliseconds) when the request was first added to the queue. This is
104 * used along with `maxRetentionTime` to remove outdated requests. In
105 * general you don't need to set this value, as it's automatically set
106 * for you (defaulting to `Date.now()`), but you can update it if you
107 * don't want particular requests to expire.
108 */
109 async pushRequest(entry) {
110 if (process.env.NODE_ENV !== 'production') {
111 assert.isType(entry, 'object', {
112 moduleName: 'workbox-background-sync',
113 className: 'Queue',
114 funcName: 'pushRequest',
115 paramName: 'entry',
116 });
117 assert.isInstance(entry.request, Request, {
118 moduleName: 'workbox-background-sync',
119 className: 'Queue',
120 funcName: 'pushRequest',
121 paramName: 'entry.request',
122 });
123 }
124 await this._addRequest(entry, 'push');
125 }
126 /**
127 * Stores the passed request in IndexedDB (with its timestamp and any
128 * metadata) at the beginning of the queue.
129 *
130 * @param {QueueEntry} entry
131 * @param {Request} entry.request The request to store in the queue.
132 * @param {Object} [entry.metadata] Any metadata you want associated with the
133 * stored request. When requests are replayed you'll have access to this
134 * metadata object in case you need to modify the request beforehand.
135 * @param {number} [entry.timestamp] The timestamp (Epoch time in
136 * milliseconds) when the request was first added to the queue. This is
137 * used along with `maxRetentionTime` to remove outdated requests. In
138 * general you don't need to set this value, as it's automatically set
139 * for you (defaulting to `Date.now()`), but you can update it if you
140 * don't want particular requests to expire.
141 */
142 async unshiftRequest(entry) {
143 if (process.env.NODE_ENV !== 'production') {
144 assert.isType(entry, 'object', {
145 moduleName: 'workbox-background-sync',
146 className: 'Queue',
147 funcName: 'unshiftRequest',
148 paramName: 'entry',
149 });
150 assert.isInstance(entry.request, Request, {
151 moduleName: 'workbox-background-sync',
152 className: 'Queue',
153 funcName: 'unshiftRequest',
154 paramName: 'entry.request',
155 });
156 }
157 await this._addRequest(entry, 'unshift');
158 }
159 /**
160 * Removes and returns the last request in the queue (along with its
161 * timestamp and any metadata). The returned object takes the form:
162 * `{request, timestamp, metadata}`.
163 *
164 * @return {Promise<QueueEntry | undefined>}
165 */
166 async popRequest() {
167 return this._removeRequest('pop');
168 }
169 /**
170 * Removes and returns the first request in the queue (along with its
171 * timestamp and any metadata). The returned object takes the form:
172 * `{request, timestamp, metadata}`.
173 *
174 * @return {Promise<QueueEntry | undefined>}
175 */
176 async shiftRequest() {
177 return this._removeRequest('shift');
178 }
179 /**
180 * Returns all the entries that have not expired (per `maxRetentionTime`).
181 * Any expired entries are removed from the queue.
182 *
183 * @return {Promise<Array<QueueEntry>>}
184 */
185 async getAll() {
186 const allEntries = await this._queueStore.getAll();
187 const now = Date.now();
188 const unexpiredEntries = [];
189 for (const entry of allEntries) {
190 // Ignore requests older than maxRetentionTime. Call this function
191 // recursively until an unexpired request is found.
192 const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
193 if (now - entry.timestamp > maxRetentionTimeInMs) {
194 await this._queueStore.deleteEntry(entry.id);
195 }
196 else {
197 unexpiredEntries.push(convertEntry(entry));
198 }
199 }
200 return unexpiredEntries;
201 }
202 /**
203 * Returns the number of entries present in the queue.
204 * Note that expired entries (per `maxRetentionTime`) are also included in this count.
205 *
206 * @return {Promise<number>}
207 */
208 async size() {
209 return await this._queueStore.size();
210 }
211 /**
212 * Adds the entry to the QueueStore and registers for a sync event.
213 *
214 * @param {Object} entry
215 * @param {Request} entry.request
216 * @param {Object} [entry.metadata]
217 * @param {number} [entry.timestamp=Date.now()]
218 * @param {string} operation ('push' or 'unshift')
219 * @private
220 */
221 async _addRequest({ request, metadata, timestamp = Date.now() }, operation) {
222 const storableRequest = await StorableRequest.fromRequest(request.clone());
223 const entry = {
224 requestData: storableRequest.toObject(),
225 timestamp,
226 };
227 // Only include metadata if it's present.
228 if (metadata) {
229 entry.metadata = metadata;
230 }
231 switch (operation) {
232 case 'push':
233 await this._queueStore.pushEntry(entry);
234 break;
235 case 'unshift':
236 await this._queueStore.unshiftEntry(entry);
237 break;
238 }
239 if (process.env.NODE_ENV !== 'production') {
240 logger.log(`Request for '${getFriendlyURL(request.url)}' has ` +
241 `been added to background sync queue '${this._name}'.`);
242 }
243 // Don't register for a sync if we're in the middle of a sync. Instead,
244 // we wait until the sync is complete and call register if
245 // `this._requestsAddedDuringSync` is true.
246 if (this._syncInProgress) {
247 this._requestsAddedDuringSync = true;
248 }
249 else {
250 await this.registerSync();
251 }
252 }
253 /**
254 * Removes and returns the first or last (depending on `operation`) entry
255 * from the QueueStore that's not older than the `maxRetentionTime`.
256 *
257 * @param {string} operation ('pop' or 'shift')
258 * @return {Object|undefined}
259 * @private
260 */
261 async _removeRequest(operation) {
262 const now = Date.now();
263 let entry;
264 switch (operation) {
265 case 'pop':
266 entry = await this._queueStore.popEntry();
267 break;
268 case 'shift':
269 entry = await this._queueStore.shiftEntry();
270 break;
271 }
272 if (entry) {
273 // Ignore requests older than maxRetentionTime. Call this function
274 // recursively until an unexpired request is found.
275 const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
276 if (now - entry.timestamp > maxRetentionTimeInMs) {
277 return this._removeRequest(operation);
278 }
279 return convertEntry(entry);
280 }
281 else {
282 return undefined;
283 }
284 }
285 /**
286 * Loops through each request in the queue and attempts to re-fetch it.
287 * If any request fails to re-fetch, it's put back in the same position in
288 * the queue (which registers a retry for the next sync event).
289 */
290 async replayRequests() {
291 let entry;
292 while ((entry = await this.shiftRequest())) {
293 try {
294 await fetch(entry.request.clone());
295 if (process.env.NODE_ENV !== 'production') {
296 logger.log(`Request for '${getFriendlyURL(entry.request.url)}' ` +
297 `has been replayed in queue '${this._name}'`);
298 }
299 }
300 catch (error) {
301 await this.unshiftRequest(entry);
302 if (process.env.NODE_ENV !== 'production') {
303 logger.log(`Request for '${getFriendlyURL(entry.request.url)}' ` +
304 `failed to replay, putting it back in queue '${this._name}'`);
305 }
306 throw new WorkboxError('queue-replay-failed', { name: this._name });
307 }
308 }
309 if (process.env.NODE_ENV !== 'production') {
310 logger.log(`All requests in queue '${this.name}' have successfully ` +
311 `replayed; the queue is now empty!`);
312 }
313 }
314 /**
315 * Registers a sync event with a tag unique to this instance.
316 */
317 async registerSync() {
318 // See https://github.com/GoogleChrome/workbox/issues/2393
319 if ('sync' in self.registration && !this._forceSyncFallback) {
320 try {
321 await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`);
322 }
323 catch (err) {
324 // This means the registration failed for some reason, possibly due to
325 // the user disabling it.
326 if (process.env.NODE_ENV !== 'production') {
327 logger.warn(`Unable to register sync event for '${this._name}'.`, err);
328 }
329 }
330 }
331 }
332 /**
333 * In sync-supporting browsers, this adds a listener for the sync event.
334 * In non-sync-supporting browsers, or if _forceSyncFallback is true, this
335 * will retry the queue on service worker startup.
336 *
337 * @private
338 */
339 _addSyncListener() {
340 // See https://github.com/GoogleChrome/workbox/issues/2393
341 if ('sync' in self.registration && !this._forceSyncFallback) {
342 self.addEventListener('sync', (event) => {
343 if (event.tag === `${TAG_PREFIX}:${this._name}`) {
344 if (process.env.NODE_ENV !== 'production') {
345 logger.log(`Background sync for tag '${event.tag}' ` + `has been received`);
346 }
347 const syncComplete = async () => {
348 this._syncInProgress = true;
349 let syncError;
350 try {
351 await this._onSync({ queue: this });
352 }
353 catch (error) {
354 if (error instanceof Error) {
355 syncError = error;
356 // Rethrow the error. Note: the logic in the finally clause
357 // will run before this gets rethrown.
358 throw syncError;
359 }
360 }
361 finally {
362 // New items may have been added to the queue during the sync,
363 // so we need to register for a new sync if that's happened...
364 // Unless there was an error during the sync, in which
365 // case the browser will automatically retry later, as long
366 // as `event.lastChance` is not true.
367 if (this._requestsAddedDuringSync &&
368 !(syncError && !event.lastChance)) {
369 await this.registerSync();
370 }
371 this._syncInProgress = false;
372 this._requestsAddedDuringSync = false;
373 }
374 };
375 event.waitUntil(syncComplete());
376 }
377 });
378 }
379 else {
380 if (process.env.NODE_ENV !== 'production') {
381 logger.log(`Background sync replaying without background sync event`);
382 }
383 // If the browser doesn't support background sync, or the developer has
384 // opted-in to not using it, retry every time the service worker starts up
385 // as a fallback.
386 void this._onSync({ queue: this });
387 }
388 }
389 /**
390 * Returns the set of queue names. This is primarily used to reset the list
391 * of queue names in tests.
392 *
393 * @return {Set<string>}
394 *
395 * @private
396 */
397 static get _queueNames() {
398 return queueNames;
399 }
400}
401export { Queue };
Note: See TracBrowser for help on using the repository browser.