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