Index: frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,27 @@
+import { WorkboxPlugin } from 'workbox-core/types.js';
+import { QueueOptions } from './Queue.js';
+import './_version.js';
+/**
+ * A class implementing the `fetchDidFail` lifecycle callback. This makes it
+ * easier to add failed requests to a background sync Queue.
+ *
+ * @memberof workbox-background-sync
+ */
+declare class BackgroundSyncPlugin implements WorkboxPlugin {
+    private readonly _queue;
+    /**
+     * @param {string} name See the {@link workbox-background-sync.Queue}
+     *     documentation for parameter details.
+     * @param {Object} [options] See the
+     *     {@link workbox-background-sync.Queue} documentation for
+     *     parameter details.
+     */
+    constructor(name: string, options?: QueueOptions);
+    /**
+     * @param {Object} options
+     * @param {Request} options.request
+     * @private
+     */
+    fetchDidFail: WorkboxPlugin['fetchDidFail'];
+}
+export { BackgroundSyncPlugin };
Index: frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.js
===================================================================
--- frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,36 @@
+/*
+  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.
+*/
+import { Queue } from './Queue.js';
+import './_version.js';
+/**
+ * A class implementing the `fetchDidFail` lifecycle callback. This makes it
+ * easier to add failed requests to a background sync Queue.
+ *
+ * @memberof workbox-background-sync
+ */
+class BackgroundSyncPlugin {
+    /**
+     * @param {string} name See the {@link workbox-background-sync.Queue}
+     *     documentation for parameter details.
+     * @param {Object} [options] See the
+     *     {@link 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);
+    }
+}
+export { BackgroundSyncPlugin };
Index: frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/BackgroundSyncPlugin.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './BackgroundSyncPlugin.js';
Index: frontend/node_modules/workbox-background-sync/LICENSE
===================================================================
--- frontend/node_modules/workbox-background-sync/LICENSE	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/LICENSE	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,19 @@
+Copyright 2018 Google LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
Index: frontend/node_modules/workbox-background-sync/Queue.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/Queue.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/Queue.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,175 @@
+import './_version.js';
+interface OnSyncCallbackOptions {
+    queue: Queue;
+}
+interface OnSyncCallback {
+    (options: OnSyncCallbackOptions): void | Promise<void>;
+}
+export interface QueueOptions {
+    forceSyncFallback?: boolean;
+    maxRetentionTime?: number;
+    onSync?: OnSyncCallback;
+}
+interface QueueEntry {
+    request: Request;
+    timestamp?: number;
+    metadata?: object;
+}
+/**
+ * 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 workbox-background-sync
+ */
+declare class Queue {
+    private readonly _name;
+    private readonly _onSync;
+    private readonly _maxRetentionTime;
+    private readonly _queueStore;
+    private readonly _forceSyncFallback;
+    private _syncInProgress;
+    private _requestsAddedDuringSync;
+    /**
+     * 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.
+     * @param {boolean} [options.forceSyncFallback=false] If `true`, instead
+     *     of attempting to use background sync events, always attempt to replay
+     *     queued request at service worker startup. Most folks will not need
+     *     this, unless you explicitly target a runtime like Electron that
+     *     exposes the interfaces for background sync, but does not have a working
+     *     implementation.
+     */
+    constructor(name: string, { forceSyncFallback, onSync, maxRetentionTime }?: QueueOptions);
+    /**
+     * @return {string}
+     */
+    get name(): string;
+    /**
+     * Stores the passed request in IndexedDB (with its timestamp and any
+     * metadata) at the end of the queue.
+     *
+     * @param {QueueEntry} 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.
+     */
+    pushRequest(entry: QueueEntry): Promise<void>;
+    /**
+     * Stores the passed request in IndexedDB (with its timestamp and any
+     * metadata) at the beginning of the queue.
+     *
+     * @param {QueueEntry} 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.
+     */
+    unshiftRequest(entry: QueueEntry): Promise<void>;
+    /**
+     * 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<QueueEntry | undefined>}
+     */
+    popRequest(): Promise<QueueEntry | undefined>;
+    /**
+     * 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<QueueEntry | undefined>}
+     */
+    shiftRequest(): Promise<QueueEntry | undefined>;
+    /**
+     * Returns all the entries that have not expired (per `maxRetentionTime`).
+     * Any expired entries are removed from the queue.
+     *
+     * @return {Promise<Array<QueueEntry>>}
+     */
+    getAll(): Promise<Array<QueueEntry>>;
+    /**
+     * Returns the number of entries present in the queue.
+     * Note that expired entries (per `maxRetentionTime`) are also included in this count.
+     *
+     * @return {Promise<number>}
+     */
+    size(): Promise<number>;
+    /**
+     * 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
+     */
+    _addRequest({ request, metadata, timestamp }: QueueEntry, operation: 'push' | 'unshift'): Promise<void>;
+    /**
+     * 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
+     */
+    _removeRequest(operation: 'pop' | 'shift'): Promise<QueueEntry | 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).
+     */
+    replayRequests(): Promise<void>;
+    /**
+     * Registers a sync event with a tag unique to this instance.
+     */
+    registerSync(): Promise<void>;
+    /**
+     * In sync-supporting browsers, this adds a listener for the sync event.
+     * In non-sync-supporting browsers, or if _forceSyncFallback is true, this
+     * will retry the queue on service worker startup.
+     *
+     * @private
+     */
+    private _addSyncListener;
+    /**
+     * Returns the set of queue names. This is primarily used to reset the list
+     * of queue names in tests.
+     *
+     * @return {Set<string>}
+     *
+     * @private
+     */
+    static get _queueNames(): Set<string>;
+}
+export { Queue };
Index: frontend/node_modules/workbox-background-sync/Queue.js
===================================================================
--- frontend/node_modules/workbox-background-sync/Queue.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/Queue.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,401 @@
+/*
+  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.
+*/
+import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
+import { logger } from 'workbox-core/_private/logger.js';
+import { assert } from 'workbox-core/_private/assert.js';
+import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
+import { QueueStore } from './lib/QueueStore.js';
+import { StorableRequest } from './lib/StorableRequest.js';
+import './_version.js';
+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 {UnidentifiedQueueStoreEntry} queueStoreEntry
+ * @return {Queue}
+ * @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 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.
+     * @param {boolean} [options.forceSyncFallback=false] If `true`, instead
+     *     of attempting to use background sync events, always attempt to replay
+     *     queued request at service worker startup. Most folks will not need
+     *     this, unless you explicitly target a runtime like Electron that
+     *     exposes the interfaces for background sync, but does not have a working
+     *     implementation.
+     */
+    constructor(name, { forceSyncFallback, onSync, maxRetentionTime } = {}) {
+        this._syncInProgress = false;
+        this._requestsAddedDuringSync = false;
+        // Ensure the store name is not already being used
+        if (queueNames.has(name)) {
+            throw new WorkboxError('duplicate-queue-name', { name });
+        }
+        else {
+            queueNames.add(name);
+        }
+        this._name = name;
+        this._onSync = onSync || this.replayRequests;
+        this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
+        this._forceSyncFallback = Boolean(forceSyncFallback);
+        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 {QueueEntry} 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) {
+        if (process.env.NODE_ENV !== 'production') {
+            assert.isType(entry, 'object', {
+                moduleName: 'workbox-background-sync',
+                className: 'Queue',
+                funcName: 'pushRequest',
+                paramName: 'entry',
+            });
+            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 {QueueEntry} 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) {
+        if (process.env.NODE_ENV !== 'production') {
+            assert.isType(entry, 'object', {
+                moduleName: 'workbox-background-sync',
+                className: 'Queue',
+                funcName: 'unshiftRequest',
+                paramName: 'entry',
+            });
+            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<QueueEntry | undefined>}
+     */
+    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<QueueEntry | undefined>}
+     */
+    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<Array<QueueEntry>>}
+     */
+    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;
+    }
+    /**
+     * Returns the number of entries present in the queue.
+     * Note that expired entries (per `maxRetentionTime`) are also included in this count.
+     *
+     * @return {Promise<number>}
+     */
+    async size() {
+        return await this._queueStore.size();
+    }
+    /**
+     * 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;
+        }
+        switch (operation) {
+            case 'push':
+                await this._queueStore.pushEntry(entry);
+                break;
+            case 'unshift':
+                await this._queueStore.unshiftEntry(entry);
+                break;
+        }
+        if (process.env.NODE_ENV !== 'production') {
+            logger.log(`Request for '${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();
+        let entry;
+        switch (operation) {
+            case 'pop':
+                entry = await this._queueStore.popEntry();
+                break;
+            case 'shift':
+                entry = await this._queueStore.shiftEntry();
+                break;
+        }
+        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 (process.env.NODE_ENV !== 'production') {
+                    logger.log(`Request for '${getFriendlyURL(entry.request.url)}' ` +
+                        `has been replayed in queue '${this._name}'`);
+                }
+            }
+            catch (error) {
+                await this.unshiftRequest(entry);
+                if (process.env.NODE_ENV !== 'production') {
+                    logger.log(`Request for '${getFriendlyURL(entry.request.url)}' ` +
+                        `failed to replay, putting it back in queue '${this._name}'`);
+                }
+                throw new WorkboxError('queue-replay-failed', { name: this._name });
+            }
+        }
+        if (process.env.NODE_ENV !== 'production') {
+            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() {
+        // See https://github.com/GoogleChrome/workbox/issues/2393
+        if ('sync' in self.registration && !this._forceSyncFallback) {
+            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.
+                if (process.env.NODE_ENV !== 'production') {
+                    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, or if _forceSyncFallback is true, this
+     * will retry the queue on service worker startup.
+     *
+     * @private
+     */
+    _addSyncListener() {
+        // See https://github.com/GoogleChrome/workbox/issues/2393
+        if ('sync' in self.registration && !this._forceSyncFallback) {
+            self.addEventListener('sync', (event) => {
+                if (event.tag === `${TAG_PREFIX}:${this._name}`) {
+                    if (process.env.NODE_ENV !== 'production') {
+                        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) {
+                            if (error instanceof 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 {
+            if (process.env.NODE_ENV !== 'production') {
+                logger.log(`Background sync replaying without background sync event`);
+            }
+            // If the browser doesn't support background sync, or the developer has
+            // opted-in to not using it, retry every time the service worker starts up
+            // as a fallback.
+            void 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<string>}
+     *
+     * @private
+     */
+    static get _queueNames() {
+        return queueNames;
+    }
+}
+export { Queue };
Index: frontend/node_modules/workbox-background-sync/Queue.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/Queue.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/Queue.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './Queue.js';
Index: frontend/node_modules/workbox-background-sync/QueueStore.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/QueueStore.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/QueueStore.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,2 @@
+import './_version.js';
+export { QueueStore } from './lib/QueueStore';
Index: frontend/node_modules/workbox-background-sync/QueueStore.js
===================================================================
--- frontend/node_modules/workbox-background-sync/QueueStore.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/QueueStore.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,12 @@
+/*
+  Copyright 2021 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.
+*/
+import './_version.js';
+// This is a temporary workaround to expose something from ./lib/ via our
+// top-level public API.
+// TODO: In Workbox v7, move the actual code from ./lib/ to this file.
+export { QueueStore } from './lib/QueueStore';
Index: frontend/node_modules/workbox-background-sync/QueueStore.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/QueueStore.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/QueueStore.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './QueueStore.js';
Index: frontend/node_modules/workbox-background-sync/README.md
===================================================================
--- frontend/node_modules/workbox-background-sync/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-background-sync
Index: frontend/node_modules/workbox-background-sync/StorableRequest.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/StorableRequest.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/StorableRequest.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,2 @@
+import './_version.js';
+export { StorableRequest } from './lib/StorableRequest';
Index: frontend/node_modules/workbox-background-sync/StorableRequest.js
===================================================================
--- frontend/node_modules/workbox-background-sync/StorableRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/StorableRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,12 @@
+/*
+  Copyright 2021 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.
+*/
+import './_version.js';
+// This is a temporary workaround to expose something from ./lib/ via our
+// top-level public API.
+// TODO: In Workbox v7, move the actual code from ./lib/ to this file.
+export { StorableRequest } from './lib/StorableRequest';
Index: frontend/node_modules/workbox-background-sync/StorableRequest.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/StorableRequest.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/StorableRequest.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './StorableRequest.js';
Index: frontend/node_modules/workbox-background-sync/_version.js
===================================================================
--- frontend/node_modules/workbox-background-sync/_version.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/_version.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,6 @@
+"use strict";
+// @ts-ignore
+try {
+    self['workbox:background-sync:6.5.4'] && _();
+}
+catch (e) { }
Index: frontend/node_modules/workbox-background-sync/_version.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/_version.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/_version.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+try{self['workbox:background-sync:6.6.0']&&_()}catch(e){}// eslint-disable-line
Index: frontend/node_modules/workbox-background-sync/index.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/index.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/index.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,25 @@
+import { BackgroundSyncPlugin } from './BackgroundSyncPlugin.js';
+import { Queue, QueueOptions } from './Queue.js';
+import { QueueStore } from './QueueStore.js';
+import { StorableRequest } from './StorableRequest.js';
+import './_version.js';
+interface SyncManager {
+    getTags(): Promise<string[]>;
+    register(tag: string): Promise<void>;
+}
+declare global {
+    interface ServiceWorkerRegistration {
+        readonly sync: SyncManager;
+    }
+    interface SyncEvent extends ExtendableEvent {
+        readonly lastChance: boolean;
+        readonly tag: string;
+    }
+    interface ServiceWorkerGlobalScopeEventMap {
+        sync: SyncEvent;
+    }
+}
+/**
+ * @module workbox-background-sync
+ */
+export { BackgroundSyncPlugin, Queue, QueueOptions, QueueStore, StorableRequest };
Index: frontend/node_modules/workbox-background-sync/index.js
===================================================================
--- frontend/node_modules/workbox-background-sync/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,16 @@
+/*
+  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.
+*/
+import { BackgroundSyncPlugin } from './BackgroundSyncPlugin.js';
+import { Queue } from './Queue.js';
+import { QueueStore } from './QueueStore.js';
+import { StorableRequest } from './StorableRequest.js';
+import './_version.js';
+/**
+ * @module workbox-background-sync
+ */
+export { BackgroundSyncPlugin, Queue, QueueStore, StorableRequest };
Index: frontend/node_modules/workbox-background-sync/index.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/index.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/index.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './index.js';
Index: frontend/node_modules/workbox-background-sync/lib/QueueDb.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/QueueDb.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/QueueDb.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,90 @@
+import { RequestData } from './StorableRequest.js';
+import '../_version.js';
+export interface UnidentifiedQueueStoreEntry {
+    requestData: RequestData;
+    timestamp: number;
+    id?: number;
+    queueName?: string;
+    metadata?: object;
+}
+export interface QueueStoreEntry extends UnidentifiedQueueStoreEntry {
+    id: number;
+}
+/**
+ * A class to interact directly an IndexedDB created specifically to save and
+ * retrieve QueueStoreEntries. This class encapsulates all the schema details
+ * to store the representation of a Queue.
+ *
+ * @private
+ */
+export declare class QueueDb {
+    private _db;
+    /**
+     * Add QueueStoreEntry to underlying db.
+     *
+     * @param {UnidentifiedQueueStoreEntry} entry
+     */
+    addEntry(entry: UnidentifiedQueueStoreEntry): Promise<void>;
+    /**
+     * Returns the first entry id in the ObjectStore.
+     *
+     * @return {number | undefined}
+     */
+    getFirstEntryId(): Promise<number | undefined>;
+    /**
+     * Get all the entries filtered by index
+     *
+     * @param queueName
+     * @return {Promise<QueueStoreEntry[]>}
+     */
+    getAllEntriesByQueueName(queueName: string): Promise<QueueStoreEntry[]>;
+    /**
+     * Returns the number of entries filtered by index
+     *
+     * @param queueName
+     * @return {Promise<number>}
+     */
+    getEntryCountByQueueName(queueName: string): Promise<number>;
+    /**
+     * Deletes a single entry by id.
+     *
+     * @param {number} id the id of the entry to be deleted
+     */
+    deleteEntry(id: number): Promise<void>;
+    /**
+     *
+     * @param queueName
+     * @returns {Promise<QueueStoreEntry | undefined>}
+     */
+    getFirstEntryByQueueName(queueName: string): Promise<QueueStoreEntry | undefined>;
+    /**
+     *
+     * @param queueName
+     * @returns {Promise<QueueStoreEntry | undefined>}
+     */
+    getLastEntryByQueueName(queueName: string): Promise<QueueStoreEntry | undefined>;
+    /**
+     * Returns either the first or the last entries, depending on direction.
+     * Filtered by index.
+     *
+     * @param {IDBCursorDirection} direction
+     * @param {IDBKeyRange} query
+     * @return {Promise<QueueStoreEntry | undefined>}
+     * @private
+     */
+    getEndEntryFromIndex(query: IDBKeyRange, direction: IDBCursorDirection): Promise<QueueStoreEntry | undefined>;
+    /**
+     * Returns an open connection to the database.
+     *
+     * @private
+     */
+    private getDb;
+    /**
+     * Upgrades QueueDB
+     *
+     * @param {IDBPDatabase<QueueDBSchema>} db
+     * @param {number} oldVersion
+     * @private
+     */
+    private _upgradeDb;
+}
Index: frontend/node_modules/workbox-background-sync/lib/QueueDb.js
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/QueueDb.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/QueueDb.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,145 @@
+/*
+  Copyright 2021 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.
+*/
+import { openDB } from 'idb';
+import '../_version.js';
+const DB_VERSION = 3;
+const DB_NAME = 'workbox-background-sync';
+const REQUEST_OBJECT_STORE_NAME = 'requests';
+const QUEUE_NAME_INDEX = 'queueName';
+/**
+ * A class to interact directly an IndexedDB created specifically to save and
+ * retrieve QueueStoreEntries. This class encapsulates all the schema details
+ * to store the representation of a Queue.
+ *
+ * @private
+ */
+export class QueueDb {
+    constructor() {
+        this._db = null;
+    }
+    /**
+     * Add QueueStoreEntry to underlying db.
+     *
+     * @param {UnidentifiedQueueStoreEntry} entry
+     */
+    async addEntry(entry) {
+        const db = await this.getDb();
+        const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, 'readwrite', {
+            durability: 'relaxed',
+        });
+        await tx.store.add(entry);
+        await tx.done;
+    }
+    /**
+     * Returns the first entry id in the ObjectStore.
+     *
+     * @return {number | undefined}
+     */
+    async getFirstEntryId() {
+        const db = await this.getDb();
+        const cursor = await db
+            .transaction(REQUEST_OBJECT_STORE_NAME)
+            .store.openCursor();
+        return cursor === null || cursor === void 0 ? void 0 : cursor.value.id;
+    }
+    /**
+     * Get all the entries filtered by index
+     *
+     * @param queueName
+     * @return {Promise<QueueStoreEntry[]>}
+     */
+    async getAllEntriesByQueueName(queueName) {
+        const db = await this.getDb();
+        const results = await db.getAllFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
+        return results ? results : new Array();
+    }
+    /**
+     * Returns the number of entries filtered by index
+     *
+     * @param queueName
+     * @return {Promise<number>}
+     */
+    async getEntryCountByQueueName(queueName) {
+        const db = await this.getDb();
+        return db.countFromIndex(REQUEST_OBJECT_STORE_NAME, QUEUE_NAME_INDEX, IDBKeyRange.only(queueName));
+    }
+    /**
+     * Deletes a single entry by id.
+     *
+     * @param {number} id the id of the entry to be deleted
+     */
+    async deleteEntry(id) {
+        const db = await this.getDb();
+        await db.delete(REQUEST_OBJECT_STORE_NAME, id);
+    }
+    /**
+     *
+     * @param queueName
+     * @returns {Promise<QueueStoreEntry | undefined>}
+     */
+    async getFirstEntryByQueueName(queueName) {
+        return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'next');
+    }
+    /**
+     *
+     * @param queueName
+     * @returns {Promise<QueueStoreEntry | undefined>}
+     */
+    async getLastEntryByQueueName(queueName) {
+        return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'prev');
+    }
+    /**
+     * Returns either the first or the last entries, depending on direction.
+     * Filtered by index.
+     *
+     * @param {IDBCursorDirection} direction
+     * @param {IDBKeyRange} query
+     * @return {Promise<QueueStoreEntry | undefined>}
+     * @private
+     */
+    async getEndEntryFromIndex(query, direction) {
+        const db = await this.getDb();
+        const cursor = await db
+            .transaction(REQUEST_OBJECT_STORE_NAME)
+            .store.index(QUEUE_NAME_INDEX)
+            .openCursor(query, direction);
+        return cursor === null || cursor === void 0 ? void 0 : cursor.value;
+    }
+    /**
+     * Returns an open connection to the database.
+     *
+     * @private
+     */
+    async getDb() {
+        if (!this._db) {
+            this._db = await openDB(DB_NAME, DB_VERSION, {
+                upgrade: this._upgradeDb,
+            });
+        }
+        return this._db;
+    }
+    /**
+     * Upgrades QueueDB
+     *
+     * @param {IDBPDatabase<QueueDBSchema>} db
+     * @param {number} oldVersion
+     * @private
+     */
+    _upgradeDb(db, oldVersion) {
+        if (oldVersion > 0 && oldVersion < DB_VERSION) {
+            if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
+                db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
+            }
+        }
+        const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
+            autoIncrement: true,
+            keyPath: 'id',
+        });
+        objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, { unique: false });
+    }
+}
Index: frontend/node_modules/workbox-background-sync/lib/QueueDb.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/QueueDb.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/QueueDb.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './QueueDb.js';
Index: frontend/node_modules/workbox-background-sync/lib/QueueStore.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/QueueStore.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/QueueStore.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,83 @@
+import { UnidentifiedQueueStoreEntry, QueueStoreEntry } from './QueueDb.js';
+import '../_version.js';
+/**
+ * A class to manage storing requests from a Queue in IndexedDB,
+ * indexed by their queue name for easier access.
+ *
+ * Most developers will not need to access this class directly;
+ * it is exposed for advanced use cases.
+ */
+export declare class QueueStore {
+    private readonly _queueName;
+    private readonly _queueDb;
+    /**
+     * Associates this instance with a Queue instance, so entries added can be
+     * identified by their queue name.
+     *
+     * @param {string} queueName
+     */
+    constructor(queueName: string);
+    /**
+     * Append an entry last in the queue.
+     *
+     * @param {Object} entry
+     * @param {Object} entry.requestData
+     * @param {number} [entry.timestamp]
+     * @param {Object} [entry.metadata]
+     */
+    pushEntry(entry: UnidentifiedQueueStoreEntry): Promise<void>;
+    /**
+     * Prepend an entry first in the queue.
+     *
+     * @param {Object} entry
+     * @param {Object} entry.requestData
+     * @param {number} [entry.timestamp]
+     * @param {Object} [entry.metadata]
+     */
+    unshiftEntry(entry: UnidentifiedQueueStoreEntry): Promise<void>;
+    /**
+     * Removes and returns the last entry in the queue matching the `queueName`.
+     *
+     * @return {Promise<QueueStoreEntry|undefined>}
+     */
+    popEntry(): Promise<QueueStoreEntry | undefined>;
+    /**
+     * Removes and returns the first entry in the queue matching the `queueName`.
+     *
+     * @return {Promise<QueueStoreEntry|undefined>}
+     */
+    shiftEntry(): Promise<QueueStoreEntry | undefined>;
+    /**
+     * Returns all entries in the store matching the `queueName`.
+     *
+     * @param {Object} options See {@link workbox-background-sync.Queue~getAll}
+     * @return {Promise<Array<Object>>}
+     */
+    getAll(): Promise<QueueStoreEntry[]>;
+    /**
+     * Returns the number of entries in the store matching the `queueName`.
+     *
+     * @param {Object} options See {@link workbox-background-sync.Queue~size}
+     * @return {Promise<number>}
+     */
+    size(): Promise<number>;
+    /**
+     * Deletes the entry for the given ID.
+     *
+     * WARNING: this method does not ensure the deleted entry 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.
+     *
+     * @param {number} id
+     */
+    deleteEntry(id: number): Promise<void>;
+    /**
+     * Removes and returns the first or last entry in the queue (based on the
+     * `direction` argument) matching the `queueName`.
+     *
+     * @return {Promise<QueueStoreEntry|undefined>}
+     * @private
+     */
+    _removeEntry(entry?: QueueStoreEntry): Promise<QueueStoreEntry | undefined>;
+}
Index: frontend/node_modules/workbox-background-sync/lib/QueueStore.js
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/QueueStore.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/QueueStore.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,152 @@
+/*
+  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.
+*/
+import { assert } from 'workbox-core/_private/assert.js';
+import { QueueDb, } from './QueueDb.js';
+import '../_version.js';
+/**
+ * A class to manage storing requests from a Queue in IndexedDB,
+ * indexed by their queue name for easier access.
+ *
+ * Most developers will not need to access this class directly;
+ * it is exposed for advanced use cases.
+ */
+export class QueueStore {
+    /**
+     * Associates this instance with a Queue instance, so entries added can be
+     * identified by their queue name.
+     *
+     * @param {string} queueName
+     */
+    constructor(queueName) {
+        this._queueName = queueName;
+        this._queueDb = new QueueDb();
+    }
+    /**
+     * Append an entry last in the queue.
+     *
+     * @param {Object} entry
+     * @param {Object} entry.requestData
+     * @param {number} [entry.timestamp]
+     * @param {Object} [entry.metadata]
+     */
+    async pushEntry(entry) {
+        if (process.env.NODE_ENV !== 'production') {
+            assert.isType(entry, 'object', {
+                moduleName: 'workbox-background-sync',
+                className: 'QueueStore',
+                funcName: 'pushEntry',
+                paramName: 'entry',
+            });
+            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._queueDb.addEntry(entry);
+    }
+    /**
+     * Prepend an entry first in the queue.
+     *
+     * @param {Object} entry
+     * @param {Object} entry.requestData
+     * @param {number} [entry.timestamp]
+     * @param {Object} [entry.metadata]
+     */
+    async unshiftEntry(entry) {
+        if (process.env.NODE_ENV !== 'production') {
+            assert.isType(entry, 'object', {
+                moduleName: 'workbox-background-sync',
+                className: 'QueueStore',
+                funcName: 'unshiftEntry',
+                paramName: 'entry',
+            });
+            assert.isType(entry.requestData, 'object', {
+                moduleName: 'workbox-background-sync',
+                className: 'QueueStore',
+                funcName: 'unshiftEntry',
+                paramName: 'entry.requestData',
+            });
+        }
+        const firstId = await this._queueDb.getFirstEntryId();
+        if (firstId) {
+            // Pick an ID one less than the lowest ID in the object store.
+            entry.id = firstId - 1;
+        }
+        else {
+            // Otherwise let the auto-incrementor assign the ID.
+            delete entry.id;
+        }
+        entry.queueName = this._queueName;
+        await this._queueDb.addEntry(entry);
+    }
+    /**
+     * Removes and returns the last entry in the queue matching the `queueName`.
+     *
+     * @return {Promise<QueueStoreEntry|undefined>}
+     */
+    async popEntry() {
+        return this._removeEntry(await this._queueDb.getLastEntryByQueueName(this._queueName));
+    }
+    /**
+     * Removes and returns the first entry in the queue matching the `queueName`.
+     *
+     * @return {Promise<QueueStoreEntry|undefined>}
+     */
+    async shiftEntry() {
+        return this._removeEntry(await this._queueDb.getFirstEntryByQueueName(this._queueName));
+    }
+    /**
+     * Returns all entries in the store matching the `queueName`.
+     *
+     * @param {Object} options See {@link workbox-background-sync.Queue~getAll}
+     * @return {Promise<Array<Object>>}
+     */
+    async getAll() {
+        return await this._queueDb.getAllEntriesByQueueName(this._queueName);
+    }
+    /**
+     * Returns the number of entries in the store matching the `queueName`.
+     *
+     * @param {Object} options See {@link workbox-background-sync.Queue~size}
+     * @return {Promise<number>}
+     */
+    async size() {
+        return await this._queueDb.getEntryCountByQueueName(this._queueName);
+    }
+    /**
+     * Deletes the entry for the given ID.
+     *
+     * WARNING: this method does not ensure the deleted entry 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.
+     *
+     * @param {number} id
+     */
+    async deleteEntry(id) {
+        await this._queueDb.deleteEntry(id);
+    }
+    /**
+     * Removes and returns the first or last entry in the queue (based on the
+     * `direction` argument) matching the `queueName`.
+     *
+     * @return {Promise<QueueStoreEntry|undefined>}
+     * @private
+     */
+    async _removeEntry(entry) {
+        if (entry) {
+            await this.deleteEntry(entry.id);
+        }
+        return entry;
+    }
+}
Index: frontend/node_modules/workbox-background-sync/lib/QueueStore.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/QueueStore.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/QueueStore.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './QueueStore.js';
Index: frontend/node_modules/workbox-background-sync/lib/StorableRequest.d.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/StorableRequest.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/StorableRequest.d.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,53 @@
+import { MapLikeObject } from 'workbox-core/types.js';
+import '../_version.js';
+export interface RequestData extends MapLikeObject {
+    url: string;
+    headers: MapLikeObject;
+    body?: ArrayBuffer;
+}
+/**
+ * A class to make it easier to serialize and de-serialize requests so they
+ * can be stored in IndexedDB.
+ *
+ * Most developers will not need to access this class directly;
+ * it is exposed for advanced use cases.
+ */
+declare class StorableRequest {
+    private readonly _requestData;
+    /**
+     * Converts a Request object to a plain object that can be structured
+     * cloned or JSON-stringified.
+     *
+     * @param {Request} request
+     * @return {Promise<StorableRequest>}
+     */
+    static fromRequest(request: Request): Promise<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}.
+     */
+    constructor(requestData: RequestData);
+    /**
+     * Returns a deep clone of the instances `_requestData` object.
+     *
+     * @return {Object}
+     */
+    toObject(): RequestData;
+    /**
+     * Converts this instance to a Request.
+     *
+     * @return {Request}
+     */
+    toRequest(): Request;
+    /**
+     * Creates and returns a deep clone of the instance.
+     *
+     * @return {StorableRequest}
+     */
+    clone(): StorableRequest;
+}
+export { StorableRequest };
Index: frontend/node_modules/workbox-background-sync/lib/StorableRequest.js
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/StorableRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/StorableRequest.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,121 @@
+/*
+  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.
+*/
+import { assert } from 'workbox-core/_private/assert.js';
+import '../_version.js';
+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.
+ *
+ * Most developers will not need to access this class directly;
+ * it is exposed for advanced use cases.
+ */
+class StorableRequest {
+    /**
+     * Converts a Request object to a plain object that can be structured
+     * cloned or JSON-stringified.
+     *
+     * @param {Request} request
+     * @return {Promise<StorableRequest>}
+     */
+    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);
+    }
+    /**
+     * 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}.
+     */
+    constructor(requestData) {
+        if (process.env.NODE_ENV !== 'production') {
+            assert.isType(requestData, 'object', {
+                moduleName: 'workbox-background-sync',
+                className: 'StorableRequest',
+                funcName: 'constructor',
+                paramName: 'requestData',
+            });
+            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;
+    }
+    /**
+     * Returns a deep clone of the instances `_requestData` object.
+     *
+     * @return {Object}
+     */
+    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}
+     */
+    toRequest() {
+        return new Request(this._requestData.url, this._requestData);
+    }
+    /**
+     * Creates and returns a deep clone of the instance.
+     *
+     * @return {StorableRequest}
+     */
+    clone() {
+        return new StorableRequest(this.toObject());
+    }
+}
+export { StorableRequest };
Index: frontend/node_modules/workbox-background-sync/lib/StorableRequest.mjs
===================================================================
--- frontend/node_modules/workbox-background-sync/lib/StorableRequest.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/lib/StorableRequest.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+export * from './StorableRequest.js';
Index: frontend/node_modules/workbox-background-sync/package.json
===================================================================
--- frontend/node_modules/workbox-background-sync/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,31 @@
+{
+  "name": "workbox-background-sync",
+  "version": "6.6.0",
+  "license": "MIT",
+  "author": "Google's Web DevRel Team",
+  "description": "Queues failed requests and uses the Background Sync API to replay them when the network is available",
+  "repository": "googlechrome/workbox",
+  "bugs": "https://github.com/googlechrome/workbox/issues",
+  "homepage": "https://github.com/GoogleChrome/workbox",
+  "keywords": [
+    "workbox",
+    "workboxjs",
+    "service worker",
+    "sw",
+    "background",
+    "sync",
+    "workbox-plugin"
+  ],
+  "workbox": {
+    "browserNamespace": "workbox.backgroundSync",
+    "packageType": "sw"
+  },
+  "main": "index.js",
+  "module": "index.mjs",
+  "types": "index.d.ts",
+  "dependencies": {
+    "idb": "^7.0.1",
+    "workbox-core": "6.6.0"
+  },
+  "gitHead": "252644491d9bb5a67518935ede6df530107c9475"
+}
Index: frontend/node_modules/workbox-background-sync/src/BackgroundSyncPlugin.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/BackgroundSyncPlugin.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/BackgroundSyncPlugin.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,43 @@
+/*
+  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.
+*/
+
+import {WorkboxPlugin} from 'workbox-core/types.js';
+import {Queue, QueueOptions} from './Queue.js';
+import './_version.js';
+
+/**
+ * A class implementing the `fetchDidFail` lifecycle callback. This makes it
+ * easier to add failed requests to a background sync Queue.
+ *
+ * @memberof workbox-background-sync
+ */
+class BackgroundSyncPlugin implements WorkboxPlugin {
+  private readonly _queue: Queue;
+
+  /**
+   * @param {string} name See the {@link workbox-background-sync.Queue}
+   *     documentation for parameter details.
+   * @param {Object} [options] See the
+   *     {@link workbox-background-sync.Queue} documentation for
+   *     parameter details.
+   */
+  constructor(name: string, options?: QueueOptions) {
+    this._queue = new Queue(name, options);
+  }
+
+  /**
+   * @param {Object} options
+   * @param {Request} options.request
+   * @private
+   */
+  fetchDidFail: WorkboxPlugin['fetchDidFail'] = async ({request}) => {
+    await this._queue.pushRequest({request});
+  };
+}
+
+export {BackgroundSyncPlugin};
Index: frontend/node_modules/workbox-background-sync/src/Queue.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/Queue.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/Queue.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,487 @@
+/*
+  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.
+*/
+
+import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
+import {logger} from 'workbox-core/_private/logger.js';
+import {assert} from 'workbox-core/_private/assert.js';
+import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
+import {QueueStore} from './lib/QueueStore.js';
+import {QueueStoreEntry, UnidentifiedQueueStoreEntry} from './lib/QueueDb.js';
+import {StorableRequest} from './lib/StorableRequest.js';
+import './_version.js';
+
+// Give TypeScript the correct global.
+declare let self: ServiceWorkerGlobalScope;
+
+interface OnSyncCallbackOptions {
+  queue: Queue;
+}
+
+interface OnSyncCallback {
+  (options: OnSyncCallbackOptions): void | Promise<void>;
+}
+
+export interface QueueOptions {
+  forceSyncFallback?: boolean;
+  maxRetentionTime?: number;
+  onSync?: OnSyncCallback;
+}
+
+interface QueueEntry {
+  request: Request;
+  timestamp?: number;
+  // We could use Record<string, unknown> as a type but that would be a breaking
+  // change, better do it in next major release.
+  // eslint-disable-next-line  @typescript-eslint/ban-types
+  metadata?: object;
+}
+
+const TAG_PREFIX = 'workbox-background-sync';
+const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
+
+const queueNames = new Set<string>();
+
+/**
+ * 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 {UnidentifiedQueueStoreEntry} queueStoreEntry
+ * @return {Queue}
+ * @private
+ */
+const convertEntry = (
+  queueStoreEntry: UnidentifiedQueueStoreEntry,
+): QueueEntry => {
+  const queueEntry: 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 workbox-background-sync
+ */
+class Queue {
+  private readonly _name: string;
+  private readonly _onSync: OnSyncCallback;
+  private readonly _maxRetentionTime: number;
+  private readonly _queueStore: QueueStore;
+  private readonly _forceSyncFallback: boolean;
+  private _syncInProgress = false;
+  private _requestsAddedDuringSync = false;
+
+  /**
+   * 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.
+   * @param {boolean} [options.forceSyncFallback=false] If `true`, instead
+   *     of attempting to use background sync events, always attempt to replay
+   *     queued request at service worker startup. Most folks will not need
+   *     this, unless you explicitly target a runtime like Electron that
+   *     exposes the interfaces for background sync, but does not have a working
+   *     implementation.
+   */
+  constructor(
+    name: string,
+    {forceSyncFallback, onSync, maxRetentionTime}: QueueOptions = {},
+  ) {
+    // Ensure the store name is not already being used
+    if (queueNames.has(name)) {
+      throw new WorkboxError('duplicate-queue-name', {name});
+    } else {
+      queueNames.add(name);
+    }
+
+    this._name = name;
+    this._onSync = onSync || this.replayRequests;
+    this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
+    this._forceSyncFallback = Boolean(forceSyncFallback);
+    this._queueStore = new QueueStore(this._name);
+
+    this._addSyncListener();
+  }
+
+  /**
+   * @return {string}
+   */
+  get name(): string {
+    return this._name;
+  }
+
+  /**
+   * Stores the passed request in IndexedDB (with its timestamp and any
+   * metadata) at the end of the queue.
+   *
+   * @param {QueueEntry} 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: QueueEntry): Promise<void> {
+    if (process.env.NODE_ENV !== 'production') {
+      assert!.isType(entry, 'object', {
+        moduleName: 'workbox-background-sync',
+        className: 'Queue',
+        funcName: 'pushRequest',
+        paramName: 'entry',
+      });
+      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 {QueueEntry} 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: QueueEntry): Promise<void> {
+    if (process.env.NODE_ENV !== 'production') {
+      assert!.isType(entry, 'object', {
+        moduleName: 'workbox-background-sync',
+        className: 'Queue',
+        funcName: 'unshiftRequest',
+        paramName: 'entry',
+      });
+      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<QueueEntry | undefined>}
+   */
+  async popRequest(): Promise<QueueEntry | undefined> {
+    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<QueueEntry | undefined>}
+   */
+  async shiftRequest(): Promise<QueueEntry | undefined> {
+    return this._removeRequest('shift');
+  }
+
+  /**
+   * Returns all the entries that have not expired (per `maxRetentionTime`).
+   * Any expired entries are removed from the queue.
+   *
+   * @return {Promise<Array<QueueEntry>>}
+   */
+  async getAll(): Promise<Array<QueueEntry>> {
+    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;
+  }
+
+  /**
+   * Returns the number of entries present in the queue.
+   * Note that expired entries (per `maxRetentionTime`) are also included in this count.
+   *
+   * @return {Promise<number>}
+   */
+  async size(): Promise<number> {
+    return await this._queueStore.size();
+  }
+
+  /**
+   * 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()}: QueueEntry,
+    operation: 'push' | 'unshift',
+  ): Promise<void> {
+    const storableRequest = await StorableRequest.fromRequest(request.clone());
+    const entry: UnidentifiedQueueStoreEntry = {
+      requestData: storableRequest.toObject(),
+      timestamp,
+    };
+
+    // Only include metadata if it's present.
+    if (metadata) {
+      entry.metadata = metadata;
+    }
+
+    switch (operation) {
+      case 'push':
+        await this._queueStore.pushEntry(entry);
+        break;
+      case 'unshift':
+        await this._queueStore.unshiftEntry(entry);
+        break;
+    }
+
+    if (process.env.NODE_ENV !== 'production') {
+      logger.log(
+        `Request for '${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: 'pop' | 'shift',
+  ): Promise<QueueEntry | undefined> {
+    const now = Date.now();
+    let entry: QueueStoreEntry | undefined;
+    switch (operation) {
+      case 'pop':
+        entry = await this._queueStore.popEntry();
+        break;
+      case 'shift':
+        entry = await this._queueStore.shiftEntry();
+        break;
+    }
+
+    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(): Promise<void> {
+    let entry;
+    while ((entry = await this.shiftRequest())) {
+      try {
+        await fetch(entry.request.clone());
+
+        if (process.env.NODE_ENV !== 'production') {
+          logger.log(
+            `Request for '${getFriendlyURL(entry.request.url)}' ` +
+              `has been replayed in queue '${this._name}'`,
+          );
+        }
+      } catch (error) {
+        await this.unshiftRequest(entry);
+
+        if (process.env.NODE_ENV !== 'production') {
+          logger.log(
+            `Request for '${getFriendlyURL(entry.request.url)}' ` +
+              `failed to replay, putting it back in queue '${this._name}'`,
+          );
+        }
+        throw new WorkboxError('queue-replay-failed', {name: this._name});
+      }
+    }
+    if (process.env.NODE_ENV !== 'production') {
+      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(): Promise<void> {
+    // See https://github.com/GoogleChrome/workbox/issues/2393
+    if ('sync' in self.registration && !this._forceSyncFallback) {
+      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.
+        if (process.env.NODE_ENV !== 'production') {
+          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, or if _forceSyncFallback is true, this
+   * will retry the queue on service worker startup.
+   *
+   * @private
+   */
+  private _addSyncListener() {
+    // See https://github.com/GoogleChrome/workbox/issues/2393
+    if ('sync' in self.registration && !this._forceSyncFallback) {
+      self.addEventListener('sync', (event: SyncEvent) => {
+        if (event.tag === `${TAG_PREFIX}:${this._name}`) {
+          if (process.env.NODE_ENV !== 'production') {
+            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) {
+              if (error instanceof 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 {
+      if (process.env.NODE_ENV !== 'production') {
+        logger.log(`Background sync replaying without background sync event`);
+      }
+      // If the browser doesn't support background sync, or the developer has
+      // opted-in to not using it, retry every time the service worker starts up
+      // as a fallback.
+      void 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<string>}
+   *
+   * @private
+   */
+  static get _queueNames(): Set<string> {
+    return queueNames;
+  }
+}
+
+export {Queue};
Index: frontend/node_modules/workbox-background-sync/src/QueueStore.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/QueueStore.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/QueueStore.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,14 @@
+/*
+  Copyright 2021 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.
+*/
+
+import './_version.js';
+
+// This is a temporary workaround to expose something from ./lib/ via our
+// top-level public API.
+// TODO: In Workbox v7, move the actual code from ./lib/ to this file.
+export {QueueStore} from './lib/QueueStore';
Index: frontend/node_modules/workbox-background-sync/src/StorableRequest.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/StorableRequest.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/StorableRequest.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,14 @@
+/*
+  Copyright 2021 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.
+*/
+
+import './_version.js';
+
+// This is a temporary workaround to expose something from ./lib/ via our
+// top-level public API.
+// TODO: In Workbox v7, move the actual code from ./lib/ to this file.
+export {StorableRequest} from './lib/StorableRequest';
Index: frontend/node_modules/workbox-background-sync/src/_version.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/_version.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/_version.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,2 @@
+// @ts-ignore
+try{self['workbox:background-sync:6.6.0']&&_()}catch(e){}
Index: frontend/node_modules/workbox-background-sync/src/index.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/index.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/index.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,40 @@
+/*
+  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.
+*/
+
+import {BackgroundSyncPlugin} from './BackgroundSyncPlugin.js';
+import {Queue, QueueOptions} from './Queue.js';
+import {QueueStore} from './QueueStore.js';
+import {StorableRequest} from './StorableRequest.js';
+
+import './_version.js';
+
+// See https://github.com/GoogleChrome/workbox/issues/2946
+interface SyncManager {
+  getTags(): Promise<string[]>;
+  register(tag: string): Promise<void>;
+}
+
+declare global {
+  interface ServiceWorkerRegistration {
+    readonly sync: SyncManager;
+  }
+
+  interface SyncEvent extends ExtendableEvent {
+    readonly lastChance: boolean;
+    readonly tag: string;
+  }
+
+  interface ServiceWorkerGlobalScopeEventMap {
+    sync: SyncEvent;
+  }
+}
+
+/**
+ * @module workbox-background-sync
+ */
+export {BackgroundSyncPlugin, Queue, QueueOptions, QueueStore, StorableRequest};
Index: frontend/node_modules/workbox-background-sync/src/lib/QueueDb.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/lib/QueueDb.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/lib/QueueDb.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,200 @@
+/*
+  Copyright 2021 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.
+*/
+
+import {openDB, DBSchema, IDBPDatabase} from 'idb';
+import {RequestData} from './StorableRequest.js';
+import '../_version.js';
+
+interface QueueDBSchema extends DBSchema {
+  requests: {
+    key: number;
+    value: QueueStoreEntry;
+    indexes: {queueName: string};
+  };
+}
+
+const DB_VERSION = 3;
+const DB_NAME = 'workbox-background-sync';
+const REQUEST_OBJECT_STORE_NAME = 'requests';
+const QUEUE_NAME_INDEX = 'queueName';
+
+export interface UnidentifiedQueueStoreEntry {
+  requestData: RequestData;
+  timestamp: number;
+  id?: number;
+  queueName?: string;
+  // We could use Record<string, unknown> as a type but that would be a breaking
+  // change, better do it in next major release.
+  // eslint-disable-next-line  @typescript-eslint/ban-types
+  metadata?: object;
+}
+
+export interface QueueStoreEntry extends UnidentifiedQueueStoreEntry {
+  id: number;
+}
+
+/**
+ * A class to interact directly an IndexedDB created specifically to save and
+ * retrieve QueueStoreEntries. This class encapsulates all the schema details
+ * to store the representation of a Queue.
+ *
+ * @private
+ */
+
+export class QueueDb {
+  private _db: IDBPDatabase<QueueDBSchema> | null = null;
+
+  /**
+   * Add QueueStoreEntry to underlying db.
+   *
+   * @param {UnidentifiedQueueStoreEntry} entry
+   */
+  async addEntry(entry: UnidentifiedQueueStoreEntry): Promise<void> {
+    const db = await this.getDb();
+    const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, 'readwrite', {
+      durability: 'relaxed',
+    });
+    await tx.store.add(entry as QueueStoreEntry);
+    await tx.done;
+  }
+
+  /**
+   * Returns the first entry id in the ObjectStore.
+   *
+   * @return {number | undefined}
+   */
+  async getFirstEntryId(): Promise<number | undefined> {
+    const db = await this.getDb();
+    const cursor = await db
+      .transaction(REQUEST_OBJECT_STORE_NAME)
+      .store.openCursor();
+    return cursor?.value.id;
+  }
+
+  /**
+   * Get all the entries filtered by index
+   *
+   * @param queueName
+   * @return {Promise<QueueStoreEntry[]>}
+   */
+  async getAllEntriesByQueueName(
+    queueName: string,
+  ): Promise<QueueStoreEntry[]> {
+    const db = await this.getDb();
+    const results = await db.getAllFromIndex(
+      REQUEST_OBJECT_STORE_NAME,
+      QUEUE_NAME_INDEX,
+      IDBKeyRange.only(queueName),
+    );
+    return results ? results : new Array<QueueStoreEntry>();
+  }
+
+  /**
+   * Returns the number of entries filtered by index
+   *
+   * @param queueName
+   * @return {Promise<number>}
+   */
+  async getEntryCountByQueueName(queueName: string): Promise<number> {
+    const db = await this.getDb();
+    return db.countFromIndex(
+      REQUEST_OBJECT_STORE_NAME,
+      QUEUE_NAME_INDEX,
+      IDBKeyRange.only(queueName),
+    );
+  }
+
+  /**
+   * Deletes a single entry by id.
+   *
+   * @param {number} id the id of the entry to be deleted
+   */
+  async deleteEntry(id: number): Promise<void> {
+    const db = await this.getDb();
+    await db.delete(REQUEST_OBJECT_STORE_NAME, id);
+  }
+
+  /**
+   *
+   * @param queueName
+   * @returns {Promise<QueueStoreEntry | undefined>}
+   */
+  async getFirstEntryByQueueName(
+    queueName: string,
+  ): Promise<QueueStoreEntry | undefined> {
+    return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'next');
+  }
+
+  /**
+   *
+   * @param queueName
+   * @returns {Promise<QueueStoreEntry | undefined>}
+   */
+  async getLastEntryByQueueName(
+    queueName: string,
+  ): Promise<QueueStoreEntry | undefined> {
+    return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'prev');
+  }
+
+  /**
+   * Returns either the first or the last entries, depending on direction.
+   * Filtered by index.
+   *
+   * @param {IDBCursorDirection} direction
+   * @param {IDBKeyRange} query
+   * @return {Promise<QueueStoreEntry | undefined>}
+   * @private
+   */
+  async getEndEntryFromIndex(
+    query: IDBKeyRange,
+    direction: IDBCursorDirection,
+  ): Promise<QueueStoreEntry | undefined> {
+    const db = await this.getDb();
+
+    const cursor = await db
+      .transaction(REQUEST_OBJECT_STORE_NAME)
+      .store.index(QUEUE_NAME_INDEX)
+      .openCursor(query, direction);
+    return cursor?.value;
+  }
+
+  /**
+   * Returns an open connection to the database.
+   *
+   * @private
+   */
+  private async getDb() {
+    if (!this._db) {
+      this._db = await openDB(DB_NAME, DB_VERSION, {
+        upgrade: this._upgradeDb,
+      });
+    }
+    return this._db;
+  }
+
+  /**
+   * Upgrades QueueDB
+   *
+   * @param {IDBPDatabase<QueueDBSchema>} db
+   * @param {number} oldVersion
+   * @private
+   */
+  private _upgradeDb(db: IDBPDatabase<QueueDBSchema>, oldVersion: number) {
+    if (oldVersion > 0 && oldVersion < DB_VERSION) {
+      if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
+        db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
+      }
+    }
+
+    const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
+      autoIncrement: true,
+      keyPath: 'id',
+    });
+    objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, {unique: false});
+  }
+}
Index: frontend/node_modules/workbox-background-sync/src/lib/QueueStore.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/lib/QueueStore.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/lib/QueueStore.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,179 @@
+/*
+  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.
+*/
+
+import {assert} from 'workbox-core/_private/assert.js';
+import {
+  UnidentifiedQueueStoreEntry,
+  QueueStoreEntry,
+  QueueDb,
+} from './QueueDb.js';
+import '../_version.js';
+
+/**
+ * A class to manage storing requests from a Queue in IndexedDB,
+ * indexed by their queue name for easier access.
+ *
+ * Most developers will not need to access this class directly;
+ * it is exposed for advanced use cases.
+ */
+export class QueueStore {
+  private readonly _queueName: string;
+  private readonly _queueDb: QueueDb;
+
+  /**
+   * Associates this instance with a Queue instance, so entries added can be
+   * identified by their queue name.
+   *
+   * @param {string} queueName
+   */
+  constructor(queueName: string) {
+    this._queueName = queueName;
+    this._queueDb = new QueueDb();
+  }
+
+  /**
+   * Append an entry last in the queue.
+   *
+   * @param {Object} entry
+   * @param {Object} entry.requestData
+   * @param {number} [entry.timestamp]
+   * @param {Object} [entry.metadata]
+   */
+  async pushEntry(entry: UnidentifiedQueueStoreEntry): Promise<void> {
+    if (process.env.NODE_ENV !== 'production') {
+      assert!.isType(entry, 'object', {
+        moduleName: 'workbox-background-sync',
+        className: 'QueueStore',
+        funcName: 'pushEntry',
+        paramName: 'entry',
+      });
+      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._queueDb.addEntry(entry);
+  }
+
+  /**
+   * Prepend an entry first in the queue.
+   *
+   * @param {Object} entry
+   * @param {Object} entry.requestData
+   * @param {number} [entry.timestamp]
+   * @param {Object} [entry.metadata]
+   */
+  async unshiftEntry(entry: UnidentifiedQueueStoreEntry): Promise<void> {
+    if (process.env.NODE_ENV !== 'production') {
+      assert!.isType(entry, 'object', {
+        moduleName: 'workbox-background-sync',
+        className: 'QueueStore',
+        funcName: 'unshiftEntry',
+        paramName: 'entry',
+      });
+      assert!.isType(entry.requestData, 'object', {
+        moduleName: 'workbox-background-sync',
+        className: 'QueueStore',
+        funcName: 'unshiftEntry',
+        paramName: 'entry.requestData',
+      });
+    }
+
+    const firstId = await this._queueDb.getFirstEntryId();
+
+    if (firstId) {
+      // Pick an ID one less than the lowest ID in the object store.
+      entry.id = firstId - 1;
+    } else {
+      // Otherwise let the auto-incrementor assign the ID.
+      delete entry.id;
+    }
+    entry.queueName = this._queueName;
+
+    await this._queueDb.addEntry(entry);
+  }
+
+  /**
+   * Removes and returns the last entry in the queue matching the `queueName`.
+   *
+   * @return {Promise<QueueStoreEntry|undefined>}
+   */
+  async popEntry(): Promise<QueueStoreEntry | undefined> {
+    return this._removeEntry(
+      await this._queueDb.getLastEntryByQueueName(this._queueName),
+    );
+  }
+
+  /**
+   * Removes and returns the first entry in the queue matching the `queueName`.
+   *
+   * @return {Promise<QueueStoreEntry|undefined>}
+   */
+  async shiftEntry(): Promise<QueueStoreEntry | undefined> {
+    return this._removeEntry(
+      await this._queueDb.getFirstEntryByQueueName(this._queueName),
+    );
+  }
+
+  /**
+   * Returns all entries in the store matching the `queueName`.
+   *
+   * @param {Object} options See {@link workbox-background-sync.Queue~getAll}
+   * @return {Promise<Array<Object>>}
+   */
+  async getAll(): Promise<QueueStoreEntry[]> {
+    return await this._queueDb.getAllEntriesByQueueName(this._queueName);
+  }
+
+  /**
+   * Returns the number of entries in the store matching the `queueName`.
+   *
+   * @param {Object} options See {@link workbox-background-sync.Queue~size}
+   * @return {Promise<number>}
+   */
+  async size(): Promise<number> {
+    return await this._queueDb.getEntryCountByQueueName(this._queueName);
+  }
+
+  /**
+   * Deletes the entry for the given ID.
+   *
+   * WARNING: this method does not ensure the deleted entry 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.
+   *
+   * @param {number} id
+   */
+  async deleteEntry(id: number): Promise<void> {
+    await this._queueDb.deleteEntry(id);
+  }
+
+  /**
+   * Removes and returns the first or last entry in the queue (based on the
+   * `direction` argument) matching the `queueName`.
+   *
+   * @return {Promise<QueueStoreEntry|undefined>}
+   * @private
+   */
+  async _removeEntry(
+    entry?: QueueStoreEntry,
+  ): Promise<QueueStoreEntry | undefined> {
+    if (entry) {
+      await this.deleteEntry(entry.id);
+    }
+    return entry;
+  }
+}
Index: frontend/node_modules/workbox-background-sync/src/lib/StorableRequest.ts
===================================================================
--- frontend/node_modules/workbox-background-sync/src/lib/StorableRequest.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/src/lib/StorableRequest.ts	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,156 @@
+/*
+  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.
+*/
+
+import {assert} from 'workbox-core/_private/assert.js';
+import {MapLikeObject} from 'workbox-core/types.js';
+import '../_version.js';
+
+type SerializableProperties =
+  | 'method'
+  | 'referrer'
+  | 'referrerPolicy'
+  | 'mode'
+  | 'credentials'
+  | 'cache'
+  | 'redirect'
+  | 'integrity'
+  | 'keepalive';
+
+const serializableProperties: SerializableProperties[] = [
+  'method',
+  'referrer',
+  'referrerPolicy',
+  'mode',
+  'credentials',
+  'cache',
+  'redirect',
+  'integrity',
+  'keepalive',
+];
+
+export interface RequestData extends MapLikeObject {
+  url: string;
+  headers: MapLikeObject;
+  body?: ArrayBuffer;
+}
+
+/**
+ * A class to make it easier to serialize and de-serialize requests so they
+ * can be stored in IndexedDB.
+ *
+ * Most developers will not need to access this class directly;
+ * it is exposed for advanced use cases.
+ */
+class StorableRequest {
+  private readonly _requestData: RequestData;
+
+  /**
+   * Converts a Request object to a plain object that can be structured
+   * cloned or JSON-stringified.
+   *
+   * @param {Request} request
+   * @return {Promise<StorableRequest>}
+   */
+  static async fromRequest(request: Request): Promise<StorableRequest> {
+    const requestData: 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);
+  }
+
+  /**
+   * 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}.
+   */
+  constructor(requestData: RequestData) {
+    if (process.env.NODE_ENV !== 'production') {
+      assert!.isType(requestData, 'object', {
+        moduleName: 'workbox-background-sync',
+        className: 'StorableRequest',
+        funcName: 'constructor',
+        paramName: 'requestData',
+      });
+      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;
+  }
+
+  /**
+   * Returns a deep clone of the instances `_requestData` object.
+   *
+   * @return {Object}
+   */
+  toObject(): RequestData {
+    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}
+   */
+  toRequest(): Request {
+    return new Request(this._requestData.url, this._requestData);
+  }
+
+  /**
+   * Creates and returns a deep clone of the instance.
+   *
+   * @return {StorableRequest}
+   */
+  clone(): StorableRequest {
+    return new StorableRequest(this.toObject());
+  }
+}
+
+export {StorableRequest};
Index: frontend/node_modules/workbox-background-sync/tsconfig.json
===================================================================
--- frontend/node_modules/workbox-background-sync/tsconfig.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/tsconfig.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,10 @@
+{
+  "extends": "../../tsconfig",
+  "compilerOptions": {
+    "outDir": "./",
+    "rootDir": "./src",
+    "tsBuildInfoFile": "./tsconfig.tsbuildinfo"
+  },
+  "include": ["src/**/*.ts"],
+  "references": [{"path": "../workbox-core/"}]
+}
Index: frontend/node_modules/workbox-background-sync/tsconfig.tsbuildinfo
===================================================================
--- frontend/node_modules/workbox-background-sync/tsconfig.tsbuildinfo	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/workbox-background-sync/tsconfig.tsbuildinfo	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+{"program":{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.webworker.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../infra/type-overrides.d.ts","../workbox-core/_version.d.ts","../workbox-core/types.d.ts","../workbox-core/_private/workboxerror.d.ts","../workbox-core/_private/logger.d.ts","../workbox-core/_private/assert.d.ts","../workbox-core/_private/getfriendlyurl.d.ts","./node_modules/idb/build/wrap-idb-value.d.ts","./node_modules/idb/build/entry.d.ts","./node_modules/idb/build/database-extras.d.ts","./node_modules/idb/build/index.d.ts","./src/_version.ts","./src/lib/storablerequest.ts","./src/lib/queuedb.ts","./src/lib/queuestore.ts","./src/queue.ts","./src/backgroundsyncplugin.ts","./src/queuestore.ts","./src/storablerequest.ts","./src/index.ts","../../node_modules/@babel/types/lib/index.d.ts","../../node_modules/@types/babel__generator/index.d.ts","../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../node_modules/@types/babel__template/index.d.ts","../../node_modules/@types/babel__traverse/index.d.ts","../../node_modules/@types/babel__core/index.d.ts","../../node_modules/@types/babel__preset-env/index.d.ts","../../node_modules/@types/common-tags/index.d.ts","../../node_modules/@types/eslint/helpers.d.ts","../../node_modules/@types/json-schema/index.d.ts","../../node_modules/@types/estree/index.d.ts","../../node_modules/@types/eslint/index.d.ts","../../node_modules/@types/eslint-scope/index.d.ts","../../node_modules/@types/node/globals.d.ts","../../node_modules/@types/node/async_hooks.d.ts","../../node_modules/@types/node/buffer.d.ts","../../node_modules/@types/node/child_process.d.ts","../../node_modules/@types/node/cluster.d.ts","../../node_modules/@types/node/console.d.ts","../../node_modules/@types/node/constants.d.ts","../../node_modules/@types/node/crypto.d.ts","../../node_modules/@types/node/dgram.d.ts","../../node_modules/@types/node/dns.d.ts","../../node_modules/@types/node/domain.d.ts","../../node_modules/@types/node/events.d.ts","../../node_modules/@types/node/fs.d.ts","../../node_modules/@types/node/fs/promises.d.ts","../../node_modules/@types/node/http.d.ts","../../node_modules/@types/node/http2.d.ts","../../node_modules/@types/node/https.d.ts","../../node_modules/@types/node/inspector.d.ts","../../node_modules/@types/node/module.d.ts","../../node_modules/@types/node/net.d.ts","../../node_modules/@types/node/os.d.ts","../../node_modules/@types/node/path.d.ts","../../node_modules/@types/node/perf_hooks.d.ts","../../node_modules/@types/node/process.d.ts","../../node_modules/@types/node/punycode.d.ts","../../node_modules/@types/node/querystring.d.ts","../../node_modules/@types/node/readline.d.ts","../../node_modules/@types/node/repl.d.ts","../../node_modules/@types/node/stream.d.ts","../../node_modules/@types/node/string_decoder.d.ts","../../node_modules/@types/node/timers.d.ts","../../node_modules/@types/node/tls.d.ts","../../node_modules/@types/node/trace_events.d.ts","../../node_modules/@types/node/tty.d.ts","../../node_modules/@types/node/url.d.ts","../../node_modules/@types/node/util.d.ts","../../node_modules/@types/node/v8.d.ts","../../node_modules/@types/node/vm.d.ts","../../node_modules/@types/node/worker_threads.d.ts","../../node_modules/@types/node/zlib.d.ts","../../node_modules/@types/node/ts3.4/base.d.ts","../../node_modules/@types/node/globals.global.d.ts","../../node_modules/@types/node/wasi.d.ts","../../node_modules/@types/node/ts3.6/base.d.ts","../../node_modules/@types/node/assert.d.ts","../../node_modules/@types/node/base.d.ts","../../node_modules/@types/node/index.d.ts","../../node_modules/@types/fs-extra/index.d.ts","../../node_modules/@types/minimatch/index.d.ts","../../node_modules/@types/glob/index.d.ts","../../node_modules/@types/html-minifier-terser/index.d.ts","../../node_modules/@types/linkify-it/index.d.ts","../../node_modules/@types/lodash/common/common.d.ts","../../node_modules/@types/lodash/common/array.d.ts","../../node_modules/@types/lodash/common/collection.d.ts","../../node_modules/@types/lodash/common/date.d.ts","../../node_modules/@types/lodash/common/function.d.ts","../../node_modules/@types/lodash/common/lang.d.ts","../../node_modules/@types/lodash/common/math.d.ts","../../node_modules/@types/lodash/common/number.d.ts","../../node_modules/@types/lodash/common/object.d.ts","../../node_modules/@types/lodash/common/seq.d.ts","../../node_modules/@types/lodash/common/string.d.ts","../../node_modules/@types/lodash/common/util.d.ts","../../node_modules/@types/lodash/index.d.ts","../../node_modules/@types/mdurl/encode.d.ts","../../node_modules/@types/mdurl/decode.d.ts","../../node_modules/@types/mdurl/parse.d.ts","../../node_modules/@types/mdurl/format.d.ts","../../node_modules/@types/mdurl/index.d.ts","../../node_modules/@types/markdown-it/lib/common/utils.d.ts","../../node_modules/@types/markdown-it/lib/token.d.ts","../../node_modules/@types/markdown-it/lib/rules_inline/state_inline.d.ts","../../node_modules/@types/markdown-it/lib/helpers/parse_link_label.d.ts","../../node_modules/@types/markdown-it/lib/helpers/parse_link_destination.d.ts","../../node_modules/@types/markdown-it/lib/helpers/parse_link_title.d.ts","../../node_modules/@types/markdown-it/lib/helpers/index.d.ts","../../node_modules/@types/markdown-it/lib/ruler.d.ts","../../node_modules/@types/markdown-it/lib/rules_block/state_block.d.ts","../../node_modules/@types/markdown-it/lib/parser_block.d.ts","../../node_modules/@types/markdown-it/lib/rules_core/state_core.d.ts","../../node_modules/@types/markdown-it/lib/parser_core.d.ts","../../node_modules/@types/markdown-it/lib/parser_inline.d.ts","../../node_modules/@types/markdown-it/lib/renderer.d.ts","../../node_modules/@types/markdown-it/lib/index.d.ts","../../node_modules/@types/markdown-it/index.d.ts","../../node_modules/@types/minimist/index.d.ts","../../node_modules/@types/normalize-package-data/index.d.ts","../../node_modules/@types/parse-json/index.d.ts","../../node_modules/@types/resolve/index.d.ts","../../node_modules/@types/semver/classes/semver.d.ts","../../node_modules/@types/semver/functions/parse.d.ts","../../node_modules/@types/semver/functions/valid.d.ts","../../node_modules/@types/semver/functions/clean.d.ts","../../node_modules/@types/semver/functions/inc.d.ts","../../node_modules/@types/semver/functions/diff.d.ts","../../node_modules/@types/semver/functions/major.d.ts","../../node_modules/@types/semver/functions/minor.d.ts","../../node_modules/@types/semver/functions/patch.d.ts","../../node_modules/@types/semver/functions/prerelease.d.ts","../../node_modules/@types/semver/functions/compare.d.ts","../../node_modules/@types/semver/functions/rcompare.d.ts","../../node_modules/@types/semver/functions/compare-loose.d.ts","../../node_modules/@types/semver/functions/compare-build.d.ts","../../node_modules/@types/semver/functions/sort.d.ts","../../node_modules/@types/semver/functions/rsort.d.ts","../../node_modules/@types/semver/functions/gt.d.ts","../../node_modules/@types/semver/functions/lt.d.ts","../../node_modules/@types/semver/functions/eq.d.ts","../../node_modules/@types/semver/functions/neq.d.ts","../../node_modules/@types/semver/functions/gte.d.ts","../../node_modules/@types/semver/functions/lte.d.ts","../../node_modules/@types/semver/functions/cmp.d.ts","../../node_modules/@types/semver/functions/coerce.d.ts","../../node_modules/@types/semver/classes/comparator.d.ts","../../node_modules/@types/semver/classes/range.d.ts","../../node_modules/@types/semver/functions/satisfies.d.ts","../../node_modules/@types/semver/ranges/max-satisfying.d.ts","../../node_modules/@types/semver/ranges/min-satisfying.d.ts","../../node_modules/@types/semver/ranges/to-comparators.d.ts","../../node_modules/@types/semver/ranges/min-version.d.ts","../../node_modules/@types/semver/ranges/valid.d.ts","../../node_modules/@types/semver/ranges/outside.d.ts","../../node_modules/@types/semver/ranges/gtr.d.ts","../../node_modules/@types/semver/ranges/ltr.d.ts","../../node_modules/@types/semver/ranges/intersects.d.ts","../../node_modules/@types/semver/ranges/simplify.d.ts","../../node_modules/@types/semver/ranges/subset.d.ts","../../node_modules/@types/semver/internals/identifiers.d.ts","../../node_modules/@types/semver/index.d.ts","../../node_modules/@types/source-list-map/index.d.ts","../../node_modules/@types/stringify-object/index.d.ts","../../node_modules/@types/tapable/index.d.ts","../../node_modules/@types/uglify-js/node_modules/source-map/source-map.d.ts","../../node_modules/@types/uglify-js/index.d.ts","../../node_modules/@types/webpack-sources/node_modules/source-map/source-map.d.ts","../../node_modules/@types/webpack-sources/lib/source.d.ts","../../node_modules/@types/webpack-sources/lib/compatsource.d.ts","../../node_modules/@types/webpack-sources/lib/concatsource.d.ts","../../node_modules/@types/webpack-sources/lib/originalsource.d.ts","../../node_modules/@types/webpack-sources/lib/prefixsource.d.ts","../../node_modules/@types/webpack-sources/lib/rawsource.d.ts","../../node_modules/@types/webpack-sources/lib/replacesource.d.ts","../../node_modules/@types/webpack-sources/lib/sizeonlysource.d.ts","../../node_modules/@types/webpack-sources/lib/sourcemapsource.d.ts","../../node_modules/@types/webpack-sources/lib/index.d.ts","../../node_modules/@types/webpack-sources/lib/cachedsource.d.ts","../../node_modules/@types/webpack-sources/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06",{"version":"d3f4771304b6b07e5a2bb992e75af76ac060de78803b1b21f0475ffc5654d817","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},{"version":"0396119f8b76a074eddc16de8dbc4231a448f2534f4c64c5ab7b71908eb6e646","affectsGlobalScope":true},"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","f0ae1ac99c66a4827469b8942101642ae65971e36db438afe67d4985caa31222","0b066351c69855f76970a460fe20a500d20f369a02d2069aa49e1195cd04c3c5",{"version":"d763b9ef68a16f3896187af5b51b5a959b479218cc65c2930bcb440cbbf10728","affectsGlobalScope":true},"7f5bced3f1bd3647585b59564e0b0fda67e1c2930325507ee922698fe8366aca","9fd40388bab591ded1f8c05b64fbfe3e342c6cd70d594d5238f42dd2186980ff","0a891b4678e2705451b79b6587efa5911288492815e1a2b46da880c06979f4ed","c488c9ba0e79f3d0f7af33ab64c9d3e736b842ce3e8ee23991830ae836397947","8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","976b7d4fa6b154ae080c22f5249da4fa5c5bfb7584abf0c06279e2814bf4660a",{"version":"c8c148395c4364477ddec3ee4c7ac5ec04a7449f2754774defe28f04d1119f59","signature":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","affectsGlobalScope":true},{"version":"fd8523b384b49eabcd2dce9d5c6d76f86fa8eddd2889078d970d5a572cbaf751","signature":"bbbc8b1e87deaedfbd78cbbcd538a52430bf3386d18ba58cbe0eb96f2e2a6d83"},{"version":"9044f4a7916d8750e84c6bb6cc0536c6906906545e1539042fd1b875ffb32a15","signature":"12cf56bf7dbb9a2d161dd61d4adf20347f1c90a406cf62f40b34251a40378f38"},{"version":"15d415f26ccb87b1e3c24e6fb846a3d142d7cd7c4b0c3a03d7822dcdb9d41fb3","signature":"442ae514433af7f208d6ff63f49cac04cae9bbcf4872efd98bd14f1ef64a0f49"},{"version":"70cdec5c4b526b70b5e2430631a14df75f2d9f51371cfa97842778c62a7492af","signature":"72c62b406af19eca8080ea63f90f4c907ee5b8348152b75ba106395cd7514f54"},{"version":"6b6b2d887a077904ae2d7e5bfeecdcec0a8de7d15eeeccde04a53ea1310cd766","signature":"3d3f0a288ad84c93fa07551ef1471447bec5357f1e23d4c113dc960a4a088f65"},{"version":"0793214b49d56eedbb371acbebacec68f09b58f5061413e7246819d6a927e3dd","signature":"a11e0f0a8864c8423ac7d9b0e4f10ff6bdfc1293496e9ba41ab134728e6f8901"},{"version":"1b39355b5db7aafcc849e3f777eb842be92c797c20e748c44cbf0177357fe935","signature":"af599cf0f7d08f1f265f458a9d5406eb8e955d0d34aeb5a99e75ed8ee0ffde7e"},{"version":"51dc9c8377a4ba60f16e5ea1b02a2715e8b7c0a74126ab7680ca675afce01375","signature":"5bd7129bf71f782c4619ca7a344208f0f774ca66be3d85ee35e6cd10dfb7f6c3","affectsGlobalScope":true},"3eb8ad25895d53cc6229dc83decbc338d649ed6f3d5b537c9966293b056b1f57","b25c5f2970d06c729f464c0aeaa64b1a5b5f1355aa93554bb5f9c199b8624b1e","8678956904af215fe917b2df07b6c54f876fa64eb1f8a158e4ff38404cef3ff4","3051751533eee92572241b3cef28333212401408c4e7aa21718714b793c0f4ed","691aea9772797ca98334eb743e7686e29325b02c6931391bcee4cc7bf27a9f3b","6f1d39d26959517da3bd105c552eded4c34702705c64d75b03f54d864b6e41c2","5d1b955e6b1974fe5f47fbde474343113ab701ca30b80e463635a29e58d80944","3b93231babdb3ee9470a7e6103e48bf6585c4185f96941c08a77e097f8f469ae",{"version":"f345b0888d003fd69cb32bad3a0aa04c615ccafc572019e4bd86a52bd5e49e46","affectsGlobalScope":true},"0359682c54e487c4cab2b53b2b4d35cc8dea4d9914bc6abcdb5701f8b8e745a4","6a38e250306ceccbab257d11b846d5bd12491157d20901fa01afe4050c93c1b5","ffa048767a32a0f6354e611b15d8b53d882da1a9a35455c35c3f6811f2416d17","e050a0afcdbb269720a900c85076d18e0c1ab73e580202a2bf6964978181222a",{"version":"68aba9c37b535b42ce96b78a4cfa93813bf4525f86dceb88a6d726c5f7c6c14f","affectsGlobalScope":true},"c438b413e94ff76dfa20ae005f33a1c84f2480d1d66e0fd687501020d0de9b50","bc6a78961535181265845bf9b9e8a147ffd0ca275097ceb670a9b92afa825152","1fc4b0908c44f39b1f2e5a728d472670a0ea0970d2c6b5691c88167fe541ff82","123ec69e4b3a686eb49afd94ebe3292a5c84a867ecbcb6bb84bdd720a12af803",{"version":"51851805d06a6878796c3a00ccf0839fe18111a38d1bae84964c269f16bcc2b7","affectsGlobalScope":true},"90c85ddbb8de82cd19198bda062065fc51b7407c0f206f2e399e65a52e979720","c5ecc351d5eaa36dc682b4c398b57a9d37c108857b71a09464a06e0185831ac2","7ecfe97b43aa6c8b8f90caa599d5648bb559962e74e6f038f73a77320569dd78","7db7569fbb3e2b01ba8751c761cdd3f0debd104170d5665b7dc20a11630df3a9",{"version":"cde4d7f6274468180fa39847b183aec22626e8212ff885d535c53f4cd7c225fd","affectsGlobalScope":true},{"version":"072b0ac82ae8fe05b0d4f2eadb7f6edd0ebd84175ecad2f9e09261290a86bcee","affectsGlobalScope":true},"f6eedd1053167b8a651d8d9c70b1772e1b501264a36dfa881d7d4b30d623a9bc","fb28748ff8d015f52e99daee4f454e57cec1a22141f1257c317f3630a15edeb7","08fb2b0e1ef13a2df43f6d8e97019c36dfbc0475cf4d274c6838e2c9223fe39d","5d9394b829cfd504b2fe17287aaad8ce1dcfb2a2183c962a90a85b96da2c1c90","c969bf4c7cdfe4d5dd28aa09432f99d09ad1d8d8b839959646579521d0467d1a","6c3857edaeeaaf43812f527830ebeece9266b6e8eb5271ab6d2f0008306c9947","bc6a77e750f4d34584e46b1405b771fb69a224197dd6bafe5b0392a29a70b665","46cac76114704902baa535b30fb66a26aeaf9430f3b3ab44746e329f12e85498","ed4ae81196cccc10f297d228bca8d02e31058e6d723a3c5bc4be5fb3c61c6a34","84044697c8b3e08ef24e4b32cfe6440143d07e469a5e34bda0635276d32d9f35","6999f789ed86a40f3bc4d7e644e8d42ffda569465969df8077cd6c4e3505dd76",{"version":"0c9f2b308e5696d0802b613aff47c99f092add29408e654f7ab6026134250c18","affectsGlobalScope":true},"4a9008d79750801375605e6cfefa4e04643f20f2aaa58404c6aae1c894e9b049","884560fda6c3868f925f022adc3a1289fe6507bbb45adb10fa1bbcc73a941bb0","6b2bb67b0942bcfce93e1d6fad5f70afd54940a2b13df7f311201fba54b2cbe9","dd3706b25d06fe23c73d16079e8c66ac775831ef419da00716bf2aee530a04a4","1298327149e93a60c24a3b5db6048f7cc8fd4e3259e91d05fc44306a04b1b873","d67e08745494b000da9410c1ae2fdc9965fc6d593fe0f381a47491f75417d457","b40652bf8ce4a18133b31349086523b219724dca8df3448c1a0742528e7ad5b9","3181290a158e54a78c1a57c41791ec1cbdc860ae565916daa1bf4e425b7edac7","a77fdb357c78b70142b2fdbbfb72958d69e8f765fd2a3c69946c1018e89d4638","3c2ac350c3baa61fd2b1925844109e098f4376d0768a4643abc82754fd752748","826d48e49c905cedb906cbde6ccaf758827ff5867d4daa006b5a79e0fb489357","5ef157fbb39494a581bd24f21b60488fe248d452c479738b5e41b48720ea69b8","289be113bad7ee27ee7fa5b1e373c964c9789a5e9ed7db5ddcb631371120b953","a1136cf18dbe1b9b600c65538fd48609a1a4772d115a0c1d775839fe6544487c","24638ed25631a94a9b0d7b580b146329f82e158e8d1e90171a73d87bebf79255","638f49a0db5d30977533a8cfabf3e10ab30724360424698e8d5fd41ca272e070","d44028ae0127eb3e9fcfa5f55a8b81d64775ce15aca1020fe25c511bbb055834",{"version":"2708349d5a11a5c2e5f3a0765259ebe7ee00cdcc8161cb9990cb4910328442a1","affectsGlobalScope":true},"4e0a4d84b15692ea8669fe4f3d05a4f204567906b1347da7a58b75f45bae48d3","0f04bc8950ad634ac8ac70f704f200ef06f8852af9017f97c446de4def5b3546","d0c575d48d6dad75648017ff18762eb97f9398cc9486541b3070e79ce12719e6","d20072cb51d8baad944bedd935a25c7f10c29744e9a648d2c72c215337356077","35cbbc58882d2c158032d7f24ba8953d7e1caeb8cb98918d85819496109f55d2","8d01c38ccb9af3a4035a68818799e5ef32ccc8cf70bdb83e181e1921d7ad32f6","1d1e6bd176eee5970968423d7e215bfd66828b6db8d54d17afec05a831322633","393137c76bd922ba70a2f8bf1ade4f59a16171a02fb25918c168d48875b0cfb0","6767cce098e1e6369c26258b7a1f9e569c5467d501a47a090136d5ea6e80ae6d","6503fb6addf62f9b10f8564d9869ad824565a914ec1ac3dd7d13da14a3f57036","3594c022901a1c8993b0f78a3f534cfb81e7b619ed215348f7f6882f3db02abc","438284c7c455a29b9c0e2d1e72abc62ee93d9a163029ffe918a34c5db3b92da2","0c75b204aed9cf6ff1c7b4bed87a3ece0d9d6fc857a6350c0c95ed0c38c814e8","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","c9f396e71966bd3a890d8a36a6a497dbf260e9b868158ea7824d4b5421210afe","509235563ea2b939e1bbe92aae17e71e6a82ceab8f568b45fb4fce7d72523a32","9364c7566b0be2f7b70ff5285eb34686f83ccb01bda529b82d23b2a844653bfb","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","c311349ec71bb69399ffc4092853e7d8a86c1ca39ddb4cd129e775c19d985793","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","4908e4c00832b26ce77a629de8501b0e23a903c094f9e79a7fec313a15da796a","2630a7cbb597e85d713b7ef47f2946d4280d3d4c02733282770741d40672b1a5",{"version":"0714e2046df66c0e93c3330d30dbc0565b3e8cd3ee302cf99e4ede6220e5fec8","affectsGlobalScope":true},"f313731860257325f13351575f381fef333d4dfe30daf5a2e72f894208feea08","951b37f7d86f6012f09e6b35f1de57c69d75f16908cb0adaa56b93675ea0b853","3816fc03ffd9cbd1a7a3362a264756a4a1d547caabea50ca68303046be40e376","0c417b4ec46b88fb62a43ec00204700b560d01eb5677c7faa8ecd34610f096a8","13d29cdeb64e8496424edf42749bbb47de5e42d201cf958911a4638cbcffbd3f","0f9e381eecc5860f693c31fe463b3ca20a64ca9b8db0cf6208cd4a053f064809","95902d5561c6aac5dfc40568a12b0aca324037749dcd32a81f23423bfde69bab","5dfb2aca4136abdc5a2740f14be8134a6e6b66fd53470bb2e954e40f8abfaf3e","577463167dd69bd81f76697dfc3f7b22b77a6152f60a602a9218e52e3183ad67","b8396e9024d554b611cbe31a024b176ba7116063d19354b5a02dccd8f0118989","4b28e1c5bf88d891e07a1403358b81a51b3ba2eae1ffada51cca7476b5ac6407","7150ad575d28bf98fae321a1c0f10ad17b127927811f488ded6ff1d88d4244e5","8b155c4757d197969553de3762c8d23d5866710301de41e1b66b97c9ed867003","93733466609dd8bf72eace502a24ca7574bd073d934216e628f1b615c8d3cb3c","45e9228761aabcadb79c82fb3008523db334491525bdb8e74e0f26eaf7a4f7f4","aeacac2778c9821512b6b889da79ac31606a863610c8f28da1e483579627bf90","569fdb354062fc098a6a3ba93a029edf22d6fe480cf72b231b3c07832b2e7c97","bf9876e62fb7f4237deafab8c7444770ef6e82b4cad2d5dc768664ff340feeb2","6cf60e76d37faf0fbc2f80a873eab0fd545f6b1bf300e7f0823f956ddb3083e9","6adaa6103086f931e3eee20f0987e86e8879e9d13aa6bd6075ccfc58b9c5681c","ee0af0f2b8d3b4d0baf669f2ff6fcef4a8816a473c894cc7c905029f7505fed0","3602dfff3072caea42f23a9b63fb34a7b0c95a62b93ce2add5fe6b159447845e","c9ad058b2cc9ce6dc2ed92960d6d009e8c04bef46d3f5312283debca6869f613","2b8264b2fefd7367e0f20e2c04eed5d3038831fe00f5efbc110ff0131aab899b","8a19491eba2108d5c333c249699f40aff05ad312c04a17504573b27d91f0aede","2b93035328f7778d200252681c1d86285d501ed424825a18f81e4c3028aa51d9","2ac9c8332c5f8510b8bdd571f8271e0f39b0577714d5e95c1e79a12b2616f069","42c21aa963e7b86fa00801d96e88b36803188018d5ad91db2a9101bccd40b3ff","d31eb848cdebb4c55b4893b335a7c0cca95ad66dee13cbb7d0893810c0a9c301","77c1d91a129ba60b8c405f9f539e42df834afb174fe0785f89d92a2c7c16b77a","7a9e0a564fee396cacf706523b5aeed96e04c6b871a8bebefad78499fbffc5bc","906c751ef5822ec0dadcea2f0e9db64a33fb4ee926cc9f7efa38afe5d5371b2a","5387c049e9702f2d2d7ece1a74836a14b47fbebe9bbeb19f94c580a37c855351","c68391fb9efad5d99ff332c65b1606248c4e4a9f1dd9a087204242b56c7126d6","e9cf02252d3a0ced987d24845dcb1f11c1be5541f17e5daa44c6de2d18138d0c","e8b02b879754d85f48489294f99147aeccc352c760d95a6fe2b6e49cd400b2fe","9f6908ab3d8a86c68b86e38578afc7095114e66b2fc36a2a96e9252aac3998e0","0eedb2344442b143ddcd788f87096961cd8572b64f10b4afc3356aa0460171c6","71405cc70f183d029cc5018375f6c35117ffdaf11846c35ebf85ee3956b1b2a6","c68baff4d8ba346130e9753cefe2e487a16731bf17e05fdacc81e8c9a26aae9d","2cd15528d8bb5d0453aa339b4b52e0696e8b07e790c153831c642c3dea5ac8af","479d622e66283ffa9883fbc33e441f7fc928b2277ff30aacbec7b7761b4e9579","ade307876dc5ca267ca308d09e737b611505e015c535863f22420a11fffc1c54","f8cdefa3e0dee639eccbe9794b46f90291e5fd3989fcba60d2f08fde56179fb9","86c5a62f99aac7053976e317dbe9acb2eaf903aaf3d2e5bb1cafe5c2df7b37a8","2b300954ce01a8343866f737656e13243e86e5baef51bd0631b21dcef1f6e954","a2d409a9ffd872d6b9d78ead00baa116bbc73cfa959fce9a2f29d3227876b2a1","b288936f560cd71f4a6002953290de9ff8dfbfbf37f5a9391be5c83322324898","61178a781ef82e0ff54f9430397e71e8f365fc1e3725e0e5346f2de7b0d50dfa","6a6ccb37feb3aad32d9be026a3337db195979cd5727a616fc0f557e974101a54","c649ea79205c029a02272ef55b7ab14ada0903db26144d2205021f24727ac7a3","38e2b02897c6357bbcff729ef84c736727b45cc152abe95a7567caccdfad2a1d","d6610ea7e0b1a7686dba062a1e5544dd7d34140f4545305b7c6afaebfb348341","3dee35db743bdba2c8d19aece7ac049bde6fa587e195d86547c882784e6ba34c","b15e55c5fa977c2f25ca0b1db52cfa2d1fd4bf0baf90a8b90d4a7678ca462ff1","f41d30972724714763a2698ae949fbc463afb203b5fa7c4ad7e4de0871129a17","843dd7b6a7c6269fd43827303f5cbe65c1fecabc30b4670a50d5a15d57daeeb9","f06d8b8567ee9fd799bf7f806efe93b67683ef24f4dea5b23ef12edff4434d9d","6017384f697ff38bc3ef6a546df5b230c3c31329db84cbfe686c83bec011e2b2","e1a5b30d9248549ca0c0bb1d653bafae20c64c4aa5928cc4cd3017b55c2177b0","a593632d5878f17295bd53e1c77f27bf4c15212822f764a2bfc1702f4b413fa0","a868a534ba1c2ca9060b8a13b0ffbbbf78b4be7b0ff80d8c75b02773f7192c29","da7545aba8f54a50fde23e2ede00158dc8112560d934cee58098dfb03aae9b9d","34baf65cfee92f110d6653322e2120c2d368ee64b3c7981dff08ed105c4f19b0","6aee496bf0ecfbf6731aa8cca32f4b6e92cdc0a444911a7d88410408a45ecc5d","67fc055eb86a0632e2e072838f889ffe1754083cb13c8c80a06a7d895d877aae","67d3e19b3b6e2c082ffd11ae5064c7a81b13d151326953b90fc26103067a1945","d558a0fe921ebcc88d3212c2c42108abf9f0d694d67ebdeba37d7728c044f579","2887592574fcdfd087647c539dcb0fbe5af2521270dad4a37f9d17c16190d579","9d74c7330800b325bb19cc8c1a153a612c080a60094e1ab6cfb6e39cf1b88c36","b90c59ac4682368a01c83881b814738eb151de8a58f52eb7edadea2bcffb11b9","8560a87b2e9f8e2c3808c8f6172c9b7eb6c9b08cb9f937db71c285ecf292c81d","ffe3931ff864f28d80ae2f33bd11123ad3d7bad9896b910a1e61504cc093e1f5","083c1bd82f8dc3a1ed6fc9e8eaddf141f7c05df418eca386598821e045253af9","274ebe605bd7f71ce161f9f5328febc7d547a2929f803f04b44ec4a7d8729517","6ca0207e70d985a24396583f55836b10dc181063ab6069733561bfde404d1bad","5908142efeaab38ffdf43927ee0af681ae77e0d7672b956dfb8b6c705dbfe106","f772b188b943549b5c5eb803133314b8aa7689eced80eed0b70e2f30ca07ab9c","0026b816ef05cfbf290e8585820eef0f13250438669107dfc44482bac007b14f","05d64cc1118031b29786632a9a0f6d7cf1dcacb303f27023a466cf3cdc860538","e0fff9119e1a5d2fdd46345734126cd6cb99c2d98a9debf0257047fe3937cc3f","d84398556ba4595ee6be554671da142cfe964cbdebb2f0c517a10f76f2b016c0","e275297155ec3251200abbb334c7f5641fecc68b2a9573e40eed50dff7584762"],"options":{"composite":true,"declaration":true,"module":99,"noFallthroughCasesInSwitch":true,"noImplicitReturns":true,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"./","preserveConstEnums":true,"rootDir":"./src","strict":true,"target":4,"tsBuildInfoFile":"./tsconfig.tsbuildinfo"},"fileIdsList":[[50],[50,51,52,53,54],[50,52],[60,61],[58,59,60],[75,109],[74,109,111],[115,117,118,119,120,121,122,123,124,125,126,127],[115,116,118,119,120,121,122,123,124,125,126,127],[116,117,118,119,120,121,122,123,124,125,126,127],[115,116,117,119,120,121,122,123,124,125,126,127],[115,116,117,118,120,121,122,123,124,125,126,127],[115,116,117,118,119,121,122,123,124,125,126,127],[115,116,117,118,119,120,122,123,124,125,126,127],[115,116,117,118,119,120,121,123,124,125,126,127],[115,116,117,118,119,120,121,122,124,125,126,127],[115,116,117,118,119,120,121,122,123,125,126,127],[115,116,117,118,119,120,121,122,123,124,126,127],[115,116,117,118,119,120,121,122,123,124,125,127],[115,116,117,118,119,120,121,122,123,124,125,126],[147],[132],[136,137,138],[135],[137],[114,133,134,139,142,144,145,146],[134,140,141,147],[140,143],[134,135,140,147],[134,147],[128,129,130,131],[106,107],[74,75,82,91],[66,74,82],[98],[70,75,83],[91],[72,74,82],[74],[74,76,91,97],[75],[82,91,97],[74,75,77,82,91,94,97],[74,77,94,97],[108],[97],[72,74,91],[64],[96],[74,91],[89,98,100],[70,72,82,91],[63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102],[103,104,105],[82],[88],[74,76,91,97,100],[109],[153,192],[153,177,192],[192],[153],[153,178,192],[153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191],[178,192],[196],[109,199,200,201,202,203,204,205,206,207,208,209],[198,199,208],[199,208],[193,198,199,208],[198,199,200,201,202,203,204,205,206,207,209],[199],[70,198,208],[37],[38,39],[38],[32,45],[45,46,47,48],[40,42],[35,43],[32,35],[33,34,35,36,42,43,44],[44],[42],[32],[43]],"referencedMap":[[52,1],[55,2],[51,1],[53,3],[54,1],[62,4],[61,5],[110,6],[112,7],[116,8],[117,9],[115,10],[118,11],[119,12],[120,13],[121,14],[122,15],[123,16],[124,17],[125,18],[126,19],[127,20],[148,21],[133,22],[139,23],[136,24],[138,25],[147,26],[142,27],[144,28],[145,29],[146,30],[141,30],[143,30],[135,30],[131,22],[132,31],[130,22],[108,32],[66,33],[67,34],[68,35],[69,36],[70,37],[71,38],[73,39],[75,40],[76,41],[77,42],[78,43],[79,44],[109,45],[80,39],[81,46],[82,47],[85,48],[86,49],[89,50],[90,51],[91,39],[94,52],[103,53],[106,54],[96,55],[97,56],[99,37],[101,57],[102,37],[152,58],[177,59],[178,60],[153,61],[156,61],[175,59],[176,59],[166,59],[165,62],[163,59],[158,59],[171,59],[169,59],[173,59],[157,59],[170,59],[174,59],[159,59],[160,59],[172,59],[154,59],[161,59],[162,59],[164,59],[168,59],[179,63],[167,59],[155,59],[192,64],[186,63],[188,65],[187,63],[180,63],[181,63],[183,63],[185,63],[189,65],[190,65],[182,65],[184,65],[197,66],[210,67],[209,68],[200,69],[201,70],[208,71],[202,70],[203,69],[204,69],[205,69],[206,72],[199,73],[207,68],[38,74],[40,75],[37,76],[46,77],[49,78],[43,79],[44,80],[42,81],[45,82],[47,83],[48,84],[35,85],[33,85]],"exportedModulesMap":[[52,1],[55,2],[51,1],[53,3],[54,1],[62,4],[61,5],[110,6],[112,7],[116,8],[117,9],[115,10],[118,11],[119,12],[120,13],[121,14],[122,15],[123,16],[124,17],[125,18],[126,19],[127,20],[148,21],[133,22],[139,23],[136,24],[138,25],[147,26],[142,27],[144,28],[145,29],[146,30],[141,30],[143,30],[135,30],[131,22],[132,31],[130,22],[108,32],[66,33],[67,34],[68,35],[69,36],[70,37],[71,38],[73,39],[75,40],[76,41],[77,42],[78,43],[79,44],[109,45],[80,39],[81,46],[82,47],[85,48],[86,49],[89,50],[90,51],[91,39],[94,52],[103,53],[106,54],[96,55],[97,56],[99,37],[101,57],[102,37],[152,58],[177,59],[178,60],[153,61],[156,61],[175,59],[176,59],[166,59],[165,62],[163,59],[158,59],[171,59],[169,59],[173,59],[157,59],[170,59],[174,59],[159,59],[160,59],[172,59],[154,59],[161,59],[162,59],[164,59],[168,59],[179,63],[167,59],[155,59],[192,64],[186,63],[188,65],[187,63],[180,63],[181,63],[183,63],[185,63],[189,65],[190,65],[182,65],[184,65],[197,66],[210,67],[209,68],[200,69],[201,70],[208,71],[202,70],[203,69],[204,69],[205,69],[206,72],[199,73],[207,68],[38,74],[40,75],[37,76],[46,77],[49,78],[43,84],[44,86],[42,85],[47,83],[48,84],[35,85],[33,85]],"semanticDiagnosticsPerFile":[30,52,50,55,51,56,53,54,57,62,58,61,60,110,112,113,59,114,116,117,115,118,119,120,121,122,123,124,125,126,127,148,133,139,137,136,138,147,142,144,145,146,140,141,143,135,134,129,128,131,132,130,111,149,107,64,108,65,66,67,68,69,70,71,72,73,74,75,76,63,104,77,78,79,109,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,103,106,96,97,98,99,100,105,101,102,150,151,152,177,178,153,156,175,176,166,165,163,158,171,169,173,157,170,174,159,160,172,154,161,162,164,168,179,167,155,192,191,186,188,187,180,181,183,185,189,190,182,184,193,194,195,197,196,210,209,200,201,208,202,203,204,205,206,199,207,198,8,7,2,9,10,11,12,13,14,15,16,3,4,20,17,18,19,21,22,23,5,24,25,26,27,28,1,29,6,39,38,40,37,41,46,49,43,44,42,45,47,48,35,36,34,33,31,32],"latestChangedDtsFile":"./index.d.ts"},"version":"4.9.5"}
