@jaseeey/request-manager 2.0.4-alpha.2 → 2.1.1

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
@@ -14,7 +14,7 @@ Should you wish to extend the behaviour, or modify it for your own purposes, the
14
14
 
15
15
  ## Features
16
16
 
17
- - Prevents duplicate simultaneous requests to the same URL and method.
17
+ - Prevents duplicate simultaneous requests to the same client instance, URL, and method.
18
18
  - Supports TypeScript for improved type safety and developer experience.
19
19
  - Allows for optional onSuccess and onError callbacks to handle responses.
20
20
  - Generates both CommonJS (CJS) and ECMAScript Module (ESM) distributions for broad compatibility.
@@ -31,52 +31,74 @@ npm install @jaseeey/request-manager
31
31
 
32
32
  Here's a basic example of how to use the RequestManager library:
33
33
 
34
- ### With Callbacks
34
+ ### Recommended: Default Instance
35
35
 
36
36
  ```javascript
37
- import { RequestManager } from '@jaseeey/request-manager';
37
+ import requestManager from '@jaseeey/request-manager';
38
38
 
39
39
  const client = axios.create();
40
40
  const url = 'https://example.com';
41
41
  const method = 'GET';
42
42
 
43
- RequestManager.call(client, method, url, null, null, res => {
44
- // Handle your response here, any processing will only be run once.
45
- }, err => {
46
- // Handle your error here
47
- });
43
+ const response = await requestManager.call(client, method, url);
48
44
  ```
49
45
 
50
- ### Without Callbacks
46
+ ### With Callbacks and Transforming Success Result
51
47
 
52
48
  ```javascript
53
- import { RequestManager } from '@jaseeey/request-manager';
49
+ import requestManager from '@jaseeey/request-manager';
54
50
 
55
51
  const client = axios.create();
56
52
  const url = 'https://example.com';
57
- const method = 'GET';
58
53
 
59
- try {
60
- const response = await RequestManager.call(client, method, url);
61
- // Handle your response here, any processing will be run multiple times upon multiple requests.
62
- }
63
- catch (err) {
64
- // Handle your error here
65
- }
54
+ const result = await requestManager.call(
55
+ client,
56
+ 'GET',
57
+ url,
58
+ {},
59
+ {},
60
+ res => res.data, // Returning a value changes the resolved result
61
+ err => {
62
+ // Handle your error here
63
+ }
64
+ );
66
65
  ```
67
66
 
67
+ ### Creating Isolated Managers
68
+
69
+ ```javascript
70
+ import { RequestManager } from '@jaseeey/request-manager';
71
+
72
+ const client = axios.create();
73
+ const scopedRequestManager = new RequestManager();
74
+
75
+ const response = await scopedRequestManager.call(client, 'GET', 'https://example.com');
76
+ ```
77
+
78
+ ### Legacy Static API (Backward Compatibility)
79
+
80
+ ```javascript
81
+ import { RequestManager } from '@jaseeey/request-manager';
82
+
83
+ const response = await RequestManager.call(client, 'GET', 'https://example.com');
84
+ ```
85
+
86
+ `RequestManager.call(...)` is supported for backward compatibility, but the default instance API is recommended.
87
+
68
88
  ## API Reference
69
89
 
70
- ### `call(client, method, url, data, config, onSuccess, onError)`
90
+ ### `requestManager.call(client, method, url, data, config, onSuccess, onError)`
71
91
 
72
92
  - `client`: The HTTP client instance used for making requests.
73
93
  - `method`: The HTTP method (e.g., 'GET', 'POST').
74
94
  - `url`: The URL to which the request is sent.
75
95
  - `data` (optional): The data to be sent as the request body.
76
96
  - `config` (optional): The configuration options for the request.
77
- - `onSuccess` (optional): A callback function that is called when the request is successful.
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.
78
98
  - `onError` (optional): A callback function that is called when the request fails.
79
99
 
100
+ `RequestManager.call(...)` is also available as a static compatibility API and delegates to the shared default instance.
101
+
80
102
  ## Known Limitations
81
103
 
82
104
  ### Unified Callback Execution
@@ -89,7 +111,7 @@ Given that only the first set of callbacks are executed, managing state updates
89
111
 
90
112
  ### Request Differentiation
91
113
 
92
- The system identifies duplicate requests based solely on URL and method, potentially overlooking requests that differ in headers, query parameters, or POST bodies. Applications requiring differentiation based on these factors may need to extend the key generation logic within the RequestManager.
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.
93
115
 
94
116
  ### Lifecycle Management
95
117
 
@@ -1,8 +1,9 @@
1
1
  import { AxiosInstance, AxiosRequestConfig, AxiosResponse, Method } from 'axios';
2
2
  type RequestKey = string;
3
- interface RequestPromise<T = any> {
4
- original: Promise<AxiosResponse<T, any>>;
5
- processed: Promise<AxiosResponse<T, any> | void>;
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ interface RequestPromise {
5
+ original: Promise<AxiosResponse<unknown>>;
6
+ processed: Promise<unknown>;
6
7
  }
7
8
  /**
8
9
  * RequestManager
@@ -11,20 +12,39 @@ interface RequestPromise<T = any> {
11
12
  * concurrently. When a request to a specific URL and method is in progress, subsequent requests to the same URL and
12
13
  * method will return the same promise, preventing duplicate requests.
13
14
  */
14
- declare class RequestManager<T = any> {
15
+ export declare class RequestManager<T = any> {
15
16
  /**
16
- * A Map to store active requests, with each key being a combination of URL and HTTP method and its corresponding
17
- * value being the Promise of the ongoing HTTP request.
17
+ * A Map to store active requests, with each key being a combination of client instance, URL, and HTTP method and
18
+ * its corresponding value being the Promise of the ongoing HTTP request.
18
19
  */
19
- activeRequests: Map<RequestKey, RequestPromise<T>>;
20
+ activeRequests: Map<RequestKey, RequestPromise>;
21
+ private clientIds;
22
+ private nextClientId;
20
23
  /**
21
- * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration. It
22
- * de-duplicates requests to the same URL with the same method by returning a previously stored promise if available.
23
- * Upon completion of the request, it optionally invokes an onSuccess callback if the request is successful, or an
24
- * onFailure callback if the request fails.
24
+ * Backward-compatible static API that delegates to the shared singleton instance.
25
+ *
26
+ * @deprecated Prefer the default exported instance (`requestManager.call(...)`).
25
27
  */
26
- call(client: AxiosInstance, method: Method, url: string, data?: any, config?: AxiosRequestConfig | null, onSuccess?: ((result: AxiosResponse) => void) | null, onError?: ((error: any) => void) | null): Promise<AxiosResponse | void>;
28
+ static call<TResponse = any, TSuccess = never>(client: AxiosInstance, method: Method, url: string, data?: any, config?: AxiosRequestConfig | null, onSuccess?: ((result: AxiosResponse<TResponse>) => MaybePromise<TSuccess | undefined>) | null, onError?: ((error: unknown) => void) | null): Promise<AxiosResponse<TResponse> | TSuccess | void>;
29
+ /**
30
+ * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration.
31
+ *
32
+ * Requests are de-duplicated only when they target the same client instance, method, and URL.
33
+ * If `onSuccess` returns a non-`undefined` value, that value becomes the resolved result.
34
+ *
35
+ * @param client Axios client used to execute the request.
36
+ * @param method HTTP method.
37
+ * @param url Target request URL.
38
+ * @param data Optional request payload.
39
+ * @param config Optional Axios request configuration.
40
+ * @param onSuccess Optional callback invoked on success. Returning a value overrides the resolved response.
41
+ * @param onError Optional callback invoked on error. If provided, the promise resolves to `void` after handling.
42
+ */
43
+ call<TResponse = T, TSuccess = never>(client: AxiosInstance, method: Method, url: string, data?: any, config?: AxiosRequestConfig | null, onSuccess?: ((result: AxiosResponse<TResponse>) => MaybePromise<TSuccess | undefined>) | null, onError?: ((error: unknown) => void) | null): Promise<AxiosResponse<TResponse> | TSuccess | void>;
44
+ /**
45
+ * Creates a stable in-memory key for de-duplication within this manager instance.
46
+ */
47
+ private buildRequestKey;
27
48
  }
28
49
  declare const requestManager: RequestManager<any>;
29
- export { RequestManager };
30
50
  export default requestManager;
@@ -10,20 +10,38 @@ exports.RequestManager = void 0;
10
10
  */
11
11
  class RequestManager {
12
12
  /**
13
- * A Map to store active requests, with each key being a combination of URL and HTTP method and its corresponding
14
- * value being the Promise of the ongoing HTTP request.
13
+ * A Map to store active requests, with each key being a combination of client instance, URL, and HTTP method and
14
+ * its corresponding value being the Promise of the ongoing HTTP request.
15
15
  */
16
16
  activeRequests = new Map();
17
+ clientIds = new WeakMap();
18
+ nextClientId = 1;
17
19
  /**
18
- * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration. It
19
- * de-duplicates requests to the same URL with the same method by returning a previously stored promise if available.
20
- * Upon completion of the request, it optionally invokes an onSuccess callback if the request is successful, or an
21
- * onFailure callback if the request fails.
20
+ * Backward-compatible static API that delegates to the shared singleton instance.
21
+ *
22
+ * @deprecated Prefer the default exported instance (`requestManager.call(...)`).
23
+ */
24
+ static call(client, method, url, data = {}, config = {}, onSuccess, onError) {
25
+ return requestManager.call(client, method, url, data, config, onSuccess, onError);
26
+ }
27
+ /**
28
+ * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration.
29
+ *
30
+ * Requests are de-duplicated only when they target the same client instance, method, and URL.
31
+ * If `onSuccess` returns a non-`undefined` value, that value becomes the resolved result.
32
+ *
33
+ * @param client Axios client used to execute the request.
34
+ * @param method HTTP method.
35
+ * @param url Target request URL.
36
+ * @param data Optional request payload.
37
+ * @param config Optional Axios request configuration.
38
+ * @param onSuccess Optional callback invoked on success. Returning a value overrides the resolved response.
39
+ * @param onError Optional callback invoked on error. If provided, the promise resolves to `void` after handling.
22
40
  */
23
41
  async call(client, method, url, data = {}, config = {}, onSuccess, onError) {
24
42
  data ??= {};
25
43
  config ??= {};
26
- const key = `${method.toLowerCase()}:${url}`;
44
+ const key = this.buildRequestKey(client, method, url);
27
45
  if (this.activeRequests.has(key)) {
28
46
  return this.activeRequests.get(key).processed;
29
47
  }
@@ -31,7 +49,7 @@ class RequestManager {
31
49
  try {
32
50
  const requestResult = await responsePromise;
33
51
  const callbackResult = onSuccess && typeof onSuccess === 'function'
34
- ? onSuccess(requestResult)
52
+ ? await onSuccess(requestResult)
35
53
  : undefined;
36
54
  return callbackResult !== undefined
37
55
  ? callbackResult
@@ -52,8 +70,18 @@ class RequestManager {
52
70
  this.activeRequests.set(key, { original: requestPromise, processed: processedPromise });
53
71
  return processedPromise;
54
72
  }
73
+ /**
74
+ * Creates a stable in-memory key for de-duplication within this manager instance.
75
+ */
76
+ buildRequestKey(client, method, url) {
77
+ let clientId = this.clientIds.get(client);
78
+ if (!clientId) {
79
+ clientId = this.nextClientId++;
80
+ this.clientIds.set(client, clientId);
81
+ }
82
+ return `${clientId}:${method.toLowerCase()}:${url}`;
83
+ }
55
84
  }
56
85
  exports.RequestManager = RequestManager;
57
- // Create default instance
58
86
  const requestManager = new RequestManager();
59
87
  exports.default = requestManager;
@@ -1,8 +1,9 @@
1
1
  import { AxiosInstance, AxiosRequestConfig, AxiosResponse, Method } from 'axios';
2
2
  type RequestKey = string;
3
- interface RequestPromise<T = any> {
4
- original: Promise<AxiosResponse<T, any>>;
5
- processed: Promise<AxiosResponse<T, any> | void>;
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ interface RequestPromise {
5
+ original: Promise<AxiosResponse<unknown>>;
6
+ processed: Promise<unknown>;
6
7
  }
7
8
  /**
8
9
  * RequestManager
@@ -11,20 +12,39 @@ interface RequestPromise<T = any> {
11
12
  * concurrently. When a request to a specific URL and method is in progress, subsequent requests to the same URL and
12
13
  * method will return the same promise, preventing duplicate requests.
13
14
  */
14
- declare class RequestManager<T = any> {
15
+ export declare class RequestManager<T = any> {
15
16
  /**
16
- * A Map to store active requests, with each key being a combination of URL and HTTP method and its corresponding
17
- * value being the Promise of the ongoing HTTP request.
17
+ * A Map to store active requests, with each key being a combination of client instance, URL, and HTTP method and
18
+ * its corresponding value being the Promise of the ongoing HTTP request.
18
19
  */
19
- activeRequests: Map<RequestKey, RequestPromise<T>>;
20
+ activeRequests: Map<RequestKey, RequestPromise>;
21
+ private clientIds;
22
+ private nextClientId;
20
23
  /**
21
- * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration. It
22
- * de-duplicates requests to the same URL with the same method by returning a previously stored promise if available.
23
- * Upon completion of the request, it optionally invokes an onSuccess callback if the request is successful, or an
24
- * onFailure callback if the request fails.
24
+ * Backward-compatible static API that delegates to the shared singleton instance.
25
+ *
26
+ * @deprecated Prefer the default exported instance (`requestManager.call(...)`).
25
27
  */
26
- call(client: AxiosInstance, method: Method, url: string, data?: any, config?: AxiosRequestConfig | null, onSuccess?: ((result: AxiosResponse) => void) | null, onError?: ((error: any) => void) | null): Promise<AxiosResponse | void>;
28
+ static call<TResponse = any, TSuccess = never>(client: AxiosInstance, method: Method, url: string, data?: any, config?: AxiosRequestConfig | null, onSuccess?: ((result: AxiosResponse<TResponse>) => MaybePromise<TSuccess | undefined>) | null, onError?: ((error: unknown) => void) | null): Promise<AxiosResponse<TResponse> | TSuccess | void>;
29
+ /**
30
+ * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration.
31
+ *
32
+ * Requests are de-duplicated only when they target the same client instance, method, and URL.
33
+ * If `onSuccess` returns a non-`undefined` value, that value becomes the resolved result.
34
+ *
35
+ * @param client Axios client used to execute the request.
36
+ * @param method HTTP method.
37
+ * @param url Target request URL.
38
+ * @param data Optional request payload.
39
+ * @param config Optional Axios request configuration.
40
+ * @param onSuccess Optional callback invoked on success. Returning a value overrides the resolved response.
41
+ * @param onError Optional callback invoked on error. If provided, the promise resolves to `void` after handling.
42
+ */
43
+ call<TResponse = T, TSuccess = never>(client: AxiosInstance, method: Method, url: string, data?: any, config?: AxiosRequestConfig | null, onSuccess?: ((result: AxiosResponse<TResponse>) => MaybePromise<TSuccess | undefined>) | null, onError?: ((error: unknown) => void) | null): Promise<AxiosResponse<TResponse> | TSuccess | void>;
44
+ /**
45
+ * Creates a stable in-memory key for de-duplication within this manager instance.
46
+ */
47
+ private buildRequestKey;
27
48
  }
28
49
  declare const requestManager: RequestManager<any>;
29
- export { RequestManager };
30
50
  export default requestManager;
@@ -5,22 +5,40 @@
5
5
  * concurrently. When a request to a specific URL and method is in progress, subsequent requests to the same URL and
6
6
  * method will return the same promise, preventing duplicate requests.
7
7
  */
8
- class RequestManager {
8
+ export class RequestManager {
9
9
  /**
10
- * A Map to store active requests, with each key being a combination of URL and HTTP method and its corresponding
11
- * value being the Promise of the ongoing HTTP request.
10
+ * A Map to store active requests, with each key being a combination of client instance, URL, and HTTP method and
11
+ * its corresponding value being the Promise of the ongoing HTTP request.
12
12
  */
13
13
  activeRequests = new Map();
14
+ clientIds = new WeakMap();
15
+ nextClientId = 1;
14
16
  /**
15
- * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration. It
16
- * de-duplicates requests to the same URL with the same method by returning a previously stored promise if available.
17
- * Upon completion of the request, it optionally invokes an onSuccess callback if the request is successful, or an
18
- * onFailure callback if the request fails.
17
+ * Backward-compatible static API that delegates to the shared singleton instance.
18
+ *
19
+ * @deprecated Prefer the default exported instance (`requestManager.call(...)`).
20
+ */
21
+ static call(client, method, url, data = {}, config = {}, onSuccess, onError) {
22
+ return requestManager.call(client, method, url, data, config, onSuccess, onError);
23
+ }
24
+ /**
25
+ * Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration.
26
+ *
27
+ * Requests are de-duplicated only when they target the same client instance, method, and URL.
28
+ * If `onSuccess` returns a non-`undefined` value, that value becomes the resolved result.
29
+ *
30
+ * @param client Axios client used to execute the request.
31
+ * @param method HTTP method.
32
+ * @param url Target request URL.
33
+ * @param data Optional request payload.
34
+ * @param config Optional Axios request configuration.
35
+ * @param onSuccess Optional callback invoked on success. Returning a value overrides the resolved response.
36
+ * @param onError Optional callback invoked on error. If provided, the promise resolves to `void` after handling.
19
37
  */
20
38
  async call(client, method, url, data = {}, config = {}, onSuccess, onError) {
21
39
  data ??= {};
22
40
  config ??= {};
23
- const key = `${method.toLowerCase()}:${url}`;
41
+ const key = this.buildRequestKey(client, method, url);
24
42
  if (this.activeRequests.has(key)) {
25
43
  return this.activeRequests.get(key).processed;
26
44
  }
@@ -28,7 +46,7 @@ class RequestManager {
28
46
  try {
29
47
  const requestResult = await responsePromise;
30
48
  const callbackResult = onSuccess && typeof onSuccess === 'function'
31
- ? onSuccess(requestResult)
49
+ ? await onSuccess(requestResult)
32
50
  : undefined;
33
51
  return callbackResult !== undefined
34
52
  ? callbackResult
@@ -49,9 +67,17 @@ class RequestManager {
49
67
  this.activeRequests.set(key, { original: requestPromise, processed: processedPromise });
50
68
  return processedPromise;
51
69
  }
70
+ /**
71
+ * Creates a stable in-memory key for de-duplication within this manager instance.
72
+ */
73
+ buildRequestKey(client, method, url) {
74
+ let clientId = this.clientIds.get(client);
75
+ if (!clientId) {
76
+ clientId = this.nextClientId++;
77
+ this.clientIds.set(client, clientId);
78
+ }
79
+ return `${clientId}:${method.toLowerCase()}:${url}`;
80
+ }
52
81
  }
53
- // Create default instance
54
82
  const requestManager = new RequestManager();
55
- // Export both the class and the default instance
56
- export { RequestManager };
57
83
  export default requestManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaseeey/request-manager",
3
- "version": "2.0.4-alpha.2",
3
+ "version": "2.1.1",
4
4
  "description": "A library to prevent multiple API requests when called simultaneously",
5
5
  "license": "MIT",
6
6
  "author": "Jason Ilicic <me@jasey.io>",
@@ -16,22 +16,21 @@
16
16
  "dist"
17
17
  ],
18
18
  "scripts": {
19
+ "prebuild": "rm -rf dist",
19
20
  "build:esm": "tsc -p tsconfig.esm.json && echo '{\"type\": \"module\"}' > dist/esm/package.json",
20
21
  "build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
21
22
  "build": "npm run build:esm && npm run build:cjs",
22
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
23
+ "prepack": "npm run build",
24
+ "test": "vitest run --coverage"
23
25
  },
24
26
  "dependencies": {
25
- "axios": "^1.11.0"
27
+ "axios": "^1.13.6"
26
28
  },
27
29
  "devDependencies": {
28
- "@jest/globals": "^29.7.0",
29
- "@types/axios": "^0.14.0",
30
- "@types/jest": "^29.5.14",
30
+ "@vitest/coverage-v8": "^4.0.18",
31
31
  "braces": "^3.0.3",
32
- "jest": "^29.7.0",
33
- "ts-jest": "^29.2.5",
34
- "typescript": "^5.7.2"
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^4.0.18"
35
34
  },
36
35
  "main": "./dist/cjs/index.js",
37
36
  "module": "./dist/esm/index.js",