@enegalan/request-manager 1.0.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/main.js ADDED
@@ -0,0 +1,662 @@
1
+ /**
2
+ * RequestManager - A library for managing and regulating HTTP requests efficiently.
3
+ * @license MIT
4
+ * This library allows you to manage HTTP requests from any library (ajax, Ext.Ajax, axios, fetch, etc.)
5
+ * by accepting Promises as parameters. When a request is repeated with the same identifier,
6
+ * 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
+ */
12
+ class RequestManager {
13
+ constructor(managerOptions = {}) {
14
+ /**
15
+ * Map to store active requests by their unique identifier.
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
25
+ */
26
+ this.activeRequests = new Map();
27
+ /**
28
+ * Verbose mode: if true, cancellation errors will include messages
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}
40
+ */
41
+ this.options = {};
42
+ /**
43
+ * AbortController instance for the current request: will be flushed after the request is completed
44
+ * @type {AbortController}
45
+ */
46
+ this.abortController = null;
47
+ }
48
+
49
+ /**
50
+ * Flushes the options for the current request
51
+ * @private
52
+ */
53
+ #_flushRequestOptions() {
54
+ this.options = {};
55
+ }
56
+
57
+ /**
58
+ * Gets the manager options
59
+ * @returns {Object} Manager options
60
+ */
61
+ getOptions() {
62
+ return this.managerOptions;
63
+ }
64
+
65
+ /**
66
+ * Sets the manager options
67
+ * @param {Object} options - The options to set
68
+ */
69
+ 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
+ this.options = options;
83
+ }
84
+
85
+ /**
86
+ * Creates an AbortController and returns its signal.
87
+ * The AbortController is stored internally and will be used by the next request() call.
88
+ * This allows users to get the signal before creating the request.
89
+ *
90
+ * @returns {AbortSignal} The signal from a new AbortController
91
+ *
92
+ * @example
93
+ * const signal = requestManager.getSignal();
94
+ * requestManager.request('/api/users', fetch('/api/users', { signal }));
95
+ */
96
+ getSignal() {
97
+ return this.getAbortController().signal;
98
+ }
99
+
100
+ /**
101
+ * Gets the current AbortController instance
102
+ * Creates a new AbortController if none exists or if the current one is aborted
103
+ * @returns {AbortController} The current AbortController instance
104
+ */
105
+ getAbortController() {
106
+ // Create a new AbortController if none exists or if the current one is aborted
107
+ if (!this.abortController || this.abortController.signal.aborted) {
108
+ this.abortController = new AbortController();
109
+ }
110
+ return this.abortController;
111
+ }
112
+
113
+ /**
114
+ * Clears the current AbortController
115
+ * @private
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
130
+ */
131
+ #_generateRequestId(url, requestKey = null, noCancel = false) {
132
+ if (noCancel) { // Generate a unique ID to prevent cancellation
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}`;
154
+ }
155
+
156
+ /**
157
+ * Prepares fetch options by merging options and removing custom properties
158
+ * @param {Object} options - Configuration options
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
163
+ */
164
+ #_prepareFetchOptions(options, signal, additionalOptions = {}) {
165
+ const fetchOptions = Object.assign({}, additionalOptions);
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;
173
+ }
174
+
175
+ /**
176
+ * Internal method that handles the core request logic.
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
186
+ */
187
+ #_request(requestId, requestPromise, options = {}) {
188
+ this.#_setRequestOptions(options);
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;
267
+ }
268
+
269
+ /**
270
+ * Executes an HTTP request, cancelling any previous request with the same identifier.
271
+ *
272
+ * @param {string} url - The URL to request
273
+ * @param {Promise|Function} requestPromise - The request promise or function that returns a promise
274
+ * @param {Object} options - Optional configuration
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.)
282
+ * @returns {Promise} A Promise that resolves/rejects based on the most recent request
283
+ *
284
+ * @example
285
+ * // Request with Promise
286
+ * requestManager.request('/api/users', axios.get('/api/users', { cancelToken: axios.CancelToken.source().token }));
287
+ *
288
+ * @example
289
+ * // Request with Function
290
+ * requestManager.request('/api/users', ({ options }) => fetch('/api/users', { signal: options.signal, ...options }));
291
+ *
292
+ * @example
293
+ * // Request with Promise and custom cancellation grouping with requestKey
294
+ * const options = {
295
+ * requestKey: 'get-users',
296
+ * cancelToken: axios.CancelToken.source().cancel
297
+ * }
298
+ * 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
+ */
306
+ request(url, requestPromise, options = {}) {
307
+ const requestOptions = options || {};
308
+ const requestId = this.#_generateRequestId(url, requestOptions.requestKey, requestOptions.noCancel);
309
+ return this.#_request(requestId, requestPromise, requestOptions);
310
+ }
311
+
312
+ /**
313
+ * Executes an HTTP request using fetch, cancelling any previous request with the same identifier.
314
+ *
315
+ * @param {string} url - The URL to fetch
316
+ * @param {Object} options - Optional configuration
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.)
324
+ * @returns {Promise} A Promise that resolves/rejects based on the most recent request
325
+ *
326
+ * @example
327
+ * // Simple GET request
328
+ * requestManager.fetch('/api/users');
329
+ *
330
+ * @example
331
+ * // POST request with options
332
+ * requestManager.fetch('/api/users', {
333
+ * method: 'POST',
334
+ * headers: { 'Content-Type': 'application/json' },
335
+ * body: JSON.stringify({ name: 'John' })
336
+ * });
337
+ *
338
+ * @example
339
+ * // Request with requestKey for custom cancellation grouping with requestKey
340
+ * requestManager.fetch('/api/users', {
341
+ * requestKey: 'get-users'
342
+ * });
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
+ */
350
+ fetch(url, options = {}) {
351
+ const requestOptions = options || {};
352
+ const requestId = this.#_generateRequestId(url, requestOptions.requestKey, requestOptions.noCancel);
353
+ return this.#_request(requestId, url, requestOptions);
354
+ }
355
+
356
+ /**
357
+ * Executes an HTTP request using axios, cancelling any previous request with the same identifier.
358
+ *
359
+ * @param {string} url - The URL to request
360
+ * @param {Object} options - Optional configuration
361
+ * @param {string|number|Function} options.requestKey - Key to identify duplicate requests.
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.
367
+ * @returns {Promise} A Promise that resolves/rejects based on the most recent request
368
+ *
369
+ * @example
370
+ * // Simple GET request (uses global axios)
371
+ * requestManager.axios('/api/users');
372
+ *
373
+ * @example
374
+ * // With custom axios instance
375
+ * const myAxios = axios.create({ baseURL: 'https://api.example.com' });
376
+ * requestManager.axios('/users', {}, myAxios);
377
+ *
378
+ * @example
379
+ * // POST request with options
380
+ * requestManager.axios('/api/users', {
381
+ * method: 'POST',
382
+ * headers: { 'Content-Type': 'application/json' },
383
+ * body: JSON.stringify({ name: 'John' })
384
+ * });
385
+ *
386
+ * @example
387
+ * // Request with requestKey for custom cancellation grouping with requestKey
388
+ * requestManager.axios('/api/users', {
389
+ * requestKey: 'get-users'
390
+ * });
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
+ */
397
+ axios(url, options = {}, axiosInstance = null) {
398
+ const requestOptions = options || {};
399
+ const axiosLib = axiosInstance || axios;
400
+ const cancelToken = axiosLib.CancelToken.source();
401
+ const requestId = this.#_generateRequestId(url, requestOptions.requestKey, requestOptions.noCancel);
402
+ return this.#_request(requestId, axiosLib.get(url, { cancelToken: cancelToken.token, ...requestOptions }), {
403
+ cancelToken: cancelToken,
404
+ ...requestOptions
405
+ });
406
+ }
407
+
408
+ /**
409
+ * Executes an HTTP request using jQuery.ajax, cancelling any previous request with the same identifier.
410
+ *
411
+ * @param {Function} ajaxMethod - A function that receives { url, ...options } and returns a Promise
412
+ * @param {string} url - The URL to request
413
+ * @param {Object} options - Optional configuration
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
419
+ * @returns {Promise} A Promise that resolves/rejects based on the most recent request
420
+ *
421
+ * @example
422
+ * // Simple GET request
423
+ * requestManager.ajax(ajaxMethod, '/api/users');
424
+ *
425
+ * @example
426
+ * // POST request with options
427
+ * requestManager.ajax(ajaxMethod, '/api/users', {
428
+ * method: 'POST',
429
+ * headers: { 'Content-Type': 'application/json' },
430
+ * body: JSON.stringify({ name: 'John' })
431
+ * });
432
+ *
433
+ * @example
434
+ * // Request with requestKey for custom cancellation grouping with requestKey
435
+ * requestManager.ajax(ajaxMethod, '/api/users', {
436
+ * requestKey: 'get-users'
437
+ * });
438
+ */
439
+ ajax(ajaxMethod, url, options = {}) {
440
+ if (typeof ajaxMethod !== 'function') throw new Error('ajaxMethod must be a function');
441
+ const requestOptions = options || {};
442
+ const requestId = this.#_generateRequestId(url, requestOptions.requestKey, requestOptions.noCancel);
443
+ try {
444
+ const req = ajaxMethod({ url, ...requestOptions });
445
+ this.addAbortListener(req.abort, this.getSignal());
446
+ return this.#_request(requestId, req, requestOptions);
447
+ } catch (error) {
448
+ return Promise.reject(error);
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Executes an HTTP request using XMLHttpRequest, cancelling any previous request with the same identifier.
454
+ *
455
+ * @param {string} url - The URL to request
456
+ * @param {Object} options - Optional configuration
457
+ * @param {string} options.method - HTTP method (GET, POST, PUT, DELETE, etc.). Defaults to 'GET'.
458
+ * @param {Object} options.headers - Headers object to set on the request
459
+ * @param {string|FormData|Blob|ArrayBuffer} options.body - Request body
460
+ * @param {string} options.responseType - Response type ('text', 'json', 'blob', 'arraybuffer', 'document'). Defaults to 'text'.
461
+ * @param {boolean} options.withCredentials - Whether to send credentials with the request
462
+ * @param {number} options.timeout - Request timeout in milliseconds
463
+ * @param {string|number|Function} options.requestKey - Key to identify duplicate requests.
464
+ * If provided, requests with the same key will cancel previous ones.
465
+ * Can be a string, number, or function that returns a key.
466
+ * @param {boolean} options.noCancel - If true, this request will not cancel previous requests with the same ID, allowing concurrent requests
467
+ * @returns {Promise} A Promise that resolves/rejects based on the most recent request
468
+ *
469
+ * @example
470
+ * // Simple GET request
471
+ * requestManager.xhr('/api/users');
472
+ *
473
+ * @example
474
+ * // POST request with options
475
+ * requestManager.xhr('/api/users', {
476
+ * method: 'POST',
477
+ * headers: { 'Content-Type': 'application/json' },
478
+ * body: JSON.stringify({ name: 'John' })
479
+ * });
480
+ *
481
+ * @example
482
+ * // Request with requestKey for custom cancellation grouping
483
+ * requestManager.xhr('/api/users', {
484
+ * requestKey: 'get-users'
485
+ * });
486
+ *
487
+ * @example
488
+ * // Request with noCancel to allow concurrent requests
489
+ * requestManager.xhr('/api/lazy?load=1', { noCancel: true });
490
+ * requestManager.xhr('/api/lazy?load=2', { noCancel: true });
491
+ */
492
+ xhr(url, options = {}) {
493
+ const requestOptions = options || {};
494
+ const requestId = this.#_generateRequestId(url, requestOptions.requestKey, requestOptions.noCancel);
495
+ const xhrFunction = ({ options: fetchOptions }) => {
496
+ // Create XMLHttpRequest
497
+ const xhr = new XMLHttpRequest();
498
+ const method = (requestOptions.method || 'GET').toUpperCase();
499
+ // Create a promise that wraps the XHR request
500
+ const xhrPromise = new Promise((resolve, reject) => {
501
+ xhr.onload = function() {
502
+ if (xhr.status >= 200 && xhr.status < 300) {
503
+ let response = xhr.response;
504
+ if (requestOptions.responseType === 'json' ||
505
+ (xhr.getResponseHeader('Content-Type') && xhr.getResponseHeader('Content-Type').includes('application/json'))) {
506
+ try {
507
+ response = JSON.parse(xhr.responseText);
508
+ } catch (e) {
509
+ response = xhr.responseText;
510
+ }
511
+ }
512
+ resolve({
513
+ data: response,
514
+ status: xhr.status,
515
+ statusText: xhr.statusText,
516
+ headers: xhr.getAllResponseHeaders(),
517
+ xhr: xhr
518
+ });
519
+ } else {
520
+ reject({
521
+ message: `Request failed with status ${xhr.status}`,
522
+ status: xhr.status,
523
+ statusText: xhr.statusText,
524
+ xhr: xhr
525
+ });
526
+ }
527
+ };
528
+ xhr.onerror = function() {
529
+ reject({
530
+ message: 'Network error',
531
+ xhr: xhr
532
+ });
533
+ };
534
+ xhr.ontimeout = function() {
535
+ reject({
536
+ message: 'Request timeout',
537
+ xhr: xhr
538
+ });
539
+ };
540
+
541
+ // Open the request
542
+ xhr.open(method, url, true);
543
+
544
+ // Set response type
545
+ if (requestOptions.responseType) xhr.responseType = requestOptions.responseType;
546
+ // Set withCredentials
547
+ if (requestOptions.withCredentials !== undefined) xhr.withCredentials = requestOptions.withCredentials;
548
+ // Set timeout
549
+ if (requestOptions.timeout !== undefined) xhr.timeout = requestOptions.timeout;
550
+ // Set headers
551
+ if (requestOptions.headers) Object.keys(requestOptions.headers).forEach(key => {
552
+ xhr.setRequestHeader(key, requestOptions.headers[key]);
553
+ });
554
+
555
+ // Connect abort signal to xhr.abort()
556
+ if (fetchOptions.signal) fetchOptions.signal.addEventListener('abort', () => xhr.abort());
557
+
558
+ // Send the request
559
+ xhr.send(requestOptions.body || null);
560
+ });
561
+ return xhrPromise;
562
+ };
563
+ return this.#_request(requestId, xhrFunction, requestOptions);
564
+ }
565
+
566
+ /**
567
+ * Cancels a specific request by its identifier.
568
+ *
569
+ * @param {string} requestId - The unique identifier of the request to cancel
570
+ * @returns {boolean} True if the request was found and cancelled, false otherwise
571
+ */
572
+ cancel(requestId) {
573
+ const requestInfo = this.activeRequests.get(requestId);
574
+ if (!requestInfo) return false;
575
+
576
+ requestInfo.isCancelled = true; // Mark as cancelled
577
+
578
+ // Try to abort using AbortController (for fetch)
579
+ if (requestInfo.abortController && !requestInfo.abortController.signal.aborted) {
580
+ try {
581
+ requestInfo.abortController.abort('Request was cancelled');
582
+ } catch (error) {}
583
+ }
584
+
585
+ // Try to cancel using cancel token/function (for axios and others)
586
+ if (requestInfo.cancelToken) {
587
+ try {
588
+ if (typeof requestInfo.cancelToken === 'function') requestInfo.cancelToken();
589
+ else if (requestInfo.cancelToken.cancel) requestInfo.cancelToken.cancel();
590
+ } catch (error) {}
591
+ }
592
+
593
+ // Reject the wrapper promise
594
+ if (requestInfo.rejectWrapper && this.verbose) {
595
+ requestInfo.rejectWrapper(new Error('Request was cancelled'));
596
+ }
597
+ this.activeRequests.delete(requestId); // Remove from active requests
598
+ return true;
599
+ }
600
+
601
+ /**
602
+ * Link abort signal with HTTP client abort method.
603
+ * Useful for custom HTTP clients that only support the abort method to cancel requests.
604
+ * @param {Function} abortMethod - The abort method to call when the signal is aborted
605
+ * @param {AbortSignal} signal - The signal to listen to
606
+ */
607
+ addAbortListener(abortMethod, signal) {
608
+ if (!signal) return;
609
+ signal.addEventListener("abort", () => {
610
+ if (typeof abortMethod === 'function') {
611
+ try {
612
+ abortMethod();
613
+ } catch (error) {}
614
+ }
615
+ });
616
+ }
617
+
618
+ /**
619
+ * Cancels all active requests.
620
+ *
621
+ * @returns {number} The number of requests that were cancelled
622
+ */
623
+ cancelAll() {
624
+ const requestIds = Array.from(this.activeRequests.keys());
625
+ let cancelledCount = 0;
626
+ requestIds.forEach((requestId) => {
627
+ if (this.cancel(requestId)) cancelledCount++;
628
+ });
629
+ return cancelledCount;
630
+ }
631
+
632
+ /**
633
+ * Checks if a request with the given identifier is currently active.
634
+ *
635
+ * @param {string} requestId - The unique identifier to check
636
+ * @returns {boolean} True if the request is active, false otherwise
637
+ */
638
+ isActive(requestId) {
639
+ return this.activeRequests.has(requestId);
640
+ }
641
+
642
+ /**
643
+ * Gets the number of active requests.
644
+ *
645
+ * @returns {number} The number of currently active requests
646
+ */
647
+ getActiveCount() {
648
+ return this.activeRequests.size;
649
+ }
650
+
651
+ /**
652
+ * Clears all active requests without cancelling them.
653
+ * Use with caution - this will not cancel the underlying HTTP requests.
654
+ */
655
+ clear() {
656
+ this.activeRequests.clear();
657
+ }
658
+ }
659
+
660
+ export default RequestManager;
661
+
662
+ export { RequestManager };