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