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