@jaseeey/request-manager 2.0.4-alpha.1 → 2.1.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
@@ -74,7 +74,7 @@ catch (err) {
74
74
  - `url`: The URL to which the request is sent.
75
75
  - `data` (optional): The data to be sent as the request body.
76
76
  - `config` (optional): The configuration options for the request.
77
- - `onSuccess` (optional): A callback function that is called when the request is successful.
77
+ - `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
78
  - `onError` (optional): A callback function that is called when the request fails.
79
79
 
80
80
  ## Known Limitations
@@ -89,7 +89,7 @@ Given that only the first set of callbacks are executed, managing state updates
89
89
 
90
90
  ### Request Differentiation
91
91
 
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.
92
+ 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
93
 
94
94
  ### Lifecycle Management
95
95
 
@@ -0,0 +1 @@
1
+ {"type": "commonjs"}
@@ -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;
@@ -0,0 +1 @@
1
+ {"type": "module"}
@@ -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.1",
3
+ "version": "2.1.0",
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,32 +16,33 @@
16
16
  "dist"
17
17
  ],
18
18
  "scripts": {
19
- "build:esm": "tsc -p tsconfig.esm.json",
20
- "build:cjs": "tsc -p tsconfig.cjs.json",
19
+ "prebuild": "rm -rf dist",
20
+ "build:esm": "tsc -p tsconfig.esm.json && echo '{\"type\": \"module\"}' > dist/esm/package.json",
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",
38
37
  "types": "./dist/esm/index.d.ts",
39
38
  "exports": {
40
39
  ".": {
40
+ "types": "./dist/esm/index.d.ts",
41
41
  "require": "./dist/cjs/index.js",
42
42
  "import": "./dist/esm/index.js"
43
43
  },
44
44
  "./request-manager": {
45
+ "types": "./dist/esm/request-manager/index.d.ts",
45
46
  "require": "./dist/cjs/request-manager/index.js",
46
47
  "import": "./dist/esm/request-manager/index.js"
47
48
  }