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