-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
189 lines (169 loc) · 6.63 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
import { openDB } from 'idb';
const dbPromise = openDB('restful-queue', 1, {
upgrade(db) {
db.createObjectStore('requests', {
keyPath: 'id',
autoIncrement: true,
});
},
});
/**
* Stores a request in the queue and then attempts to send it to the server.
* If there are existing requests in the queue, those will be attempted first to ensure they are sent in the intended order.
* Succesful requests are removed from the queue. The first unsuccessful request blocks subsequent requests.
* Existing PUT and DELETE requests in the queue for the same URL will be dropped if the method is PUT or DELETE.
*
* @param {Request} request A Request object from the Fetch API. Must not be in an "unusable" state.
*
* @returns {Promise<Response>} A promise which resolves with a "202 Added to Queue" response once the request has been successfully added to the queue.
*/
export async function queueAndAttemptRequest(request) {
const requestid = await queueRequest(request);
// Asynchronously fire off a sync
// Suppress any failures, knowing the request is stored safely in the queue
syncRequests().catch(() => {});
return new Response(new Blob(), {status: 202, statusText: "Added to Queue"});
}
/**
* Stores a request object in indexDB
*
* @private
* @param {Request} request A Request object from the Fetch API
*
* @returns {Promise.<number>} A promise which resolves with a unique requestid when succesfully stored (or rejects on failure)
*/
async function queueRequest(request) {
// Store a cloned version of the request, so the orginal can still be fetched later
request = request.clone();
const { url, method } = request;
const headers = [...request.headers];
const body = await request.blob();
const rawData = { url, method, headers, body };
const db = await dbPromise;
if (method == "PUT" || method == "DELETE") await pruneQueueByUrl(url);
const requestid = await db.add('requests', rawData);
return requestid;
}
/**
* Attempts to fetch a request from the queue. If successful, the request is removed from the queue.
*
* @private
* @param {number} requestid The unique ID for this request stored in indexDB
* @param {Request} request A Request object from the Fetch API
*
* @returns {Promise.<Response>} A promise which resolves with the requests response following removal from the queue (or rejects on failure)
*/
async function attemptRequest(requestid, request) {
const response = await fetch(request);
if (response.status >= 500) throw new Error(`HTTP Error ${response.status} received`);
await removeFromQueue(requestid);
return response;
}
/**
* Removes a request from the queue
*
* @private
* @param {number} requestid The unique ID for the request to remove from indexDB+
*
* @returns {Promise} A promise which resolves when succesfully removed (or rejects on failure)
*/
async function removeFromQueue(requestid) {
const db = await dbPromise;
await db.delete('requests', requestid);
}
/**
* Fetches all the outstanding requests, along with their IDs from indexDB
* NB: getOutstandRequests is a simplified public wrapper for this function, which doesn't expose the internal requestids
*
* @private
* @returns {Promise.<Array.<{id: number, request: Request}>>} An array containing requests and their associated requestids
*/
async function getOutstandingRequestsAndIds() {
const db = await dbPromise;
return (await db.getAll('requests')).map(raw => {
const { url, method, headers, body, id } = raw;
const request = new Request(url, {method, headers, body});
return {id, request};
});
}
/**
* Fetches all the outstanding requests from the queue
*
* @returns {Promise.<Array.<Request>>} An array of outstanding requests in the order they are to be sent to the server
*/
export async function getOutstandingRequests() {
return (await getOutstandingRequestsAndIds())
.map(({request}) => request);
}
let currentSync = null;
let queueSync = false;
/**
* Asynchronously attempts to send queued requests to the server in order.
* Succesful requests are removed from the queue. The first unsuccessful request blocks subsequent requests.
* To avoid any race conditions, only one attempt to sync the queue will happen at a time.
* If called when an existing attempt is being made, another attempt will be made after the current one completes.
*
* @returns {Promise} A promise which resolves when all requests have been succesfully removed from the queue, or rejects after encountering the first failure
*/
export function syncRequests() {
queueSync = false;
// If there's no existing sync, then start one
if (!currentSync) {
currentSync = attemptOutstandingRequests()
// Once complete, set currentSync back to null, regardless of outcome
.finally(() => {
currentSync = null;
if (queueSync) return syncRequests();
});
} else {
// Otherwise, queue another sync after the current one.
queueSync = true;
}
return currentSync;
}
/**
* Attempts to fetch an outstanding requests, and if successful remove them from the queue.
* Stops after the first failure and doesn't attempt any subsequent requests in the queue.
* NB: Calling this function whilst a previous invocation hasn't completed yet, may cause a race condition. Use the `syncRequests` function to avoid this.
*
* @private
* @returns {Promise} A promise which resolves when all requests have been succesfully removed from the queue, or rejects after encountering the first failure
*/
async function attemptOutstandingRequests() {
const requests = await getOutstandingRequestsAndIds();
for (const {id, request} of requests) {
await attemptRequest(id, request);
}
}
/**
* Removes all PUT or DELETE requests to the given URL from the queue
* This is because a subsequest request will make these unneeded
*
* @private
* @param {string} url The URL of requests to prune from the queue
*
* @returns {Promise} A promise which resolves when the pruning has completed
*/
async function pruneQueueByUrl(url) {
const queue = await getOutstandingRequestsAndIds();
for (const {id, request} of queue) {
if (request.url != url) continue;
if (request.method != 'PUT' && request.method != 'DELETE') continue;
await removeFromQueue(id);
}
}
// Code which only gets run when in the context of a service worker
if (typeof self === "object" && !!self.serviceWorker) {
/**
* Attempts to sync any requests in the queue every 5 minutes
* If there are no requests in the queue, the impact of this should be negligible
* @private
*/
function regularSync() {
setTimeout(regularSync, 5 * 60 * 1000);
syncRequests();
}
regularSync();
// Automatically try to sync any requests in the queue any time the device comes online
self.addEventListener("online", syncRequests);
}