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