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