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