@enegalan/request-manager 1.0.2 → 1.0.10
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 +316 -232
- package/dist/request-manager.cjs.js +298 -363
- package/dist/request-manager.cjs.js.map +1 -1
- package/dist/request-manager.cjs.min.js +3 -6
- package/dist/request-manager.cjs.min.js.map +1 -1
- package/dist/request-manager.esm.js +298 -363
- package/dist/request-manager.esm.js.map +1 -1
- package/dist/request-manager.esm.min.js +3 -6
- package/dist/request-manager.esm.min.js.map +1 -1
- package/dist/request-manager.js +298 -363
- package/dist/request-manager.js.map +1 -1
- package/dist/request-manager.min.js +2 -5
- package/dist/request-manager.min.js.map +1 -1
- package/dist/request-manager.umd.js +298 -363
- package/dist/request-manager.umd.js.map +1 -1
- package/dist/request-manager.umd.min.js +2 -5
- package/dist/request-manager.umd.min.js.map +1 -1
- package/index.d.ts +119 -70
- package/main.js +298 -364
- package/package.json +78 -69
package/dist/request-manager.js
CHANGED
|
@@ -4,94 +4,51 @@ var RequestManager = (function () {
|
|
|
4
4
|
/**
|
|
5
5
|
* RequestManager - A library for managing and regulating HTTP requests efficiently.
|
|
6
6
|
* @license MIT
|
|
7
|
+
* @author Eneko Galan <enekogalanelorza@gmail.com>
|
|
7
8
|
* This library allows you to manage HTTP requests from any library (ajax, Ext.Ajax, axios, fetch, etc.)
|
|
8
9
|
* by accepting Promises as parameters. When a request is repeated with the same identifier,
|
|
9
10
|
* the previous request is automatically cancelled and the new one is executed, giving priority to the most recent requests.
|
|
10
|
-
|
|
11
|
-
* @param {Object} managerOptions - The options for the manager
|
|
12
|
-
* @param {boolean} managerOptions.verbose - If true, cancellation errors will include messages
|
|
13
|
-
* @returns {RequestManager} A new RequestManager instance
|
|
14
|
-
*/
|
|
11
|
+
*/
|
|
15
12
|
class RequestManager {
|
|
16
|
-
constructor(
|
|
13
|
+
constructor(options = {}) {
|
|
17
14
|
/**
|
|
18
15
|
* Map to store active requests by their unique identifier.
|
|
19
|
-
*
|
|
20
|
-
* - promise: The original promise
|
|
21
|
-
* - abortController: AbortController instance (if available)
|
|
22
|
-
* - cancelToken: Cancel token (for axios compatibility)
|
|
23
|
-
* - wrapperPromise: The promise that wraps the request
|
|
24
|
-
* - resolveWrapper: The function to resolve the wrapper promise
|
|
25
|
-
* - rejectWrapper: The function to reject the wrapper promise
|
|
26
|
-
* - isCancelled: Whether the request has been cancelled
|
|
27
|
-
* - verbose: Whether to include verbose messages in the cancellation error
|
|
16
|
+
* @type {Map<string, import('./index.d.ts').ActiveRequest>}
|
|
28
17
|
*/
|
|
29
18
|
this.activeRequests = new Map();
|
|
30
19
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @type {boolean}
|
|
33
|
-
*/
|
|
34
|
-
this.verbose = managerOptions.verbose || false;
|
|
35
|
-
/**
|
|
36
|
-
* Manager options: the options that were passed to the constructor
|
|
37
|
-
* @type {Object}
|
|
20
|
+
* @type {import('./index.d.ts').Options}
|
|
38
21
|
*/
|
|
39
|
-
this.
|
|
40
|
-
/**
|
|
41
|
-
* Options for the current request: will be flushed after the request is completed
|
|
42
|
-
* @type {Object}
|
|
43
|
-
*/
|
|
44
|
-
this.options = {};
|
|
22
|
+
this.options = options;
|
|
45
23
|
/**
|
|
46
|
-
* AbortController
|
|
47
|
-
*
|
|
24
|
+
* One-shot AbortController for getSignal()/getAbortController() handoff.
|
|
25
|
+
* Consumed by the next request that does not pass options.abortController.
|
|
26
|
+
* @type {AbortController|null}
|
|
48
27
|
*/
|
|
49
28
|
this.abortController = null;
|
|
50
29
|
}
|
|
51
30
|
|
|
52
|
-
/**
|
|
53
|
-
* Flushes the options for the current request
|
|
54
|
-
* @private
|
|
55
|
-
*/
|
|
56
|
-
#_flushRequestOptions() {
|
|
57
|
-
this.options = {};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
31
|
/**
|
|
61
32
|
* Gets the manager options
|
|
62
|
-
* @returns {
|
|
33
|
+
* @returns {import('./index.d.ts').Options} Manager options
|
|
63
34
|
*/
|
|
64
35
|
getOptions() {
|
|
65
|
-
return this.
|
|
36
|
+
return this.options;
|
|
66
37
|
}
|
|
67
38
|
|
|
68
39
|
/**
|
|
69
40
|
* Sets the manager options
|
|
70
|
-
* @param {
|
|
41
|
+
* @param {import('./index.d.ts').Options} options - The options to set
|
|
71
42
|
*/
|
|
72
43
|
setOptions(options) {
|
|
73
|
-
this.managerOptions = options;
|
|
74
|
-
if (options.verbose !== undefined) {
|
|
75
|
-
this.verbose = options.verbose;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Sets the options for the current request
|
|
81
|
-
* @param {Object} options - The options to set
|
|
82
|
-
* @private
|
|
83
|
-
*/
|
|
84
|
-
#_setRequestOptions(options) {
|
|
85
44
|
this.options = options;
|
|
86
45
|
}
|
|
87
46
|
|
|
88
47
|
/**
|
|
89
|
-
* Creates
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
48
|
+
* Creates a new AbortController and returns its signal for the next request()
|
|
49
|
+
* (one getSignal → one request). Do not use for parallel requests; use fetch(),
|
|
50
|
+
* axios(), or request(url, ({ options }) => ...) instead — they create their own signal.
|
|
93
51
|
* @returns {AbortSignal} The signal from a new AbortController
|
|
94
|
-
*
|
|
95
52
|
* @example
|
|
96
53
|
* const signal = requestManager.getSignal();
|
|
97
54
|
* requestManager.request('/api/users', fetch('/api/users', { signal }));
|
|
@@ -101,197 +58,52 @@ var RequestManager = (function () {
|
|
|
101
58
|
}
|
|
102
59
|
|
|
103
60
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @returns {AbortController}
|
|
61
|
+
* Creates a new AbortController for the next request handoff.
|
|
62
|
+
* Always returns a fresh controller (never reuses one from another in-flight request).
|
|
63
|
+
* @returns {AbortController} A new AbortController instance
|
|
107
64
|
*/
|
|
108
65
|
getAbortController() {
|
|
109
|
-
|
|
110
|
-
if (!this.abortController || this.abortController.signal.aborted) {
|
|
111
|
-
this.abortController = new AbortController();
|
|
112
|
-
}
|
|
66
|
+
this.abortController = new AbortController();
|
|
113
67
|
return this.abortController;
|
|
114
68
|
}
|
|
115
69
|
|
|
116
70
|
/**
|
|
117
|
-
*
|
|
118
|
-
* @
|
|
119
|
-
|
|
120
|
-
#_clearAbortController() {
|
|
121
|
-
this.abortController = null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Generates a request identifier based on the requestKey or URL
|
|
126
|
-
* @param {string} url - The URL of the request
|
|
127
|
-
* @param {string|number|Function} requestKey - Optional key to generate a deterministic ID.
|
|
128
|
-
* If provided, requests with the same key will share the same ID.
|
|
129
|
-
* If null or undefined, the cleaned URL will be used as the key.
|
|
130
|
-
* @param {boolean} noCancel - If true, generates a unique ID to prevent cancellation
|
|
131
|
-
* @returns {string} A unique request identifier
|
|
132
|
-
* @private
|
|
71
|
+
* Checks if a request with the given identifier is currently active.
|
|
72
|
+
* @param {string} requestId - The unique identifier to check
|
|
73
|
+
* @returns {boolean} True if the request is active, false otherwise
|
|
133
74
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return `request_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
137
|
-
}
|
|
138
|
-
if (requestKey !== null && requestKey !== undefined) {
|
|
139
|
-
if (typeof requestKey === 'function') {
|
|
140
|
-
try {
|
|
141
|
-
requestKey = requestKey();
|
|
142
|
-
} catch (error) {
|
|
143
|
-
requestKey = null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (requestKey !== null && requestKey !== undefined) return `request_${String(requestKey)}`;
|
|
147
|
-
}
|
|
148
|
-
// Use cleaned URL as key when requestKey is null/undefined
|
|
149
|
-
let cleanedUrl = url || '';
|
|
150
|
-
let hasProtocol = cleanedUrl.includes('://');
|
|
151
|
-
if (hasProtocol) cleanedUrl = cleanedUrl.split('://')[1];
|
|
152
|
-
let hasParams = cleanedUrl.includes('?');
|
|
153
|
-
if (hasParams) cleanedUrl = cleanedUrl.split('?')[0];
|
|
154
|
-
let hasHash = cleanedUrl.includes('#');
|
|
155
|
-
if (hasHash) cleanedUrl = cleanedUrl.split('#')[0];
|
|
156
|
-
return `request_${cleanedUrl}`;
|
|
75
|
+
isActive(requestId) {
|
|
76
|
+
return this.activeRequests.has(requestId);
|
|
157
77
|
}
|
|
158
78
|
|
|
159
79
|
/**
|
|
160
|
-
*
|
|
161
|
-
* @
|
|
162
|
-
* @param {AbortSignal} signal - Abort signal to add to fetch options
|
|
163
|
-
* @param {Object} additionalOptions - Additional options to merge
|
|
164
|
-
* @returns {Object} Prepared fetch options
|
|
165
|
-
* @private
|
|
80
|
+
* Gets the number of active requests.
|
|
81
|
+
* @returns {number} The number of currently active requests
|
|
166
82
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const customOptions = ['abortController', 'cancelToken', 'requestKey', 'noCancel'];
|
|
170
|
-
Object.keys(options).forEach(key => {
|
|
171
|
-
if (customOptions.includes(key)) return;
|
|
172
|
-
fetchOptions[key] = options[key];
|
|
173
|
-
});
|
|
174
|
-
fetchOptions.signal = signal;
|
|
175
|
-
return fetchOptions;
|
|
83
|
+
getActiveCount() {
|
|
84
|
+
return this.activeRequests.size;
|
|
176
85
|
}
|
|
177
86
|
|
|
178
87
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* @param {string} requestId - Unique identifier for the request
|
|
182
|
-
* @param {Promise|Function|string} requestPromise - The request promise, function, or URL string
|
|
183
|
-
* @param {Object} options - Configuration options
|
|
184
|
-
* @param {AbortController} options.abortController - AbortController instance
|
|
185
|
-
* @param {Function} options.cancelToken - Cancel token or cancel function
|
|
186
|
-
* @param {boolean} options.noCancel - If true, this request will not cancel previous requests with the same ID
|
|
187
|
-
* @returns {Promise} A Promise that resolves/rejects based on the most recent request
|
|
188
|
-
* @private
|
|
88
|
+
* Clears all active requests without cancelling them.
|
|
89
|
+
* Use with caution - this will not cancel the underlying HTTP requests.
|
|
189
90
|
*/
|
|
190
|
-
|
|
191
|
-
this
|
|
192
|
-
if (!options.abortController) this.#_clearAbortController(); // Clear any existing abortController to ensure each request gets a fresh one
|
|
193
|
-
this.#_flushRequestOptions();
|
|
194
|
-
const abortController = options.abortController || this.getAbortController();
|
|
195
|
-
|
|
196
|
-
// Handle different types of requestPromise inputs
|
|
197
|
-
// Priority: Function (Custom logic) > String (URL) > Promise (axios, fetch, etc.)
|
|
198
|
-
if (typeof requestPromise === 'function') {
|
|
199
|
-
// Function: custom logic for any library (axios, ajax, etc.)
|
|
200
|
-
const fetchOptions = this.#_prepareFetchOptions(options, abortController.signal);
|
|
201
|
-
requestPromise = requestPromise({ options: fetchOptions });
|
|
202
|
-
} else if (typeof requestPromise === 'string') {
|
|
203
|
-
// String (URL): make fetch internally
|
|
204
|
-
const fetchOptions = this.#_prepareFetchOptions(options, abortController.signal);
|
|
205
|
-
requestPromise = fetch(requestPromise, fetchOptions);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Cancel previous request with the same ID if it exists
|
|
209
|
-
if (!options.noCancel) this.cancel(requestId);
|
|
210
|
-
|
|
211
|
-
// Create a wrapper promise that will be resolved/rejected based on the request
|
|
212
|
-
let resolveWrapper, rejectWrapper;
|
|
213
|
-
const wrapperPromise = new Promise((resolve, reject) => {
|
|
214
|
-
resolveWrapper = resolve;
|
|
215
|
-
rejectWrapper = reject;
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Store the request information
|
|
219
|
-
const requestInfo = {
|
|
220
|
-
promise: requestPromise,
|
|
221
|
-
abortController: abortController,
|
|
222
|
-
cancelToken: options.cancelToken || null,
|
|
223
|
-
wrapperPromise: wrapperPromise,
|
|
224
|
-
resolveWrapper: resolveWrapper,
|
|
225
|
-
rejectWrapper: rejectWrapper,
|
|
226
|
-
isCancelled: false,
|
|
227
|
-
verbose: this.verbose
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
this.activeRequests.set(requestId, requestInfo);
|
|
231
|
-
|
|
232
|
-
// Handle request promise completion
|
|
233
|
-
if (requestPromise && typeof requestPromise.then === 'function') {
|
|
234
|
-
try {
|
|
235
|
-
let req = requestPromise.then((result) => {
|
|
236
|
-
if (this.activeRequests.get(requestId) === requestInfo && !requestInfo.isCancelled) {
|
|
237
|
-
this.activeRequests.delete(requestId);
|
|
238
|
-
resolveWrapper(result);
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
if (req.catch) req.catch((error) => {
|
|
242
|
-
onError(this, error);
|
|
243
|
-
});
|
|
244
|
-
} catch (error) {
|
|
245
|
-
onError(this, error);
|
|
246
|
-
}
|
|
247
|
-
function onError(scope, error) {
|
|
248
|
-
// Check if this requestInfo is still the active one, or if it was cancelled
|
|
249
|
-
const currentRequestInfo = scope.activeRequests.get(requestId);
|
|
250
|
-
if (currentRequestInfo !== requestInfo) return;
|
|
251
|
-
// Only delete if this is still the active request
|
|
252
|
-
scope.activeRequests.delete(requestId);
|
|
253
|
-
if (!requestInfo.isCancelled) rejectWrapper(error);
|
|
254
|
-
else if (requestInfo.isCancelled && requestInfo.verbose) rejectWrapper(new Error('Request was cancelled'));
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
// If requestPromise is not a promise, we can't track its completion automatically
|
|
258
|
-
// This can happen with libraries like ExtJS that return request objects instead of promises
|
|
259
|
-
// In this case, the user should handle the request object themselves
|
|
260
|
-
// We'll resolve the wrapper promise immediately to prevent it from hanging
|
|
261
|
-
// The user can still use the request object returned by the library
|
|
262
|
-
setTimeout(() => {
|
|
263
|
-
if (this.activeRequests.get(requestId) === requestInfo && !requestInfo.isCancelled) {
|
|
264
|
-
this.activeRequests.delete(requestId);
|
|
265
|
-
resolveWrapper(requestPromise); // Resolve with the request object so the user can use it
|
|
266
|
-
}
|
|
267
|
-
}, 0);
|
|
268
|
-
}
|
|
269
|
-
return wrapperPromise;
|
|
91
|
+
clear() {
|
|
92
|
+
this.activeRequests.clear();
|
|
270
93
|
}
|
|
271
94
|
|
|
272
95
|
/**
|
|
273
96
|
* Executes an HTTP request, cancelling any previous request with the same identifier.
|
|
274
|
-
*
|
|
275
97
|
* @param {string} url - The URL to request
|
|
276
|
-
* @param {Promise|
|
|
277
|
-
* @param {
|
|
278
|
-
* @param {string|number|Function} options.requestKey - Key to identify duplicate requests.
|
|
279
|
-
* If provided, requests with the same key will cancel previous ones.
|
|
280
|
-
* Can be a string, number, or function that returns a key.
|
|
281
|
-
* @param {AbortController} options.abortController - AbortController instance (created automatically if not provided)
|
|
282
|
-
* @param {Function} options.cancelToken - Cancel token or cancel function for other libraries
|
|
283
|
-
* @param {boolean} options.noCancel - If true, this request will not cancel previous requests with the same ID, allowing concurrent requests
|
|
284
|
-
* Any other properties are passed as fetch options (method, headers, body, etc.)
|
|
98
|
+
* @param {Promise|import('./index.d.ts').RequestFunction|string} requestPromise - The request promise or function that returns a promise
|
|
99
|
+
* @param {import('./index.d.ts').RequestOptions} options - Optional configuration
|
|
285
100
|
* @returns {Promise} A Promise that resolves/rejects based on the most recent request
|
|
286
|
-
*
|
|
287
101
|
* @example
|
|
288
102
|
* // Request with Promise
|
|
289
103
|
* requestManager.request('/api/users', axios.get('/api/users', { cancelToken: axios.CancelToken.source().token }));
|
|
290
|
-
*
|
|
291
104
|
* @example
|
|
292
105
|
* // Request with Function
|
|
293
106
|
* requestManager.request('/api/users', ({ options }) => fetch('/api/users', { signal: options.signal, ...options }));
|
|
294
|
-
*
|
|
295
107
|
* @example
|
|
296
108
|
* // Request with Promise and custom cancellation grouping with requestKey
|
|
297
109
|
* const options = {
|
|
@@ -299,37 +111,19 @@ var RequestManager = (function () {
|
|
|
299
111
|
* cancelToken: axios.CancelToken.source().cancel
|
|
300
112
|
* }
|
|
301
113
|
* requestManager.request('/api/users', axios.get('/api/users', options), options);
|
|
302
|
-
*
|
|
303
|
-
* @example
|
|
304
|
-
* // Request with noCancel to allow concurrent requests (e.g., lazy loading)
|
|
305
|
-
* requestManager.request('/api/lazy?load=1', fetch('/api/lazy?load=1'), { noCancel: true });
|
|
306
|
-
* requestManager.request('/api/lazy?load=2', fetch('/api/lazy?load=2'), { noCancel: true });
|
|
307
|
-
* // Both requests will execute concurrently without canceling each other
|
|
308
114
|
*/
|
|
309
115
|
request(url, requestPromise, options = {}) {
|
|
310
|
-
|
|
311
|
-
const requestId = this.#_generateRequestId(url, requestOptions.requestKey, requestOptions.noCancel);
|
|
312
|
-
return this.#_request(requestId, requestPromise, requestOptions);
|
|
116
|
+
return this.#_request(this.getRequestId(url, options), requestPromise, options);
|
|
313
117
|
}
|
|
314
118
|
|
|
315
119
|
/**
|
|
316
120
|
* Executes an HTTP request using fetch, cancelling any previous request with the same identifier.
|
|
317
|
-
*
|
|
318
121
|
* @param {string} url - The URL to fetch
|
|
319
|
-
* @param {
|
|
320
|
-
* @param {string|number|Function} options.requestKey - Key to identify duplicate requests.
|
|
321
|
-
* If provided, requests with the same key will cancel previous ones.
|
|
322
|
-
* Can be a string, number, or function that returns a key.
|
|
323
|
-
* @param {AbortController} options.abortController - AbortController instance (created automatically if not provided)
|
|
324
|
-
* @param {Function} options.cancelToken - Cancel token or cancel function for other libraries
|
|
325
|
-
* @param {boolean} options.noCancel - If true, this request will not cancel previous requests with the same ID, allowing concurrent requests
|
|
326
|
-
* Any other properties are passed as fetch options (method, headers, body, etc.)
|
|
122
|
+
* @param {import('./index.d.ts').FetchOptions} options - Optional configuration
|
|
327
123
|
* @returns {Promise} A Promise that resolves/rejects based on the most recent request
|
|
328
|
-
*
|
|
329
124
|
* @example
|
|
330
125
|
* // Simple GET request
|
|
331
126
|
* requestManager.fetch('/api/users');
|
|
332
|
-
*
|
|
333
127
|
* @example
|
|
334
128
|
* // POST request with options
|
|
335
129
|
* requestManager.fetch('/api/users', {
|
|
@@ -337,47 +131,29 @@ var RequestManager = (function () {
|
|
|
337
131
|
* headers: { 'Content-Type': 'application/json' },
|
|
338
132
|
* body: JSON.stringify({ name: 'John' })
|
|
339
133
|
* });
|
|
340
|
-
*
|
|
341
134
|
* @example
|
|
342
135
|
* // Request with requestKey for custom cancellation grouping with requestKey
|
|
343
136
|
* requestManager.fetch('/api/users', {
|
|
344
137
|
* requestKey: 'get-users'
|
|
345
138
|
* });
|
|
346
|
-
*
|
|
347
|
-
* @example
|
|
348
|
-
* // Request with noCancel to allow concurrent requests (e.g., lazy loading)
|
|
349
|
-
* requestManager.fetch('/api/lazy?load=1', { noCancel: true });
|
|
350
|
-
* requestManager.fetch('/api/lazy?load=2', { noCancel: true });
|
|
351
|
-
* // Both requests will execute concurrently without canceling each other
|
|
352
139
|
*/
|
|
353
140
|
fetch(url, options = {}) {
|
|
354
|
-
|
|
355
|
-
const requestId = this.#_generateRequestId(url, requestOptions.requestKey, requestOptions.noCancel);
|
|
356
|
-
return this.#_request(requestId, url, requestOptions);
|
|
141
|
+
return this.#_request(this.getRequestId(url, options), url, options);
|
|
357
142
|
}
|
|
358
143
|
|
|
359
144
|
/**
|
|
360
145
|
* Executes an HTTP request using axios, cancelling any previous request with the same identifier.
|
|
361
|
-
*
|
|
362
146
|
* @param {string} url - The URL to request
|
|
363
|
-
* @param {
|
|
364
|
-
* @param {
|
|
365
|
-
* If provided, requests with the same key will cancel previous ones.
|
|
366
|
-
* Can be a string, number, or function that returns a key.
|
|
367
|
-
* @param {boolean} options.noCancel - If true, this request will not cancel previous requests with the same ID, allowing concurrent requests
|
|
368
|
-
* Any other properties are passed as axios options (method, headers, params, data, etc.)
|
|
369
|
-
* @param {Object} axiosInstance - Optional axios instance to use. If not provided, uses global axios.
|
|
147
|
+
* @param {import('./index.d.ts').AxiosRequestOptions} options - Optional configuration
|
|
148
|
+
* @param {import('./index.d.ts').AxiosInstance|import('./index.d.ts').AxiosStatic|null} axiosInstance - Optional axios instance to use. If not provided, uses global axios.
|
|
370
149
|
* @returns {Promise} A Promise that resolves/rejects based on the most recent request
|
|
371
|
-
*
|
|
372
150
|
* @example
|
|
373
151
|
* // Simple GET request (uses global axios)
|
|
374
152
|
* requestManager.axios('/api/users');
|
|
375
|
-
*
|
|
376
153
|
* @example
|
|
377
154
|
* // With custom axios instance
|
|
378
155
|
* const myAxios = axios.create({ baseURL: 'https://api.example.com' });
|
|
379
156
|
* requestManager.axios('/users', {}, myAxios);
|
|
380
|
-
*
|
|
381
157
|
* @example
|
|
382
158
|
* // POST request with options
|
|
383
159
|
* requestManager.axios('/api/users', {
|
|
@@ -385,94 +161,61 @@ var RequestManager = (function () {
|
|
|
385
161
|
* headers: { 'Content-Type': 'application/json' },
|
|
386
162
|
* body: JSON.stringify({ name: 'John' })
|
|
387
163
|
* });
|
|
388
|
-
*
|
|
389
164
|
* @example
|
|
390
165
|
* // Request with requestKey for custom cancellation grouping with requestKey
|
|
391
166
|
* requestManager.axios('/api/users', {
|
|
392
167
|
* requestKey: 'get-users'
|
|
393
168
|
* });
|
|
394
|
-
*
|
|
395
|
-
* @example
|
|
396
|
-
* // Request with noCancel to allow concurrent requests
|
|
397
|
-
* requestManager.axios('/api/lazy?load=1', { noCancel: true });
|
|
398
|
-
* requestManager.axios('/api/lazy?load=2', { noCancel: true });
|
|
399
169
|
*/
|
|
400
170
|
axios(url, options = {}, axiosInstance = null) {
|
|
401
|
-
const requestOptions = options || {};
|
|
402
171
|
const axiosLib = axiosInstance || axios;
|
|
403
172
|
const cancelToken = axiosLib.CancelToken.source();
|
|
404
|
-
const requestId = this
|
|
405
|
-
return this.#_request(requestId, axiosLib
|
|
173
|
+
const requestId = this.getRequestId(url, options);
|
|
174
|
+
return this.#_request(requestId, axiosLib({ url, cancelToken: cancelToken.token, ...options }), {
|
|
406
175
|
cancelToken: cancelToken,
|
|
407
|
-
...
|
|
176
|
+
...options,
|
|
408
177
|
});
|
|
409
178
|
}
|
|
410
179
|
|
|
411
180
|
/**
|
|
412
181
|
* Executes an HTTP request using jQuery.ajax, cancelling any previous request with the same identifier.
|
|
413
|
-
*
|
|
414
|
-
* @param {Function} ajaxMethod - A function that receives { url, ...options } and returns a Promise
|
|
182
|
+
* @param {import('./index.d.ts').AjaxFunction} ajaxFunction - A function that receives { url, ...options } and returns a Promise
|
|
415
183
|
* @param {string} url - The URL to request
|
|
416
|
-
* @param {
|
|
417
|
-
* @param {string|number|Function} options.requestKey - Key to identify duplicate requests.
|
|
418
|
-
* If provided, requests with the same key will cancel previous ones.
|
|
419
|
-
* Can be a string, number, or function that returns a key.
|
|
420
|
-
* @param {boolean} options.noCancel - If true, this request will not cancel previous requests with the same ID, allowing concurrent requests
|
|
421
|
-
* Any other properties are passed to the ajax method function
|
|
184
|
+
* @param {import('./index.d.ts').BaseRequestOptions & Record<string, any>} options - Optional configuration
|
|
422
185
|
* @returns {Promise} A Promise that resolves/rejects based on the most recent request
|
|
423
|
-
*
|
|
424
186
|
* @example
|
|
425
187
|
* // Simple GET request
|
|
426
|
-
* requestManager.ajax(
|
|
427
|
-
*
|
|
188
|
+
* requestManager.ajax(ajaxFunction, '/api/users');
|
|
428
189
|
* @example
|
|
429
190
|
* // POST request with options
|
|
430
|
-
* requestManager.ajax(
|
|
191
|
+
* requestManager.ajax(ajaxFunction, '/api/users', {
|
|
431
192
|
* method: 'POST',
|
|
432
193
|
* headers: { 'Content-Type': 'application/json' },
|
|
433
194
|
* body: JSON.stringify({ name: 'John' })
|
|
434
195
|
* });
|
|
435
|
-
*
|
|
436
196
|
* @example
|
|
437
197
|
* // Request with requestKey for custom cancellation grouping with requestKey
|
|
438
|
-
* requestManager.ajax(
|
|
198
|
+
* requestManager.ajax(ajaxFunction, '/api/users', {
|
|
439
199
|
* requestKey: 'get-users'
|
|
440
200
|
* });
|
|
441
201
|
*/
|
|
442
|
-
ajax(
|
|
443
|
-
if (typeof
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return this.#_request(requestId, req, requestOptions);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
return Promise.reject(error);
|
|
452
|
-
}
|
|
202
|
+
ajax(ajaxFunction, url, options = {}) {
|
|
203
|
+
if (typeof ajaxFunction !== 'function') throw new Error('ajaxFunction parameter must be a function');
|
|
204
|
+
return this.#_request(
|
|
205
|
+
this.getRequestId(url, options),
|
|
206
|
+
({ options: requestOptions }) => ajaxFunction({ url, ...requestOptions }),
|
|
207
|
+
options
|
|
208
|
+
);
|
|
453
209
|
}
|
|
454
210
|
|
|
455
211
|
/**
|
|
456
212
|
* Executes an HTTP request using XMLHttpRequest, cancelling any previous request with the same identifier.
|
|
457
|
-
*
|
|
458
213
|
* @param {string} url - The URL to request
|
|
459
|
-
* @param {
|
|
460
|
-
* @
|
|
461
|
-
* @param {Object} options.headers - Headers object to set on the request
|
|
462
|
-
* @param {string|FormData|Blob|ArrayBuffer} options.body - Request body
|
|
463
|
-
* @param {string} options.responseType - Response type ('text', 'json', 'blob', 'arraybuffer', 'document'). Defaults to 'text'.
|
|
464
|
-
* @param {boolean} options.withCredentials - Whether to send credentials with the request
|
|
465
|
-
* @param {number} options.timeout - Request timeout in milliseconds
|
|
466
|
-
* @param {string|number|Function} options.requestKey - Key to identify duplicate requests.
|
|
467
|
-
* If provided, requests with the same key will cancel previous ones.
|
|
468
|
-
* Can be a string, number, or function that returns a key.
|
|
469
|
-
* @param {boolean} options.noCancel - If true, this request will not cancel previous requests with the same ID, allowing concurrent requests
|
|
470
|
-
* @returns {Promise} A Promise that resolves/rejects based on the most recent request
|
|
471
|
-
*
|
|
214
|
+
* @param {import('./index.d.ts').XhrOptions} options - Optional configuration
|
|
215
|
+
* @returns {Promise<import('./index.d.ts').XhrResponse>} A Promise that resolves/rejects based on the most recent request
|
|
472
216
|
* @example
|
|
473
217
|
* // Simple GET request
|
|
474
218
|
* requestManager.xhr('/api/users');
|
|
475
|
-
*
|
|
476
219
|
* @example
|
|
477
220
|
* // POST request with options
|
|
478
221
|
* requestManager.xhr('/api/users', {
|
|
@@ -480,35 +223,32 @@ var RequestManager = (function () {
|
|
|
480
223
|
* headers: { 'Content-Type': 'application/json' },
|
|
481
224
|
* body: JSON.stringify({ name: 'John' })
|
|
482
225
|
* });
|
|
483
|
-
*
|
|
484
226
|
* @example
|
|
485
227
|
* // Request with requestKey for custom cancellation grouping
|
|
486
228
|
* requestManager.xhr('/api/users', {
|
|
487
229
|
* requestKey: 'get-users'
|
|
488
230
|
* });
|
|
489
|
-
*
|
|
490
|
-
* @example
|
|
491
|
-
* // Request with noCancel to allow concurrent requests
|
|
492
|
-
* requestManager.xhr('/api/lazy?load=1', { noCancel: true });
|
|
493
|
-
* requestManager.xhr('/api/lazy?load=2', { noCancel: true });
|
|
494
231
|
*/
|
|
495
232
|
xhr(url, options = {}) {
|
|
496
|
-
const
|
|
497
|
-
|
|
233
|
+
const requestId = this.getRequestId(url, options);
|
|
234
|
+
/** @type {import('./index.d.ts').RequestFunction<import('./index.d.ts').XhrResponse>} */
|
|
498
235
|
const xhrFunction = ({ options: fetchOptions }) => {
|
|
499
236
|
// Create XMLHttpRequest
|
|
500
237
|
const xhr = new XMLHttpRequest();
|
|
501
|
-
const method = (
|
|
238
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
502
239
|
// Create a promise that wraps the XHR request
|
|
503
240
|
const xhrPromise = new Promise((resolve, reject) => {
|
|
504
|
-
xhr.onload = function() {
|
|
241
|
+
xhr.onload = function () {
|
|
505
242
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
506
243
|
let response = xhr.response;
|
|
507
|
-
if (
|
|
508
|
-
|
|
244
|
+
if (
|
|
245
|
+
options.responseType === 'json' ||
|
|
246
|
+
(xhr.getResponseHeader('Content-Type') &&
|
|
247
|
+
xhr.getResponseHeader('Content-Type').includes('application/json'))
|
|
248
|
+
) {
|
|
509
249
|
try {
|
|
510
250
|
response = JSON.parse(xhr.responseText);
|
|
511
|
-
} catch
|
|
251
|
+
} catch {
|
|
512
252
|
response = xhr.responseText;
|
|
513
253
|
}
|
|
514
254
|
}
|
|
@@ -517,27 +257,27 @@ var RequestManager = (function () {
|
|
|
517
257
|
status: xhr.status,
|
|
518
258
|
statusText: xhr.statusText,
|
|
519
259
|
headers: xhr.getAllResponseHeaders(),
|
|
520
|
-
xhr: xhr
|
|
260
|
+
xhr: xhr,
|
|
521
261
|
});
|
|
522
262
|
} else {
|
|
523
263
|
reject({
|
|
524
264
|
message: `Request failed with status ${xhr.status}`,
|
|
525
265
|
status: xhr.status,
|
|
526
266
|
statusText: xhr.statusText,
|
|
527
|
-
xhr: xhr
|
|
267
|
+
xhr: xhr,
|
|
528
268
|
});
|
|
529
269
|
}
|
|
530
270
|
};
|
|
531
|
-
xhr.onerror = function() {
|
|
271
|
+
xhr.onerror = function () {
|
|
532
272
|
reject({
|
|
533
273
|
message: 'Network error',
|
|
534
|
-
xhr: xhr
|
|
274
|
+
xhr: xhr,
|
|
535
275
|
});
|
|
536
276
|
};
|
|
537
|
-
xhr.ontimeout = function() {
|
|
277
|
+
xhr.ontimeout = function () {
|
|
538
278
|
reject({
|
|
539
279
|
message: 'Request timeout',
|
|
540
|
-
xhr: xhr
|
|
280
|
+
xhr: xhr,
|
|
541
281
|
});
|
|
542
282
|
};
|
|
543
283
|
|
|
@@ -545,34 +285,73 @@ var RequestManager = (function () {
|
|
|
545
285
|
xhr.open(method, url, true);
|
|
546
286
|
|
|
547
287
|
// Set response type
|
|
548
|
-
if (
|
|
288
|
+
if (options.responseType) xhr.responseType = options.responseType;
|
|
549
289
|
// Set withCredentials
|
|
550
|
-
if (
|
|
290
|
+
if (options.withCredentials !== undefined) xhr.withCredentials = options.withCredentials;
|
|
551
291
|
// Set timeout
|
|
552
|
-
if (
|
|
292
|
+
if (options.timeout !== undefined) xhr.timeout = options.timeout;
|
|
553
293
|
// Set headers
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
|
|
294
|
+
if (options.headers)
|
|
295
|
+
Object.keys(options.headers).forEach((key) => {
|
|
296
|
+
xhr.setRequestHeader(key, options.headers[key]);
|
|
297
|
+
});
|
|
557
298
|
|
|
558
299
|
// Connect abort signal to xhr.abort()
|
|
559
300
|
if (fetchOptions.signal) fetchOptions.signal.addEventListener('abort', () => xhr.abort());
|
|
560
301
|
|
|
561
302
|
// Send the request
|
|
562
|
-
xhr.send(
|
|
303
|
+
xhr.send(options.body || null);
|
|
563
304
|
});
|
|
564
305
|
return xhrPromise;
|
|
565
306
|
};
|
|
566
|
-
return this.#_request(requestId, xhrFunction,
|
|
307
|
+
return this.#_request(requestId, xhrFunction, options);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Returns the request identifier for a URL and options.
|
|
312
|
+
* @param {string} url - The URL used when starting the request
|
|
313
|
+
* @param {import('./index.d.ts').BaseRequestOptions} options - Same options used for the request
|
|
314
|
+
* @returns {string} The request identifier
|
|
315
|
+
* @example
|
|
316
|
+
* requestManager.fetch('/api/users');
|
|
317
|
+
* const id = requestManager.getRequestId('/api/users');
|
|
318
|
+
* requestManager.cancel(id);
|
|
319
|
+
*/
|
|
320
|
+
getRequestId(url, options = {}) {
|
|
321
|
+
let requestKey = options.requestKey;
|
|
322
|
+
|
|
323
|
+
const prefix = 'request_';
|
|
324
|
+
|
|
325
|
+
// Generate a unique identifier to prevent cancellation for non cancelable requests
|
|
326
|
+
if (options.noCancel) {
|
|
327
|
+
return `${prefix}${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Handle function requestKey
|
|
331
|
+
if (typeof requestKey === 'function') {
|
|
332
|
+
try {
|
|
333
|
+
requestKey = requestKey();
|
|
334
|
+
} catch {
|
|
335
|
+
requestKey = null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (requestKey !== null && requestKey !== undefined) return `${prefix}${String(requestKey)}`;
|
|
339
|
+
|
|
340
|
+
// Use cleaned URL as key as fallback
|
|
341
|
+
let cleanedUrl = url || '';
|
|
342
|
+
if (cleanedUrl.includes('://')) cleanedUrl = cleanedUrl.split('://')[1];
|
|
343
|
+
if (cleanedUrl.includes('#')) cleanedUrl = cleanedUrl.split('#')[0];
|
|
344
|
+
if (!options.includeQuery && cleanedUrl.includes('?')) cleanedUrl = cleanedUrl.split('?')[0];
|
|
345
|
+
return `${prefix}${cleanedUrl}`;
|
|
567
346
|
}
|
|
568
347
|
|
|
569
348
|
/**
|
|
570
349
|
* Cancels a specific request by its identifier.
|
|
571
|
-
*
|
|
572
350
|
* @param {string} requestId - The unique identifier of the request to cancel
|
|
573
351
|
* @returns {boolean} True if the request was found and cancelled, false otherwise
|
|
574
352
|
*/
|
|
575
353
|
cancel(requestId) {
|
|
354
|
+
/** @type {import('./index.d.ts').ActiveRequest|undefined} */
|
|
576
355
|
const requestInfo = this.activeRequests.get(requestId);
|
|
577
356
|
if (!requestInfo) return false;
|
|
578
357
|
|
|
@@ -594,10 +373,11 @@ var RequestManager = (function () {
|
|
|
594
373
|
}
|
|
595
374
|
|
|
596
375
|
// Reject the wrapper promise
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
376
|
+
this.#_deleteRequest(
|
|
377
|
+
requestId,
|
|
378
|
+
requestInfo.rejectWrapper,
|
|
379
|
+
this.getOptions().verbose ? new Error(`Request ${requestId} was cancelled`) : null
|
|
380
|
+
);
|
|
601
381
|
return true;
|
|
602
382
|
}
|
|
603
383
|
|
|
@@ -608,8 +388,8 @@ var RequestManager = (function () {
|
|
|
608
388
|
* @param {AbortSignal} signal - The signal to listen to
|
|
609
389
|
*/
|
|
610
390
|
addAbortListener(abortMethod, signal) {
|
|
611
|
-
if (!signal) return;
|
|
612
|
-
signal.addEventListener(
|
|
391
|
+
if (!abortMethod || !signal) return;
|
|
392
|
+
signal.addEventListener('abort', () => {
|
|
613
393
|
if (typeof abortMethod === 'function') {
|
|
614
394
|
try {
|
|
615
395
|
abortMethod();
|
|
@@ -620,7 +400,6 @@ var RequestManager = (function () {
|
|
|
620
400
|
|
|
621
401
|
/**
|
|
622
402
|
* Cancels all active requests.
|
|
623
|
-
*
|
|
624
403
|
* @returns {number} The number of requests that were cancelled
|
|
625
404
|
*/
|
|
626
405
|
cancelAll() {
|
|
@@ -633,30 +412,186 @@ var RequestManager = (function () {
|
|
|
633
412
|
}
|
|
634
413
|
|
|
635
414
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* @param {
|
|
639
|
-
* @returns {
|
|
415
|
+
* Resolves the AbortController for a request: explicit option, pending handoff, or new.
|
|
416
|
+
* Clears the pending handoff so concurrent requests do not share it.
|
|
417
|
+
* @param {AbortController|undefined} provided - Optional AbortController from options
|
|
418
|
+
* @returns {AbortController}
|
|
419
|
+
* @private
|
|
640
420
|
*/
|
|
641
|
-
|
|
642
|
-
|
|
421
|
+
#_resolveAbortController(provided) {
|
|
422
|
+
const abortController = provided || this.abortController || new AbortController();
|
|
423
|
+
this.abortController = null;
|
|
424
|
+
return abortController;
|
|
643
425
|
}
|
|
644
426
|
|
|
645
427
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* @returns {
|
|
428
|
+
* Picks the best abort callback for a client request object.
|
|
429
|
+
* @param {Object} req - The request object
|
|
430
|
+
* @returns {Function|null}
|
|
431
|
+
* @private
|
|
649
432
|
*/
|
|
650
|
-
|
|
651
|
-
return
|
|
433
|
+
#_resolveAbortMethod(req) {
|
|
434
|
+
if (!req) return null;
|
|
435
|
+
if (typeof req.abort === 'function') return () => req.abort();
|
|
436
|
+
const ExtAjax =
|
|
437
|
+
typeof globalThis !== 'undefined' && globalThis.Ext && globalThis.Ext.Ajax ? globalThis.Ext.Ajax : null;
|
|
438
|
+
if (req.xhr && ExtAjax && typeof ExtAjax.abort === 'function') {
|
|
439
|
+
return () => ExtAjax.abort(req);
|
|
440
|
+
}
|
|
441
|
+
if (req.xhr && typeof req.xhr.abort === 'function') return () => req.xhr.abort();
|
|
442
|
+
return null;
|
|
652
443
|
}
|
|
653
444
|
|
|
654
445
|
/**
|
|
655
|
-
*
|
|
656
|
-
*
|
|
446
|
+
* Prepares request options by merging options and removing custom properties
|
|
447
|
+
* @param {import('./index.d.ts').RequestOptions} options - Configuration options
|
|
448
|
+
* @param {AbortSignal} signal - Abort signal to add to request options
|
|
449
|
+
* @returns {Object} Prepared request options
|
|
450
|
+
* @private
|
|
657
451
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
452
|
+
#_prepareRequestOptions(options, signal) {
|
|
453
|
+
const requestOptions = {};
|
|
454
|
+
const customOptions = ['abortController', 'cancelToken', 'requestKey', 'noCancel', 'includeQuery'];
|
|
455
|
+
Object.keys(options).forEach((key) => {
|
|
456
|
+
if (customOptions.includes(key)) return;
|
|
457
|
+
requestOptions[key] = options[key];
|
|
458
|
+
});
|
|
459
|
+
requestOptions.signal = signal;
|
|
460
|
+
return requestOptions;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Deletes a request from the active requests map and rejects the wrapper promise
|
|
465
|
+
* @param {string} requestId - The unique identifier of the request
|
|
466
|
+
* @param {Function} rejectWrapper - The function to reject the wrapper promise
|
|
467
|
+
* @param {*} error - The error to reject the wrapper promise with; the wrapper is not rejected if null/undefined
|
|
468
|
+
* @private
|
|
469
|
+
*/
|
|
470
|
+
#_deleteRequest(requestId, rejectWrapper, error) {
|
|
471
|
+
this.activeRequests.delete(requestId);
|
|
472
|
+
if (error !== null && error !== undefined) {
|
|
473
|
+
rejectWrapper(error);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Completes a request by deleting it from the active requests map and resolving the wrapper promise
|
|
479
|
+
* @param {string} requestId - The unique identifier of the request
|
|
480
|
+
* @param {Function} resolveWrapper - The function to resolve the wrapper promise
|
|
481
|
+
* @param {Promise} requestPromise - The request promise
|
|
482
|
+
* @param {boolean} isCancelled - Whether the request was cancelled
|
|
483
|
+
* @private
|
|
484
|
+
*/
|
|
485
|
+
#_completeRequest(requestId, resolveWrapper, requestPromise, isCancelled) {
|
|
486
|
+
this.activeRequests.delete(requestId);
|
|
487
|
+
if (!isCancelled) {
|
|
488
|
+
resolveWrapper(requestPromise);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Internal method that handles the core request logic.
|
|
494
|
+
* @param {string} requestId - Unique identifier for the request
|
|
495
|
+
* @param {Promise|import('./index.d.ts').RequestFunction|string} requestPromise - The request promise, function, or URL string
|
|
496
|
+
* @param {import('./index.d.ts').BaseRequestOptions} options - Configuration options
|
|
497
|
+
* @returns {Promise} A Promise that resolves/rejects based on the most recent request
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
#_request(requestId, requestPromise, options = {}) {
|
|
501
|
+
const abortController = this.#_resolveAbortController(options.abortController);
|
|
502
|
+
|
|
503
|
+
// Handle different types of requestPromise inputs
|
|
504
|
+
// Priority: Function (Custom logic) > String (URL) > Promise (axios, fetch, etc.)
|
|
505
|
+
if (typeof requestPromise === 'function') {
|
|
506
|
+
// Function: custom logic for any library (axios, ajax, etc.)
|
|
507
|
+
try {
|
|
508
|
+
requestPromise = requestPromise({
|
|
509
|
+
options: this.#_prepareRequestOptions(options, abortController.signal),
|
|
510
|
+
});
|
|
511
|
+
} catch (error) {
|
|
512
|
+
return Promise.reject(error);
|
|
513
|
+
}
|
|
514
|
+
} else if (typeof requestPromise === 'string') {
|
|
515
|
+
// String (URL): make fetch internally
|
|
516
|
+
try {
|
|
517
|
+
requestPromise = fetch(requestPromise, this.#_prepareRequestOptions(options, abortController.signal));
|
|
518
|
+
} catch (error) {
|
|
519
|
+
return Promise.reject(error);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Link the request object's abort method (Ext.Ajax, XHR, etc.) to the cancellation signal
|
|
524
|
+
this.addAbortListener(this.#_resolveAbortMethod(requestPromise), abortController.signal);
|
|
525
|
+
|
|
526
|
+
// Cancel previous request with the same identifier if it exists
|
|
527
|
+
if (!options.noCancel) this.cancel(requestId);
|
|
528
|
+
|
|
529
|
+
// Create a wrapper promise that will be resolved/rejected based on the request
|
|
530
|
+
let resolveWrapper, rejectWrapper;
|
|
531
|
+
const wrapperPromise = new Promise((resolve, reject) => {
|
|
532
|
+
resolveWrapper = resolve;
|
|
533
|
+
rejectWrapper = reject;
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* @type {import('./index.d.ts').ActiveRequest}
|
|
538
|
+
*/
|
|
539
|
+
const requestInfo = {
|
|
540
|
+
promise: requestPromise,
|
|
541
|
+
abortController: abortController,
|
|
542
|
+
cancelToken: options.cancelToken || null,
|
|
543
|
+
resolveWrapper: resolveWrapper,
|
|
544
|
+
rejectWrapper: rejectWrapper,
|
|
545
|
+
isCancelled: false,
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
this.activeRequests.set(requestId, requestInfo);
|
|
549
|
+
|
|
550
|
+
// Handle request promise completion
|
|
551
|
+
if (requestPromise && typeof requestPromise.then === 'function') {
|
|
552
|
+
try {
|
|
553
|
+
let req = requestPromise.then((result) => {
|
|
554
|
+
if (this.activeRequests.get(requestId) !== requestInfo) return;
|
|
555
|
+
this.#_completeRequest(requestId, resolveWrapper, result, requestInfo.isCancelled);
|
|
556
|
+
});
|
|
557
|
+
if (req.catch)
|
|
558
|
+
req.catch((error) => {
|
|
559
|
+
onError(this, error);
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
onError(this, error);
|
|
563
|
+
}
|
|
564
|
+
function onError(scope, error) {
|
|
565
|
+
// Check if this requestInfo is still the active one, or if it was cancelled
|
|
566
|
+
if (scope.activeRequests.get(requestId) !== requestInfo) return;
|
|
567
|
+
if (requestInfo.isCancelled) {
|
|
568
|
+
// Already cancelled: let cancel() handle cleanup and reject the wrapper promise
|
|
569
|
+
scope.cancel(requestId);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// Only delete if this is still the active request
|
|
573
|
+
scope.#_deleteRequest(requestId, rejectWrapper, error);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
// Non-promise (Ext.Ajax request object, raw XHR, etc.). Keep tracked until the
|
|
577
|
+
// underlying XHR finishes so a later duplicate can still cancel it.
|
|
578
|
+
const xhr =
|
|
579
|
+
requestPromise &&
|
|
580
|
+
(requestPromise.xhr ||
|
|
581
|
+
(typeof XMLHttpRequest !== 'undefined' && requestPromise instanceof XMLHttpRequest
|
|
582
|
+
? requestPromise
|
|
583
|
+
: null));
|
|
584
|
+
const finish = () => {
|
|
585
|
+
if (this.activeRequests.get(requestId) !== requestInfo) return;
|
|
586
|
+
this.#_completeRequest(requestId, resolveWrapper, requestPromise, requestInfo.isCancelled);
|
|
587
|
+
};
|
|
588
|
+
if (xhr && typeof xhr.addEventListener === 'function') {
|
|
589
|
+
xhr.addEventListener('loadend', finish);
|
|
590
|
+
} else {
|
|
591
|
+
setTimeout(finish, 0);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return wrapperPromise;
|
|
660
595
|
}
|
|
661
596
|
}
|
|
662
597
|
|