RequestManager is a JavaScript library designed to manage and regulate HTTP requests efficiently. It cancels duplicate in-flight calls and works with fetch, axios, jQuery.ajax, Ext.Ajax, raw XHR, and custom clients.
RequestManager avoids repeated HTTP requests: when a new call starts with the same identifier (cleaned URL, method, or a custom requestKey), the previous one is aborted on the spot. You decide what cancels what — group by URL, key, or query string; or let requests run concurrently with noCancel.
requestManager.fetch('/api/search?q=hi'); // aborted when...
requestManager.fetch('/api/search?q=ho'); // ...this one starts (same cleaned URL)
requestManager.fetch('/api/feed', { noCancel: true }); // never cancelled| Universal compatibility | Dedicated helpers for fetch, axios, ajax-style clients (jQuery / Ext.Ajax), and XMLHttpRequest — plus a low-level request() escape hatch |
| Real cancellation | Duplicates are aborted at the network level (AbortSignal, req.abort(), xhr.abort()) — not silently discarded |
| Latest request wins | Only the most recent request per identifier survives; older ones are aborted automatically |
| Simple API | Pick the helper for your client and everything is wired for you. Manual abort plumbing only if you drop to request() |
| Configurable grouping | Shared options across helpers: requestKey, noCancel, includeQuery, includeMethod |
| TypeScript support | Full type definitions included and resolved automatically |
| Every module format | ESM, CommonJS, UMD, and minified CDN bundles |
- @enegalan/request-manager
- Why?
- Key Features
- Table of Contents
- Installation
- Usage
- Usage in Different Environments
- TypeScript
- Which method should I use?
- Basic Example with fetch()
- POST Request with Options
- Using request()
- Automatic Cancellation with Same URL
- Using requestKey to Override URL-based ID
- Using requestKey with Function
- Using noCancel to Allow Concurrent Requests
- Using includeQuery to Distinguish Query Strings
- Using with Axios
- Using with jQuery / Ext.Ajax (
ajax()) - Using with Other Libraries
- API Reference
new RequestManager(options)request(url, requestPromise, options)fetch(url, options)axios(url, options, axiosInstance)ajax(ajaxFunction, url, options)xhr(url, options)getRequestId(url, options)cancel(requestId)cancelAll()getActiveRequests()getActiveRequest(requestId)isActive(requestId)getActiveCount()clear()getSignal()getAbortController()getOptions()setOptions(options)addAbortListener(abortMethod, signal)
- Browser Support
- Contributing
- License
npm install @enegalan/request-manager
# or
yarn add @enegalan/request-manager
# or
pnpm add @enegalan/request-managerES Modules (recommended):
import RequestManager from '@enegalan/request-manager';CommonJS:
const { RequestManager } = require('@enegalan/request-manager');Browser (CDN):
<!-- Using unpkg -->
<script src="https://unpkg.com/@enegalan/request-manager/dist/request-manager.min.js"></script>
<!-- Or using jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@enegalan/request-manager/dist/request-manager.min.js"></script>
<script>
const requestManager = new RequestManager();
</script>Full TypeScript support is included. Types are automatically resolved:
import RequestManager, { XhrOptions } from '@enegalan/request-manager';
const requestManager = new RequestManager({ verbose: true });
const options: XhrOptions = { method: 'GET', responseType: 'json' };
const xhr = await requestManager.xhr('/api/user/1', options);
const user = xhr.response; // caller reads/parses the XHRPick the dedicated helper for your HTTP client. Use request() only when none of the helpers fit.
| Client | Use this | Why |
|---|---|---|
fetch |
fetch(url, options) |
Creates the AbortSignal and passes it to fetch for you |
axios |
axios(url, options, axiosInstance?) |
Creates an AbortSignal and wires cancel for you (axios ≥ 0.22) |
jQuery .ajax, Ext.Ajax, similar |
ajax(ajaxFunction, url, options) |
Runs your ajax function, then wires abort for you (req.abort, Ext.Ajax.abort(req), or xhr.abort) |
Raw XMLHttpRequest |
xhr(url, options) |
Owns open/send and abort lifecycle |
| Custom / already-started Promise | request(url, promiseOrFn, options) |
Escape hatch — you must pass signal / cancelToken / addAbortListener |
Rule of thumb
- Known client → use its helper (
fetch/axios/ajax/xhr). - Helper already cancels the real network call — no manual abort wiring.
request()is for edge cases (wrapping an existing Promise, exotic clients). Same cancellation map as the helpers, but abort plumbing is your job.
// Preferred
requestManager.fetch('/api/users');
requestManager.axios('/api/users');
requestManager.ajax(({ url, ...opts }) => $.ajax({ url, ...opts }), '/api/users');
requestManager.ajax(({ url, ...opts }) => Ext.Ajax.request({ url, ...opts }), '/api/users');
requestManager.xhr('/api/users');
// Escape hatch — you wire cancel yourself (see API notes below)
requestManager.request('/api/users', ({ options }) => fetch('/api/users', { signal: options.signal, ...options }));Important
Calling request(url, Ext.Ajax.request(...)) or request(url, $.ajax(...)) without linking abort (via addAbortListener / cancelToken / signal) does not abort the browser request when a duplicate starts. Use ajax() for those clients.
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager({ verbose: true });
requestManager
.fetch('/api/users')
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.message.includes('was cancelled')) {
console.log('Request was cancelled');
} else {
console.error('Request failed:', error);
}
});import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
requestManager
.fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John' }),
})
.then((response) => response.json())
.then((data) => console.log(data));request() is the low-level API when you already have a Promise or need custom wiring.
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// Function form — options include signal; pass it into fetch (or prefer requestManager.fetch)
requestManager
.request('/api/users', ({ options }) => {
return fetch('/api/users', { signal: options.signal, ...options });
})
.then((response) => response.json())
.then((data) => console.log(data));
// Pre-created Promise — must also pass abortController / cancelToken or cancel is incomplete
const abortController = requestManager.getAbortController();
requestManager.request('/api/users', fetch('/api/users', { signal: abortController.signal }), {
abortController,
});import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// By default, requests with the same method + URL (cleaned) will cancel previous ones
// The URL is automatically cleaned (protocol and query params removed) and the HTTP
// method is prepended to generate the request ID (e.g. request_GET_/api/search)
requestManager.fetch('/api/search?q=test').catch((error) => {
console.log('First request cancelled:', error.message);
});
// This second request will automatically cancel the first one
// because they share the same cleaned URL
setTimeout(() => {
requestManager
.fetch('/api/search?q=updated')
.then((response) => response.json())
.then((data) => console.log('Second request completed:', data));
}, 100);import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// You can use requestKey to override the default URL-based ID generation
requestManager
.fetch('/api/search?q=test', {
requestKey: 'search-users', // Custom key instead of cleaned URL
})
.catch((error) => {
console.log('First request cancelled:', error.message);
});
// This second request will cancel the first one because they share the same requestKey
setTimeout(() => {
requestManager
.fetch('/api/search?q=updated', {
requestKey: 'search-users', // Same key = same request ID = cancellation
})
.then((response) => response.json())
.then((data) => console.log('Second request completed:', data));
}, 100);import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// You can use a function to generate the requestKey dynamically
function searchUsers(query) {
return requestManager.fetch(`/api/search?q=${query}`, {
requestKey: () => `search-${query}`, // Function that returns the key
});
}
// Both calls will share the same requestKey and cancel each other
searchUsers('test');
searchUsers('test'); // This will cancel the previous oneimport RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// Use noCancel: true to allow multiple requests to execute concurrently
// This is useful for lazy loading scenarios where you want all requests to complete
requestManager
.fetch('/api/lazy?load=1', { noCancel: true })
.then((response) => response.json())
.then((data) => console.log('Load 1:', data));
requestManager
.fetch('/api/lazy?load=2', { noCancel: true })
.then((response) => response.json())
.then((data) => console.log('Load 2:', data));
requestManager
.fetch('/api/lazy?load=3', { noCancel: true })
.then((response) => response.json())
.then((data) => console.log('Load 3:', data));
// All three requests will execute concurrently without canceling each other
// Even though they share the same cleaned URL (without query params)import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// By default, query params are stripped from the ID:
// /api/users?page=1 and /api/users?page=2 share the same ID and cancel each other.
// With includeQuery: true, the query string is part of the ID
requestManager.fetch('/api/users?page=1', { includeQuery: true });
requestManager.fetch('/api/users?page=2', { includeQuery: true });
// Both run — different query = different ID
// Same full URL still cancels the previous one
requestManager.fetch('/api/users?page=1', { includeQuery: true });
requestManager.fetch('/api/users?page=1', { includeQuery: true }); // cancels the previous page=1import axios from 'axios';
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
requestManager
.axios('/api/users')
.then((response) => console.log(response.data))
.catch((error) => {
if (axios.isCancel(error)) {
console.log('Request was cancelled');
} else {
console.error('Request failed:', error);
}
});ajax() invokes your function, inspects the returned request object, and registers abort automatically (req.abort, Ext.Ajax.abort(req), or xhr.abort).
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// jQuery
requestManager.ajax($.ajax.bind($), '/api/users', { method: 'GET' });
// Ext.Ajax — return the Ext request object (not a Promise)
requestManager.ajax(({ url, ...options }) => Ext.Ajax.request({ url, ...options }), '/api/users');
// Or bind Ext.Ajax.request directly when options shape matches
requestManager.ajax(Ext.Ajax.request.bind(Ext.Ajax), '/api/users');Equivalent with request() (more boilerplate — not recommended):
requestManager.request('/api/users', ({ options }) => {
const req = Ext.Ajax.request({ url: '/api/users', ...options });
requestManager.addAbortListener(() => Ext.Ajax.abort(req), options.signal);
return req;
});If there is no dedicated helper, use request() and make sure you wire abort yourself via options.signal (or pass cancelToken / addAbortListener).
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
requestManager
.request('/api/data', ({ options }) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(new Error('Request failed'));
xhr.send();
options.signal.addEventListener('abort', () => {
xhr.abort();
reject(new Error('Request was cancelled'));
});
});
})
.then((data) => console.log(data))
.catch((error) => console.error(error));Creates a new RequestManager instance.
Parameters:
options(Object, optional): Configuration optionsverbose(boolean, optional): If true, cancellation rejects with a message that includes the request id. If false (default), cancellation is silent (wrapper promise does not settle; nothing is logged).
Example:
// Create with verbose mode enabled
const requestManager = new RequestManager({ verbose: true });Low-level entry point. Tracks the call by ID and cancels the previous one with the same ID — but you must connect abort to the underlying client (signal, cancelToken, or addAbortListener). Otherwise the manager drops the tracked entry while the HTTP request may keep running.
Parameters:
url(string): The URL of the request (used to generate request ID from cleaned URL)requestPromise(Promise|Function|string): A Promise from any HTTP library, a Function that receives{ options }and returns a Promise/request object, or a URL string (fetch internally)options(Object, optional): Configuration optionsabortController(AbortController): AbortController instance (created automatically if not provided)cancelToken(Function|Object): Cancel token or cancel function for other librariesrequestKey(string|number|Function, optional): Key to identify duplicate requests. If provided, requests with the same key will share the same ID and cancel previous ones. If not provided, the cleaned URL is used as the key. Can be a string, number, or function that returns a key.noCancel(boolean): If true, this request will not cancel previous requests with the same ID, allowing concurrent requests. Useful for lazy loading scenarios where multiple requests should execute in parallel.includeQuery(boolean): If true, keeps the query string when generating the request ID from the URL.includeMethod(boolean): If true (default), the HTTP method is part of the URL-based request ID
Tip
When requestPromise is a Function, you can pass custom properties in options. These will be accessible inside the callback via the { options } parameter.
Returns: Promise that resolves/rejects based on the most recent request
Note: The request ID is automatically generated from the cleaned URL (protocol and hash removed; query params removed unless includeQuery is true; HTTP method included unless includeMethod is false) unless requestKey is specified. When noCancel is true, a unique ID is generated for each request to prevent cancellation. When requestPromise is a Function, it receives { options } where options contains the signal (AbortSignal) and any other fetch options.
Executes an HTTP request using fetch, cancelling any previous request with the same identifier.
Parameters:
url(string): The URL to fetchoptions(Object, optional): Configuration options (same asrequest()method)-
requestKey(string|number|Function, optional): Key to identify duplicate requests. If not provided, the cleaned URL is used as the key. -
abortController(AbortController): AbortController instance (created automatically if not provided) -
cancelToken(Function|Object): Cancel token or cancel function for other libraries -
noCancel(boolean): If true, this request will not cancel previous requests with the same ID, allowing concurrent requests -
includeQuery(boolean): If true, keeps the query string in the URL-based request ID -
includeMethod(boolean): If true (default), the HTTP method is part of the URL-based request ID -
Any other properties are passed as fetch options (method, headers, body, etc.)
-
Returns: Promise that resolves/rejects based on the most recent request
Note: This is a convenience method that internally calls request() with the URL as the requestPromise. The request ID is automatically generated from the cleaned URL unless requestKey is specified. When noCancel is true, a unique ID is generated for each request.
Executes an HTTP request using axios, cancelling any previous request with the same identifier.
Parameters:
url(string): The URL to requestoptions(Object, optional): Configuration options-
requestKey(string|number|Function, optional): Key to identify duplicate requests. If provided, requests with the same key will cancel previous ones. Can be a string, number, or function that returns a key. -
noCancel(boolean): If true, this request will not cancel previous requests with the same ID, allowing concurrent requests -
includeQuery(boolean): If true, keeps the query string in the URL-based request ID -
includeMethod(boolean): If true (default), the HTTP method is part of the URL-based request ID -
Any other properties are passed as axios options (method, headers, params, data, etc.)
-
axiosInstance(Object, optional): Custom axios instance to use. If not provided, uses the globalaxiosobject.
Returns: Promise that resolves/rejects based on the most recent request
Note: This method automatically creates an AbortController and passes its signal in the axios config, so cancellation requires axios >= 0.22.0 (the first version supporting AbortSignal). Older versions silently ignore the signal; a console warning is emitted when one is detected. The request ID is automatically generated from the cleaned URL unless requestKey is specified. When noCancel is true, a unique ID is generated for each request.
Example:
import axios from 'axios';
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// Simple GET request (uses global axios)
requestManager
.axios('/api/users')
.then((response) => console.log(response.data))
.catch((error) => console.error(error));
// With custom axios instance
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
});
requestManager.axios('/users', {}, apiClient).then((response) => console.log(response.data));
// POST request with options
requestManager
.axios('/api/users', {
method: 'POST',
data: { name: 'John' },
headers: { 'Content-Type': 'application/json' },
})
.then((response) => console.log(response.data));Helper for ajax-style clients (jQuery.ajax, Ext.Ajax, etc.) that return a request object rather than (or in addition to) a Promise.
Calls ajaxFunction({ url, ...options }), then auto-wires cancel by inspecting the returned object:
req.abortif present (jQuery)- else
Ext.Ajax.abort(req)when Ext is available andreq.xhrexists - else
req.xhr.abort/ rawXMLHttpRequest.abort
Parameters:
ajaxFunction(Function): Receives{ url, ...options }and returns the library request object (or a Promise).url(string): The URL to requestoptions(Object, optional): Configuration options-
requestKey(string|number|Function, optional): Key to identify duplicate requests. If provided, requests with the same key will cancel previous ones. Can be a string, number, or function that returns a key. -
abortController(AbortController): AbortController instance (created automatically if not provided) -
cancelToken(Function|Object): Cancel token or cancel function for other libraries -
noCancel(boolean): If true, this request will not cancel previous requests with the same ID, allowing concurrent requests -
includeQuery(boolean): If true, keeps the query string in the URL-based request ID -
includeMethod(boolean): If true (default), the HTTP method is part of the URL-based request ID -
Any other properties are passed to the ajax method function
-
Returns: Promise that resolves/rejects based on the most recent request
Example:
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// jQuery
requestManager
.ajax($.ajax.bind($), '/api/users', { method: 'GET' })
.then((data) => console.log(data))
.catch((error) => console.error(error));
// Ext.Ajax
requestManager.ajax(({ url, ...options }) => Ext.Ajax.request({ url, ...options }), '/api/users');Executes an HTTP request using XMLHttpRequest, cancelling any previous request with the same identifier.
Parameters:
url(string): The URL to requestoptions(Object, optional): Configuration optionsmethod(string): HTTP method (GET, POST, PUT, DELETE, etc.). Defaults to 'GET'.headers(Object): Headers object to set on the requestbody(string|FormData|Blob|ArrayBuffer): Request bodyresponseType(string): Response type ('text', 'json', 'blob', 'arraybuffer', 'document'). Defaults to 'text'.withCredentials(boolean): Whether to send credentials with the requesttimeout(number): Request timeout in millisecondsrequestKey(string|number|Function, optional): Key to identify duplicate requests. If provided, requests with the same key will cancel previous ones. Can be a string, number, or function that returns a key.abortController(AbortController): AbortController instance (created automatically if not provided)noCancel(boolean): If true, this request will not cancel previous requests with the same ID, allowing concurrent requestsincludeQuery(boolean): If true, keeps the query string in the URL-based request IDincludeMethod(boolean): If true (default), the HTTP method is part of the URL-based request ID
Returns: Promise that resolves with the XMLHttpRequest instance (or rejects on HTTP/network/timeout/abort). Parsing the body is left to the caller (xhr.response, xhr.responseText, etc.).
Note: If you abort the request yourself (via your own AbortController or xhr.abort()), the returned promise rejects with { message: 'Request was cancelled', xhr } and the manager removes the entry from its active requests.
Example:
import RequestManager from '@enegalan/request-manager';
const requestManager = new RequestManager();
// Simple GET request
requestManager
.xhr('/api/users')
.then((xhr) => console.log(xhr.responseText))
.catch((error) => console.error(error));
// POST request with options
requestManager
.xhr('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John' }),
responseType: 'json',
})
.then((xhr) => console.log(xhr.response));Returns the request ID that RequestManager assigns for a URL and options.
Parameters:
url(string): The URL used when starting the requestoptions(Object, optional): Same options used for the requestrequestKey(string|number|Function, optional): Key overrideincludeQuery(boolean, optional): Keep query string in the URL-based IDincludeMethod(boolean, optional): If false, the HTTP method is not part of the URL-based ID (default true)noCancel(boolean, optional): If true, returns a new unique ID (will not match an already in-flightnoCancelrequest)
Returns: string — the request identifier
Example:
requestManager.fetch('/api/users');
const id = requestManager.getRequestId('/api/users');
if (requestManager.isActive(id)) {
requestManager.cancel(id);
}
// With the same options used for the request:
const searchId = requestManager.getRequestId('/api/search?q=test', { requestKey: 'search-users' });
requestManager.cancel(searchId);Cancels a specific request by its identifier.
Parameters:
requestId(string): The unique identifier of the request to cancel
Returns: true if the request was found and cancelled, false otherwise
Cancels all active requests.
Returns: The number of requests that were cancelled
Returns the live Map of in-flight requests, keyed by request identifier. Same instance as requestManager.activeRequests (same pattern as options / getOptions()).
Returns: Map<string, ActiveRequest>
Example:
for (const [id, entry] of requestManager.getActiveRequests()) {
console.log(id, entry.abortController);
}Returns the in-flight request entry for an identifier, or undefined if it is not active.
Parameters:
requestId(string): The unique identifier of the request
Returns: ActiveRequest | undefined
Example:
const id = requestManager.getRequestId('/api/users');
const entry = requestManager.getActiveRequest(id);
if (entry) {
entry.abortController.abort();
}Checks if a request with the given identifier is currently active.
Parameters:
requestId(string): The unique identifier to check
Returns: true if the request is active, false otherwise
Gets the number of active requests.
Returns: The number of currently active requests
Clears all active requests without cancelling them. Use with caution - this will not cancel the underlying HTTP requests.
Creates a new AbortController and returns its signal for the next request() (one getSignal → one request). Do not use for parallel requests; use fetch(), axios(), or request(url, ({ options }) => ...) instead — they create their own signal.
Returns: AbortSignal from a new AbortController
Example:
const signal = requestManager.getSignal();
requestManager.request('/api/users', fetch('/api/users', { signal }));Creates a new AbortController for the next request handoff. Always returns a fresh controller (never reuses one from another in-flight request).
Returns: AbortController instance
Example:
const abortController = requestManager.getAbortController();
requestManager.request('/api/users', fetch('/api/users', { signal: abortController.signal }));Gets the manager options that were passed to the constructor or set via setOptions.
Returns: Object containing the manager options
Sets the manager options.
Parameters:
options(Object): Configuration optionsverbose(boolean, optional): If true, cancellation rejects with a message that includes the request id. If false (default), cancellation is silent.
Example:
const requestManager = new RequestManager();
// Enable verbose cancellation messages at runtime
requestManager.setOptions({ verbose: true });
// Silent cancellation (default) — no rejection / no console noise
requestManager.setOptions({ verbose: false });Links an abort signal with an HTTP client abort method. Useful for custom HTTP clients that only support the abort method to cancel requests.
Parameters:
abortMethod(Function): The abort method to call when the signal is abortedsignal(AbortSignal): The signal to listen to
Example:
const abortController = new AbortController();
const req = $.ajax({ url });
requestManager.addAbortListener(req.abort, abortController.signal);
requestManager.request(url, req, { abortController: abortController });@enegalan/request-manager supports the following browser versions:
| Browser | Supported version |
|---|---|
| Chrome | ≥ 66 |
| Firefox | ≥ 57 |
| Safari | ≥ 12.1 |
| Edge | ≥ 16 |
| Internet Explorer | Not supported |
Note
@enegalan/request-manager relies on modern browser APIs such as
AbortController. Older browsers may work with the appropriate polyfills, but are not officially supported.
Issues and pull requests are welcome at github.com/enegalan/request-manager.
git clone https://github.com/enegalan/request-manager.git
cd request-manager
npm install
npm test # run the test suite
npm run test:watch # run tests on file changes
npm run lint # check code style
npm run build # generate dist/ bundles