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