@serwist/background-sync 8.4.3 → 9.0.0-preview.0

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.
@@ -20,3 +20,4 @@ declare class BackgroundSyncPlugin implements SerwistPlugin {
20
20
  fetchDidFail: SerwistPlugin["fetchDidFail"];
21
21
  }
22
22
  export { BackgroundSyncPlugin };
23
+ //# sourceMappingURL=BackgroundSyncPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackgroundSyncPlugin.d.ts","sourceRoot":"","sources":["../src/BackgroundSyncPlugin.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C;;;GAGG;AACH,cAAM,oBAAqB,YAAW,aAAa;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B;;;;;OAKG;gBACS,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;IAIhD;;;OAGG;IACH,YAAY,EAAE,aAAa,CAAC,cAAc,CAAC,CAEzC;CACH;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
package/dist/Queue.d.ts CHANGED
@@ -114,7 +114,7 @@ declare class Queue {
114
114
  *
115
115
  * @returns
116
116
  */
117
- getAll(): Promise<Array<QueueEntry>>;
117
+ getAll(): Promise<QueueEntry[]>;
118
118
  /**
119
119
  * Returns the number of entries present in the queue.
120
120
  * Note that expired entries (per `maxRetentionTime`) are also included in this count.
@@ -167,3 +167,4 @@ declare class Queue {
167
167
  static get _queueNames(): Set<string>;
168
168
  }
169
169
  export { Queue };
170
+ //# sourceMappingURL=Queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../src/Queue.ts"],"names":[],"mappings":"AAgBA,UAAU,qBAAqB;IAC7B,KAAK,EAAE,KAAK,CAAC;CACd;AAED,UAAU,cAAc;IACtB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AA2BD;;;;GAIG;AACH,cAAM,KAAK;IACT,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAU;IAC7C,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,wBAAwB,CAAS;IAEzC;;;;;;;;OAQG;gBACS,IAAI,EAAE,MAAM,EAAE,EAAE,iBAAiB,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAE,YAAiB;IAgB5F;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;OAKG;IACG,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBnD;;;;;OAKG;IACG,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBtD;;;;;;OAMG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAInD;;;;;;OAMG;IACG,YAAY,IAAI,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAIrD;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAmBrC;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAI7B;;;;;;OAMG;IACG,WAAW,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAsB,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAmC1H;;;;;;;OAOG;IACG,cAAc,CAAC,SAAS,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IA0BjF;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBrC;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAenC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAmDxB;;;;;;OAMG;IACH,MAAM,KAAK,WAAW,IAAI,GAAG,CAAC,MAAM,CAAC,CAEpC;CACF;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
package/dist/QueueDb.d.ts CHANGED
@@ -87,3 +87,4 @@ export declare class QueueDb {
87
87
  */
88
88
  private _upgradeDb;
89
89
  }
90
+ //# sourceMappingURL=QueueDb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QueueDb.d.ts","sourceRoot":"","sources":["../src/QueueDb.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAexD,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,eAAgB,SAAQ,2BAA2B;IAClE,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AAEH,qBAAa,OAAO;IAClB,OAAO,CAAC,GAAG,CAA4C;IAEvD;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IASjE;;;;OAIG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAMpD;;;;;OAKG;IACG,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAM7E;;;;;OAKG;IACG,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKlE;;;;OAIG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK5C;;;;OAIG;IACG,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAIvF;;;;OAIG;IACG,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAItF;;;;;;;;OAQG;IACG,oBAAoB,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAOnH;;;;OAIG;YACW,KAAK;IASnB;;;;;;OAMG;IACH,OAAO,CAAC,UAAU;CAanB"}
@@ -72,3 +72,4 @@ export declare class QueueStore {
72
72
  */
73
73
  _removeEntry(entry?: QueueStoreEntry): Promise<QueueStoreEntry | undefined>;
74
74
  }
75
+ //# sourceMappingURL=QueueStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QueueStore.d.ts","sourceRoot":"","sources":["../src/QueueStore.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAGjF;;;;;;GAMG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IAEnC;;;;;OAKG;gBACS,SAAS,EAAE,MAAM;IAK7B;;;;OAIG;IACG,SAAS,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBlE;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BrE;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAItD;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAIxD;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAI1C;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAI7B;;;;;;;;;OASG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5C;;;;;;OAMG;IACG,YAAY,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;CAMlF"}
@@ -49,3 +49,4 @@ declare class StorableRequest {
49
49
  clone(): StorableRequest;
50
50
  }
51
51
  export { StorableRequest };
52
+ //# sourceMappingURL=StorableRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StorableRequest.d.ts","sourceRoot":"","sources":["../src/StorableRequest.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAiBnD,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;GAMG;AACH,cAAM,eAAe;IACnB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAE3C;;;;;;OAMG;WACU,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IA6BpE;;;;;;OAMG;gBACS,WAAW,EAAE,WAAW;IAyBpC;;;;OAIG;IACH,QAAQ,IAAI,WAAW;IAUvB;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;OAIG;IACH,KAAK,IAAI,eAAe;CAGzB;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
package/dist/index.d.ts CHANGED
@@ -21,3 +21,4 @@ declare global {
21
21
  }
22
22
  export { BackgroundSyncPlugin, Queue, QueueStore, StorableRequest };
23
23
  export type { QueueOptions, QueueEntry };
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,UAAU,WAAW;IACnB,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,yBAAyB;QACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;KAC5B;IAED,UAAU,SAAU,SAAQ,eAAe;QACzC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;QAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;KACtB;IAED,UAAU,gCAAgC;QACxC,IAAI,EAAE,SAAS,CAAC;KACjB;CACF;AAED,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;AAEpE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@serwist/background-sync",
3
- "version": "8.4.3",
3
+ "version": "9.0.0-preview.0",
4
4
  "type": "module",
5
5
  "description": "Queues failed requests and uses the Background Sync API to replay them when the network is available",
6
6
  "files": [
7
- "dist",
8
- "!dist/**/dts"
7
+ "src",
8
+ "dist"
9
9
  ],
10
10
  "keywords": [
11
11
  "serwist",
@@ -21,30 +21,31 @@
21
21
  "repository": "serwist/serwist",
22
22
  "bugs": "https://github.com/serwist/serwist/issues",
23
23
  "homepage": "https://serwist.pages.dev",
24
- "module": "./dist/index.js",
25
- "main": "./dist/index.cjs",
24
+ "main": "./dist/index.js",
26
25
  "types": "./dist/index.d.ts",
27
26
  "exports": {
28
27
  ".": {
29
- "import": {
30
- "types": "./dist/index.d.ts",
31
- "default": "./dist/index.js"
32
- },
33
- "require": {
34
- "types": "./dist/index.d.cts",
35
- "default": "./dist/index.cjs"
36
- }
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
37
30
  }
38
31
  },
39
32
  "dependencies": {
40
33
  "idb": "8.0.0",
41
- "@serwist/core": "8.4.3"
34
+ "@serwist/core": "9.0.0-preview.0"
42
35
  },
43
36
  "devDependencies": {
44
37
  "cross-env": "7.0.3",
45
- "rollup": "4.9.1",
46
- "typescript": "5.4.0-dev.20231226",
47
- "@serwist/constants": "8.4.3"
38
+ "rollup": "4.9.6",
39
+ "typescript": "5.4.0-dev.20240203",
40
+ "@serwist/constants": "9.0.0-preview.0"
41
+ },
42
+ "peerDependencies": {
43
+ "typescript": ">=5.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "typescript": {
47
+ "optional": true
48
+ }
48
49
  },
49
50
  "scripts": {
50
51
  "build": "rimraf dist && cross-env NODE_ENV=production rollup --config rollup.config.js",
@@ -0,0 +1,40 @@
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
+
9
+ import type { SerwistPlugin } from "@serwist/core";
10
+
11
+ import type { QueueOptions } from "./Queue.js";
12
+ import { Queue } from "./Queue.js";
13
+
14
+ /**
15
+ * A class implementing the `fetchDidFail` lifecycle callback. This makes it
16
+ * easier to add failed requests to a background sync Queue.
17
+ */
18
+ class BackgroundSyncPlugin implements SerwistPlugin {
19
+ private readonly _queue: Queue;
20
+
21
+ /**
22
+ * @param name See the `@serwist/background-sync.Queue`
23
+ * documentation for parameter details.
24
+ * @param options See the `@serwist/background-sync.Queue`
25
+ * documentation for parameter details.
26
+ */
27
+ constructor(name: string, options?: QueueOptions) {
28
+ this._queue = new Queue(name, options);
29
+ }
30
+
31
+ /**
32
+ * @param options
33
+ * @private
34
+ */
35
+ fetchDidFail: SerwistPlugin["fetchDidFail"] = async ({ request }) => {
36
+ await this._queue.pushRequest({ request });
37
+ };
38
+ }
39
+
40
+ export { BackgroundSyncPlugin };
package/src/Queue.ts ADDED
@@ -0,0 +1,444 @@
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 };