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