@jaseeey/request-manager 2.1.3 → 2.2.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.
package/README.md CHANGED
@@ -1,128 +1,666 @@
1
1
  # RequestManager
2
2
 
3
- RequestManager is a TypeScript-based library designed to manage and regulate HTTP requests efficiently. It ensures that multiple requests to the same URL with the same method are not made concurrently, thus preventing duplicate requests. When a request to a specific URL and method is in progress, subsequent requests to the same URL and method will return the same promise.
3
+ `@jaseeey/request-manager` is a small TypeScript library that **de-duplicates concurrent HTTP requests** made through [Axios](https://axios-http.com/).
4
4
 
5
- ## Background and Scope
5
+ If several parts of your application call the same endpoint at the same time (same Axios client instance, method, and URL), only **one** network request is made. Every caller receives the **same shared promise** and resolves or rejects together.
6
6
 
7
- The RequestManager library was specifically written to address a particular issue encountered in my projects. It was not designed or intended to serve as a comprehensive request management solution. Instead, its primary aim is to tackle the specific challenges I faced, offering a targeted approach to prevent duplicate requests and manage HTTP calls more efficiently.
7
+ It is intentionally focused: not a full HTTP client, cache layer, or request queue. Use it when concurrent duplicate in-flight calls are the problem you need to solve.
8
8
 
9
- Given its focused nature, there are a set of known limitations that have been accepted, as these constraints do not affect the scenarios for which the library was developed. While the library provides a solution to the identified issues, its functionality may not cover all use cases or requirements that might be expected from a full-fledged request manager.
9
+ ---
10
10
 
11
- Users are encouraged to understand these known limitations and consider how they align with their specific needs before integrating the library into their projects.
11
+ ## Table of contents
12
12
 
13
- Should you wish to extend the behaviour, or modify it for your own purposes, then you are free to do so.
13
+ - [When to use it](#when-to-use-it)
14
+ - [Related tools](#related-tools)
15
+ - [Installation](#installation)
16
+ - [Quick start](#quick-start)
17
+ - [How de-duplication works](#how-de-duplication-works)
18
+ - [Choosing a manager instance](#choosing-a-manager-instance)
19
+ - [API reference](#api-reference)
20
+ - [Effective usage patterns](#effective-usage-patterns)
21
+ - [TypeScript tips](#typescript-tips)
22
+ - [CommonJS](#commonjs)
23
+ - [FAQ](#faq)
24
+ - [Known limitations](#known-limitations)
25
+ - [Background and scope](#background-and-scope)
26
+ - [Contributing](#contributing)
27
+ - [License](#license)
14
28
 
15
- ## Features
29
+ ---
16
30
 
17
- - Prevents duplicate simultaneous requests to the same client instance, URL, and method.
18
- - Supports TypeScript for improved type safety and developer experience.
19
- - Allows for optional onSuccess and onError callbacks to handle responses.
20
- - Generates both CommonJS (CJS) and ECMAScript Module (ESM) distributions for broad compatibility.
31
+ ## When to use it
32
+
33
+ **Good fit**
34
+
35
+ - Multiple components mount at once and each load the same resource (`GET /users/me`, `GET /config`, …).
36
+ - A user double-clicks a button and you want a single in-flight `POST`/`PUT` until it finishes.
37
+ - You already use Axios (or an Axios-compatible `request()` surface) and want de-duplication without rewriting your client.
38
+
39
+ **Usually not the right tool**
40
+
41
+ - You need response **caching** after the request has finished (this library only de-duplicates **in-flight** requests).
42
+ - You need keys based on **body or headers** (params *are* part of the key; bodies are not — see [Known limitations](#known-limitations)).
43
+ - You need automatic retries, offline queues, or GraphQL batching.
44
+
45
+ ## Related tools
46
+
47
+ Libraries such as **TanStack Query**, **SWR**, and **Vue Query** solve a broader problem: server-state caching, background revalidation, stale-while-revalidate UX, and often retries.
48
+
49
+ RequestManager is narrower:
50
+
51
+ | Concern | RequestManager | Query libraries (typical) |
52
+ |---------|----------------|---------------------------|
53
+ | Collapse concurrent identical in-flight HTTP calls | Yes | Sometimes, as part of a larger cache model |
54
+ | Keep a cache after the request finishes | No | Yes |
55
+ | Revalidate on focus / interval | No | Yes |
56
+ | Requires Axios | Yes (client with `request`) | Usually `fetch` or pluggable clients |
57
+
58
+ You can use both: a query library for cache and lifecycle, and RequestManager underneath an Axios-based API module when multiple non-query call sites still risk duplicate in-flight requests. Do not expect RequestManager alone to replace a query library.
59
+
60
+ ---
21
61
 
22
62
  ## Installation
23
63
 
24
- To install RequestManager, use the following npm command:
64
+ ```bash
65
+ npm install @jaseeey/request-manager axios
66
+ ```
67
+
68
+ **Axios is a peer dependency** (`axios >= 1`). The package does not bundle a client for you; you pass your own `AxiosInstance` into `call()`. Install Axios alongside this library if it is not already in your project:
25
69
 
26
70
  ```bash
27
- npm install @jaseeey/request-manager
71
+ npm install axios
28
72
  ```
29
73
 
30
- ## Usage
74
+ Requires **Node.js 18+** for development tooling; the published package is plain ESM/CJS JavaScript for bundlers and Node.
31
75
 
32
- Here's a basic example of how to use the RequestManager library:
76
+ ---
33
77
 
34
- ### Recommended: Default Instance
78
+ ## Quick start
35
79
 
36
- ```javascript
80
+ ### ESM (recommended)
81
+
82
+ ```typescript
83
+ import axios from 'axios';
37
84
  import requestManager from '@jaseeey/request-manager';
38
85
 
39
- const client = axios.create();
40
- const url = 'https://example.com';
41
- const method = 'GET';
86
+ const client = axios.create({
87
+ baseURL: 'https://api.example.com',
88
+ timeout: 10_000
89
+ });
90
+
91
+ // Only one HTTP request is sent; both callers share the result.
92
+ const [a, b] = await Promise.all([
93
+ requestManager.call(client, 'GET', '/users/me'),
94
+ requestManager.call(client, 'GET', '/users/me')
95
+ ]);
42
96
 
43
- const response = await requestManager.call(client, method, url);
97
+ console.log(a === b); // true (same resolved value / same shared completion)
44
98
  ```
45
99
 
46
- ### With Callbacks and Transforming Success Result
100
+ ### Return response data only
47
101
 
48
- ```javascript
102
+ ```typescript
103
+ import axios from 'axios';
49
104
  import requestManager from '@jaseeey/request-manager';
50
105
 
51
- const client = axios.create();
52
- const url = 'https://example.com';
106
+ const client = axios.create({ baseURL: 'https://api.example.com' });
107
+
108
+ const user = await requestManager.call(
109
+ client,
110
+ 'GET',
111
+ '/users/me',
112
+ undefined,
113
+ undefined,
114
+ (response) => response.data // non-undefined return becomes the promise result
115
+ );
116
+ ```
117
+
118
+ ### Handle errors without throwing
119
+
120
+ ```typescript
121
+ import axios from 'axios';
122
+ import requestManager from '@jaseeey/request-manager';
123
+
124
+ const client = axios.create({ baseURL: 'https://api.example.com' });
53
125
 
54
126
  const result = await requestManager.call(
55
127
  client,
56
128
  'GET',
57
- url,
58
- {},
59
- {},
60
- res => res.data, // Returning a value changes the resolved result
61
- err => {
62
- // Handle your error here
129
+ '/users/me',
130
+ undefined,
131
+ undefined,
132
+ undefined,
133
+ (error) => {
134
+ console.error('Load failed', error);
135
+ // Promise resolves to undefined instead of rejecting
63
136
  }
64
137
  );
65
138
  ```
66
139
 
67
- ### Creating Isolated Managers
140
+ ---
68
141
 
69
- ```javascript
142
+ ## How de-duplication works
143
+
144
+ ### Request key
145
+
146
+ Each in-flight request is stored under a key built from:
147
+
148
+ 1. **Axios client instance** (identity, not config equality)
149
+ 2. **HTTP method** (compared case-insensitively, e.g. `GET` and `get` match)
150
+ 3. **URL string** (exact string match of the `url` argument—not Axios's fully resolved URL)
151
+ 4. **`config.params`** (deterministic serialisation; missing/`undefined`/empty params are equivalent)
152
+
153
+ ```text
154
+ identity = `${clientId}:${method.toLowerCase()}:${url}:${stableParams}`
155
+ mapKey = sha256Hex(identity) // fixed-length hex; used as Map key only
156
+ ```
157
+
158
+ Object key order in `params` does not matter (`{ a: 1, b: 2 }` matches `{ b: 2, a: 1 }`). Nested plain objects and arrays are included. `URLSearchParams` is supported.
159
+
160
+ The Map key is a **SHA-256 hash** so long URLs/params do not bloat the map. Full details stay on each entry:
161
+
162
+ ```typescript
163
+ for (const [hash, entry] of requestManager.activeRequests) {
164
+ entry.hash; // same as Map key
165
+ entry.identity.method; // e.g. 'get'
166
+ entry.identity.url;
167
+ entry.identity.params; // as supplied (or null)
168
+ entry.identity.paramsSerialised;
169
+ entry.original; // underlying Axios promise
170
+ entry.processed; // shared caller promise
171
+ }
172
+ ```
173
+
174
+ See also [`baseURL` is not part of the key](#baseurl-is-not-part-of-the-key).
175
+
176
+ ### Lifecycle
177
+
178
+ 1. First `call()` for a key creates the Axios request and stores its promise.
179
+ 2. Further `call()`s with the same key **while the HTTP request is still in flight** return the **existing processed promise**.
180
+ 3. When the **HTTP request** settles (success or failure), the key is removed from the active map. This happens before `onSuccess` finishes if that callback is async or slow.
181
+ 4. A later `call()` with the same key—including one made from inside `onSuccess` after the HTTP response arrived—starts a **new** network request.
182
+
183
+ Keeping the key only for the HTTP phase avoids deadlocking when `onSuccess` itself issues another same-key `call()` (joining the still-running processed promise would wait forever).
184
+
185
+ There is **no cache** of completed responses. De-duplication applies only to concurrent in-flight HTTP work, not to success-callback execution time.
186
+
187
+ ### What is and is not part of the key
188
+
189
+ | Input | Same key? | Notes |
190
+ |--------|-----------|--------|
191
+ | Same client, method, URL, and `params` | Yes | Concurrent callers share one Promise |
192
+ | Different `config.params` (different subsets/filters) | No | Separate HTTP requests — correct for different data slices |
193
+ | Missing / `undefined` / `{}` params | Yes (equivalent) | Treated as “no params” |
194
+ | Different `data` bodies | Yes (same key) | Body is **not** in the key (mutations are usually user actions) |
195
+ | Different headers, timeout, signal, etc. | Yes (same key) | Other config is **not** in the key |
196
+ | Same path, different full URL strings | No | `'/users?id=1'` and `'/users?id=2'` are different keys |
197
+ | Different Axios instances | No | Separate clients never share de-duplication |
198
+ | Different methods | No | `GET` and `POST` to the same URL are independent |
199
+
200
+ ### Shared promise and callbacks
201
+
202
+ When a second caller joins an in-flight request:
203
+
204
+ - It receives the **same** processed promise as the first caller.
205
+ - Only the **first** caller’s `onSuccess` / `onError` run for that network attempt.
206
+ - Callbacks passed by later callers are **ignored** for that in-flight request.
207
+
208
+ If you need per-caller side effects, prefer:
209
+
210
+ ```typescript
211
+ const response = await requestManager.call(client, 'GET', url);
212
+ // each caller runs its own logic after await
213
+ updateUi(response.data);
214
+ ```
215
+
216
+ Joiners also share **promise identity**: concurrent callers for the same key receive the same `Promise` instance, not merely the same eventual value.
217
+
218
+ ```typescript
219
+ const p1 = requestManager.call(client, 'GET', '/users/me');
220
+ const p2 = requestManager.call(client, 'GET', '/users/me');
221
+ console.log(p1 === p2); // true while the request is in flight
222
+ ```
223
+
224
+ ### Axios interceptors run once
225
+
226
+ Request and response interceptors on the Axios client run for the **single** underlying `client.request(...)`. Joined callers do not re-enter interceptors.
227
+
228
+ That is usually what you want for auth headers, logging, and token refresh: one network attempt, one interceptor chain, many awaiters.
229
+
230
+ ---
231
+
232
+ ## Choosing a manager instance
233
+
234
+ ### Default shared instance (most apps)
235
+
236
+ ```typescript
237
+ import requestManager from '@jaseeey/request-manager';
238
+ ```
239
+
240
+ Use this when you want app-wide de-duplication for a given Axios client and endpoint. This is the recommended default in browser single-page apps.
241
+
242
+ The default export is a **module-level singleton**: one shared `RequestManager` for the entire JavaScript realm that loaded the module (typically one per browser tab, or one per Node process).
243
+
244
+ ### Isolated managers
245
+
246
+ ```typescript
247
+ import { RequestManager } from '@jaseeey/request-manager';
248
+
249
+ const billingRequests = new RequestManager();
250
+ const adminRequests = new RequestManager();
251
+ ```
252
+
253
+ Each instance has its **own** active-request map. The same client/method/URL can run in parallel across different manager instances.
254
+
255
+ Use isolated managers when:
256
+
257
+ - You need separate de-duplication domains (e.g. multi-tenant tabs, micro-frontends).
258
+ - Tests should not share state with the default singleton (or clear `activeRequests` carefully).
259
+ - You run **SSR or multi-request Node** code and must not share in-flight maps across concurrent HTTP requests or users.
260
+
261
+ #### Server-side rendering and multi-request Node
262
+
263
+ On the server, the default export can be shared across concurrent incoming requests in the same process. That may incorrectly join unrelated users' in-flight calls if they hit the same client/method/URL key.
264
+
265
+ Prefer creating a manager (and often an Axios client) **per request** or per app context:
266
+
267
+ ```typescript
268
+ import axios from 'axios';
70
269
  import { RequestManager } from '@jaseeey/request-manager';
71
270
 
271
+ export function createRequestContext() {
272
+ const api = axios.create({ baseURL: process.env.API_URL });
273
+ const requests = new RequestManager();
274
+ return { api, requests };
275
+ }
276
+
277
+ // inside a single incoming request handler / RSC context
278
+ const { api, requests } = createRequestContext();
279
+ await requests.call(api, 'GET', '/users/me');
280
+ ```
281
+
282
+ In the browser, the default singleton is usually correct because one tab is one user session.
283
+
284
+ ### Legacy static API
285
+
286
+ ```typescript
287
+ import axios from 'axios';
288
+ import { RequestManager } from '@jaseeey/request-manager';
289
+
290
+ const client = axios.create();
291
+ await RequestManager.call(client, 'GET', 'https://example.com/health');
292
+ ```
293
+
294
+ `RequestManager.call(...)` is a **deprecated** compatibility helper. It always delegates to the **default shared instance**, not to `new RequestManager()`. Prefer the default export.
295
+
296
+ ---
297
+
298
+ ## API reference
299
+
300
+ ### Imports
301
+
302
+ | Import | Description |
303
+ |--------|-------------|
304
+ | `import requestManager from '@jaseeey/request-manager'` | Default shared instance |
305
+ | `import { RequestManager } from '@jaseeey/request-manager'` | Class for new instances / static legacy API |
306
+ | `import requestManager, { RequestManager } from '@jaseeey/request-manager'` | Both |
307
+ | `import … from '@jaseeey/request-manager/request-manager'` | Subpath export of the same surface |
308
+
309
+ ### `requestManager.call(client, method, url, data?, config?, onSuccess?, onError?)`
310
+
311
+ | Parameter | Type | Default | Description |
312
+ |-----------|------|---------|-------------|
313
+ | `client` | `AxiosInstance` | required | Axios instance used to perform `client.request(...)` |
314
+ | `method` | `Method` | required | HTTP method (`'GET'`, `'POST'`, …) |
315
+ | `url` | `string` | required | Request URL (relative or absolute, as Axios expects) |
316
+ | `data` | `any` | `{}` | Request body (also used when `null`/`undefined` is passed → coerced to `{}`) |
317
+ | `config` | `AxiosRequestConfig \| null` | `{}` | Extra Axios config merged into the request (`null`/`undefined` → `{}`) |
318
+ | `onSuccess` | `((response) => T \| Promise<T \| undefined>) \| null` | — | Optional success hook; see below |
319
+ | `onError` | `((error) => void) \| null` | — | Optional error hook; see below |
320
+
321
+ **Return value**
322
+
323
+ - With no `onSuccess` (or `onSuccess` returns `undefined`): resolves to the full `AxiosResponse`.
324
+ - If `onSuccess` returns a value other than `undefined`: resolves to that value (may be async).
325
+ - If the request fails and `onError` is provided: `onError` is invoked and the promise **resolves to `undefined`** (does not rethrow).
326
+ - If the request fails and `onError` is omitted: the promise **rejects** with the error.
327
+
328
+ **Notes**
329
+
330
+ - `onSuccess` may return a `Promise`; it is awaited.
331
+ - `onError` is not awaited; keep it synchronous or fire-and-forget async work carefully.
332
+ - Method matching for de-duplication is case-insensitive.
333
+
334
+ ### Instance fields
335
+
336
+ | Member | Description |
337
+ |--------|-------------|
338
+ | `activeRequests` | `Map<hash, ActiveRequest>` of in-flight entries. Keys are SHA-256 digests; values include `identity`, `original`, and `processed`. Prefer not to depend on hash format in production code. |
339
+
340
+ `ActiveRequest` keeps **backward-compatible** promise fields (`original`, `processed`) and adds `hash` + `identity` for inspection.
341
+
342
+ Clearing the map mid-flight is not recommended except in tests. If you clear an in-flight key, a later `call()` with the same client/method/URL/params will start a **new** HTTP request while the original promise may still settle independently.
343
+
344
+ ### Constructor
345
+
346
+ ```typescript
347
+ const manager = new RequestManager();
348
+ ```
349
+
350
+ Creates an isolated manager. The generic type parameter on the class is historical; prefer generics on `call()` for response typing.
351
+
352
+ ---
353
+
354
+ ## Effective usage patterns
355
+
356
+ ### 1. Central Axios client + default manager
357
+
358
+ ```typescript
359
+ // api/client.ts
360
+ import axios from 'axios';
361
+
362
+ export const api = axios.create({
363
+ baseURL: import.meta.env.VITE_API_URL,
364
+ withCredentials: true
365
+ });
366
+
367
+ api.interceptors.request.use((config) => {
368
+ // attach auth headers, etc.
369
+ return config;
370
+ });
371
+ ```
372
+
373
+ ```typescript
374
+ // api/users.ts
375
+ import requestManager from '@jaseeey/request-manager';
376
+ import { api } from './client';
377
+
378
+ export function fetchCurrentUser() {
379
+ return requestManager.call(
380
+ api,
381
+ 'GET',
382
+ '/users/me',
383
+ undefined,
384
+ undefined,
385
+ (res) => res.data
386
+ );
387
+ }
388
+ ```
389
+
390
+ Several UI components can call `fetchCurrentUser()` on mount safely; only one HTTP request runs at a time for that key.
391
+
392
+ ### 2. Distinguish list/filter loads with URL query or `config.params`
393
+
394
+ Either style works; both affect identity:
395
+
396
+ ```typescript
397
+ // Different keys via URL string
398
+ await requestManager.call(api, 'GET', `/items?id=${id}`);
399
+
400
+ // Different keys via config.params (included in the de-dupe key)
401
+ await requestManager.call(api, 'GET', '/items', undefined, { params: { id } });
402
+
403
+ // Same params → same key → one in-flight request for concurrent callers
404
+ await Promise.all([
405
+ requestManager.call(api, 'GET', '/jobs', {}, { params: { includeArchived: false } }),
406
+ requestManager.call(api, 'GET', '/jobs', {}, { params: { includeArchived: false } })
407
+ ]);
408
+ ```
409
+
410
+ Prefer one convention in a given app (query in the URL **or** `params`) so keys stay predictable. Mixing both for the same logical filter can create two keys that look “the same” to humans but differ in the key string.
411
+
412
+ ### 3. Avoid accidental de-duplication of different POST bodies
413
+
414
+ Concurrent `POST`s to the same URL with different bodies currently share one request (first body wins for the network call; all callers share that outcome). If that is wrong for your API:
415
+
416
+ - Use distinct URLs, or
417
+ - Use separate manager instances, or
418
+ - Call Axios directly for those operations.
419
+
420
+ ### 4. Prefer post-await logic over dual callbacks for multi-caller UI
421
+
422
+ ```typescript
423
+ // Preferred when many callers may join one request
424
+ try {
425
+ const res = await requestManager.call(api, 'GET', '/settings');
426
+ applySettings(res.data);
427
+ }
428
+ catch (err) {
429
+ showError(err);
430
+ }
431
+ ```
432
+
433
+ ### 5. Double-submit protection
434
+
435
+ ```typescript
436
+ async function saveProfile(payload: Profile) {
437
+ return requestManager.call(
438
+ api,
439
+ 'PUT',
440
+ '/profile',
441
+ payload,
442
+ undefined,
443
+ (res) => res.data
444
+ );
445
+ }
446
+
447
+ // Rapid repeated calls while the first is in flight share one PUT.
448
+ await Promise.all([saveProfile(data), saveProfile(data)]);
449
+ ```
450
+
451
+ Remember: body is not in the key—only use this when concurrent calls are intentionally identical or the first body is acceptable for all joiners.
452
+
453
+ ### 6. Testing
454
+
455
+ ```typescript
456
+ import requestManager from '@jaseeey/request-manager';
457
+
458
+ afterEach(() => {
459
+ requestManager.activeRequests.clear();
460
+ });
461
+ ```
462
+
463
+ Or construct `new RequestManager()` per test file to avoid shared state.
464
+
465
+ ### 7. Cancellation
466
+
467
+ This library does not cancel Axios requests. To cancel, pass an `AbortSignal` in `config` as you would with Axios. Joined callers still share the same promise; aborting affects the single underlying request.
468
+
469
+ ```typescript
470
+ const controller = new AbortController();
471
+
472
+ const promise = requestManager.call(
473
+ api,
474
+ 'GET',
475
+ '/slow',
476
+ undefined,
477
+ { signal: controller.signal }
478
+ );
479
+
480
+ controller.abort();
481
+ ```
482
+
483
+ ---
484
+
485
+ ## TypeScript tips
486
+
487
+ ```typescript
488
+ import type { AxiosResponse } from 'axios';
489
+ import axios from 'axios';
490
+ import requestManager from '@jaseeey/request-manager';
491
+
492
+ interface User {
493
+ id: string;
494
+ name: string;
495
+ }
496
+
72
497
  const client = axios.create();
73
- const scopedRequestManager = new RequestManager();
74
498
 
75
- const response = await scopedRequestManager.call(client, 'GET', 'https://example.com');
499
+ // Full AxiosResponse
500
+ const response = await requestManager.call<User>(client, 'GET', '/users/me');
501
+ // response is AxiosResponse<User> when onSuccess is omitted
502
+
503
+ // Mapped result type via onSuccess return value
504
+ const user = await requestManager.call<User, User>(
505
+ client,
506
+ 'GET',
507
+ '/users/me',
508
+ undefined,
509
+ undefined,
510
+ (res: AxiosResponse<User>) => res.data
511
+ );
76
512
  ```
77
513
 
78
- ### Legacy Static API (Backward Compatibility)
514
+ ---
515
+
516
+ ## CommonJS
79
517
 
80
518
  ```javascript
81
- import { RequestManager } from '@jaseeey/request-manager';
519
+ const axios = require('axios');
520
+ const requestManager = require('@jaseeey/request-manager').default;
521
+ // or: const { RequestManager } = require('@jaseeey/request-manager');
522
+
523
+ const client = axios.create();
82
524
 
83
- const response = await RequestManager.call(client, 'GET', 'https://example.com');
525
+ requestManager.call(client, 'GET', 'https://example.com').then((res) => {
526
+ console.log(res.status);
527
+ });
84
528
  ```
85
529
 
86
- `RequestManager.call(...)` is supported for backward compatibility, but the default instance API is recommended.
530
+ Package exports:
531
+
532
+ - `require('@jaseeey/request-manager')` → CJS build
533
+ - `import … from '@jaseeey/request-manager'` → ESM build
534
+ - Types resolve from the ESM declaration entry
535
+
536
+ ---
537
+
538
+ ## FAQ
539
+
540
+ ### Does RequestManager cache responses?
541
+
542
+ No. It only de-duplicates **in-flight** requests. After a call settles, the next `call()` with the same key performs a new network request. For caching, revalidation, and background refresh, use a data library (see [Related tools](#related-tools)) or your own cache.
543
+
544
+ ### Why did two different POST bodies share one request?
87
545
 
88
- ## API Reference
546
+ The de-duplication key ignores `data`. Concurrent `POST`s to the same client, method, and URL string join the first request; later bodies are not sent separately. Use distinct URLs, separate manager instances, or call Axios directly when bodies must not merge. See [Avoid accidental de-duplication of different POST bodies](#3-avoid-accidental-de-duplication-of-different-post-bodies).
89
547
 
90
- ### `requestManager.call(client, method, url, data, config, onSuccess, onError)`
548
+ ### Do different `config.params` create separate keys?
549
+
550
+ Yes. `config.params` is part of the de-duplication key (stable serialisation). Different filters/subsets produce different keys and separate HTTP requests; identical params still share one in-flight Promise. See [Request key](#request-key).
551
+
552
+ ### Can I use `fetch` instead of Axios?
553
+
554
+ Not with this package as-is. `call()` expects an Axios-like client with `request(config)`. You could wrap `fetch` behind a minimal `request()` adapter, but that is outside the supported surface.
555
+
556
+ ### What if `onSuccess` returns `undefined`?
557
+
558
+ The promise still resolves to the full `AxiosResponse`. Only a **non-`undefined`** return value from `onSuccess` replaces the result.
559
+
560
+ ### How can I see what is in flight?
561
+
562
+ Inspect `requestManager.activeRequests.size` or iterate values and read `entry.identity` for debugging and tests. Treat hash keys as opaque; use `identity` for human-readable details.
563
+
564
+ ### Should I migrate off `RequestManager.call`?
565
+
566
+ Yes, when convenient. The static helper is deprecated and always uses the default shared instance:
567
+
568
+ ```typescript
569
+ // Before
570
+ await RequestManager.call(client, 'GET', '/users/me');
571
+
572
+ // After
573
+ import requestManager from '@jaseeey/request-manager';
574
+ await requestManager.call(client, 'GET', '/users/me');
575
+ ```
91
576
 
92
- - `client`: The HTTP client instance used for making requests.
93
- - `method`: The HTTP method (e.g., 'GET', 'POST').
94
- - `url`: The URL to which the request is sent.
95
- - `data` (optional): The data to be sent as the request body.
96
- - `config` (optional): The configuration options for the request.
97
- - `onSuccess` (optional): A callback function that is called when the request is successful. Returning a non-`undefined` value from this callback changes the resolved value of the `call()` promise.
98
- - `onError` (optional): A callback function that is called when the request fails.
577
+ ---
99
578
 
100
- `RequestManager.call(...)` is also available as a static compatibility API and delegates to the shared default instance.
579
+ ## Known limitations
101
580
 
102
- ## Known Limitations
581
+ ### In-flight only (no response cache)
103
582
 
104
- ### Unified Callback Execution
583
+ After a request completes, a new `call()` hits the network again. Add your own cache if you need longer-lived memoisation.
105
584
 
106
- The RequestManager executes only the `onSuccess` or `onError` callbacks of the first request in the case of identical requests made concurrently. This design choice optimises network and computational resources but may limit individual response handling flexibility. Though, you could simply bypass the use of the callbacks and handle each response independently.
585
+ ### First caller owns callbacks
107
586
 
108
- ### State and Error Handling
587
+ Only the first in-flight caller’s `onSuccess` / `onError` execute. Joiners share the processed promise only.
109
588
 
110
- Given that only the first set of callbacks are executed, managing state updates or performing granular error handling based on different parts of the application's needs could be challenging. This approach assumes a uniform handling strategy for success and error responses.
589
+ ### Key ignores body and most config
111
590
 
112
- ### Request Differentiation
591
+ De-duplication includes `config.params` but not request bodies, headers, timeout, or abort signals. Concurrent mutations with different bodies on the same URL can still share one request unless you isolate them.
113
592
 
114
- The system identifies duplicate requests based on client instance, URL, and method, but still overlooks differences in headers, query parameters, or POST bodies for otherwise matching requests. Applications requiring differentiation based on those fields may need to extend the key generation logic within the RequestManager.
593
+ ### Exact URL string matching
115
594
 
116
- ### Lifecycle Management
595
+ `/users` and `/users/` are different keys. Relative URLs are not normalised against `baseURL` for keying—the string you pass is the key segment.
117
596
 
118
- The centralised management of requests might complicate the handling of component lifecycles, such as cancelling requests when components unmount, especially in scenarios where multiple components depend on the outcome of a single request.
597
+ ### `baseURL` is not part of the key
119
598
 
120
- ### Singleton Design
121
- The RequestManager is implemented as a singleton, which inherently restricts the creation of multiple instances of the class. This design choice aligns with the library's goal to centrally manage and de-duplicate HTTP requests across the application. While this meets the requirements for which the library was developed, it may pose a limitation for scenarios where multiple, isolated request managers are needed. Users should consider this design aspect when planning to integrate the library into their applications, especially if there's a potential need for managing requests in isolated contexts.
599
+ Axios may resolve a relative `url` against the client's `baseURL` when sending the request, but RequestManager keys only on the **`url` argument string**.
600
+
601
+ ```typescript
602
+ const client = axios.create({ baseURL: 'https://api.example.com' });
603
+
604
+ // These share one in-flight request (same url string: '/users/me')
605
+ await Promise.all([
606
+ requestManager.call(client, 'GET', '/users/me'),
607
+ requestManager.call(client, 'GET', '/users/me')
608
+ ]);
609
+
610
+ // This is a different key (different url string), even if it hits the same origin
611
+ await requestManager.call(client, 'GET', 'https://api.example.com/users/me');
612
+ ```
613
+
614
+ Keep the `url` argument consistent across call sites (usually the same relative path) so de-duplication works as intended.
615
+
616
+ ### Client identity, not configuration equality
617
+
618
+ Two `axios.create({ baseURL: 'https://api.example.com' })` instances are different clients and will not de-duplicate against each other.
619
+
620
+ ### Lifecycle / unmount
621
+
622
+ If multiple components share one in-flight request, unmounting one should not cancel for all unless you coordinate abort signals carefully.
623
+
624
+ ### `onError` is not awaited
625
+
626
+ Async work inside `onError` is not tracked by the returned promise.
627
+
628
+ ### Default export is shared process-wide
629
+
630
+ The default `requestManager` is a single module-level instance for the whole realm. Use `new RequestManager()` when you need isolation—especially under SSR or multi-tenant Node servers (see [Server-side rendering and multi-request Node](#server-side-rendering-and-multi-request-node)).
631
+
632
+ ---
633
+
634
+ ## Background and scope
635
+
636
+ This library was written to solve a concrete problem: concurrent duplicate HTTP calls in application UIs. It is not intended as a general-purpose request framework.
637
+
638
+ Accepted trade-offs (key design, first-callback wins, in-flight-only) match that goal. If you need different behaviour, fork or wrap the class—`RequestManager` is small and MIT-licensed.
639
+
640
+ ---
641
+
642
+ ## Features summary
643
+
644
+ - De-duplicates concurrent requests per **Axios instance + method + URL + `config.params`**
645
+ - Shared promise for all joiners until the request settles
646
+ - Optional `onSuccess` / `onError` hooks (with documented join semantics)
647
+ - Default shared instance **and** constructible isolated managers
648
+ - TypeScript types, ESM + CJS builds
649
+ - Legacy `RequestManager.call` static helper (deprecated)
650
+
651
+ ---
122
652
 
123
653
  ## Contributing
124
654
 
125
- Contributions to the RequestManager library are welcome. If you have suggestions or improvements, feel free to fork the repository and submit a pull request.
655
+ Contributions are welcome. Fork the repository, open a pull request, and keep changes focused. Run tests with:
656
+
657
+ ```bash
658
+ npm ci
659
+ npm test
660
+ npm run build
661
+ ```
662
+
663
+ ---
126
664
 
127
665
  ## License
128
666