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