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