@jaseeey/request-manager 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -9
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/request-manager/index.d.ts +2 -0
- package/dist/cjs/request-manager/index.js +8 -0
- package/dist/cjs/request-manager/request-manager.d.ts +29 -0
- package/dist/cjs/request-manager/request-manager.js +55 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/request-manager/index.d.ts +2 -0
- package/dist/esm/request-manager/index.js +2 -0
- package/dist/esm/request-manager/request-manager.d.ts +29 -0
- package/{request-manager.js → dist/esm/request-manager/request-manager.js} +1 -15
- package/package.json +42 -22
- package/index.js +0 -3
- package/jest.config.mjs +0 -9
- package/test/request-manager.spec.js +0 -97
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# RequestManager
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
RequestManager is a JavaScript 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
|
+
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.
|
|
6
4
|
|
|
7
5
|
## Background and Scope
|
|
8
6
|
|
|
@@ -17,8 +15,17 @@ Should you wish to extend the behaviour, or modify it for your own purposes, the
|
|
|
17
15
|
## Features
|
|
18
16
|
|
|
19
17
|
- Prevents duplicate simultaneous requests to the same URL and method.
|
|
20
|
-
- Supports
|
|
21
|
-
-
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
To install RequestManager, use the following npm command:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @jaseeey/request-manager
|
|
28
|
+
```
|
|
22
29
|
|
|
23
30
|
## Usage
|
|
24
31
|
|
|
@@ -27,15 +34,15 @@ Here's a basic example of how to use the RequestManager library:
|
|
|
27
34
|
### With Callbacks
|
|
28
35
|
|
|
29
36
|
```javascript
|
|
30
|
-
import RequestManager from '@jaseeey/request-manager';
|
|
37
|
+
import { RequestManager } from '@jaseeey/request-manager';
|
|
31
38
|
|
|
32
39
|
const client = axios.create();
|
|
33
40
|
const url = 'https://example.com';
|
|
34
41
|
const method = 'GET';
|
|
35
42
|
|
|
36
|
-
RequestManager.call(client, method, url
|
|
43
|
+
RequestManager.call(client, method, url, null, null, res => {
|
|
37
44
|
// Handle your response here, any processing will only be run once.
|
|
38
|
-
}
|
|
45
|
+
}, err => {
|
|
39
46
|
// Handle your error here
|
|
40
47
|
});
|
|
41
48
|
```
|
|
@@ -43,7 +50,7 @@ RequestManager.call(client, method, url).then(res => {
|
|
|
43
50
|
### Without Callbacks
|
|
44
51
|
|
|
45
52
|
```javascript
|
|
46
|
-
import RequestManager from '@jaseeey/request-manager';
|
|
53
|
+
import { RequestManager } from '@jaseeey/request-manager';
|
|
47
54
|
|
|
48
55
|
const client = axios.create();
|
|
49
56
|
const url = 'https://example.com';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RequestManager = void 0;
|
|
7
|
+
const request_manager_1 = __importDefault(require("./request-manager/request-manager"));
|
|
8
|
+
exports.RequestManager = request_manager_1.default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RequestManager = void 0;
|
|
7
|
+
const request_manager_1 = __importDefault(require("./request-manager"));
|
|
8
|
+
exports.RequestManager = request_manager_1.default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, Method } from 'axios';
|
|
2
|
+
type RequestKey = string;
|
|
3
|
+
interface RequestPromise<T = any> {
|
|
4
|
+
original: Promise<AxiosResponse<T, any>>;
|
|
5
|
+
processed: Promise<AxiosResponse<T, any> | void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* RequestManager
|
|
9
|
+
*
|
|
10
|
+
* Manages and regulates HTTP requests to ensure that multiple requests to the same URL and method are not made
|
|
11
|
+
* concurrently. When a request to a specific URL and method is in progress, subsequent requests to the same URL and
|
|
12
|
+
* method will return the same promise, preventing duplicate requests.
|
|
13
|
+
*/
|
|
14
|
+
declare class RequestManager<T = any> {
|
|
15
|
+
/**
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
activeRequests: Map<RequestKey, RequestPromise<T>>;
|
|
20
|
+
/**
|
|
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.
|
|
25
|
+
*/
|
|
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>;
|
|
27
|
+
}
|
|
28
|
+
declare const _default: RequestManager<any>;
|
|
29
|
+
export default _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* RequestManager
|
|
5
|
+
*
|
|
6
|
+
* Manages and regulates HTTP requests to ensure that multiple requests to the same URL and method are not made
|
|
7
|
+
* concurrently. When a request to a specific URL and method is in progress, subsequent requests to the same URL and
|
|
8
|
+
* method will return the same promise, preventing duplicate requests.
|
|
9
|
+
*/
|
|
10
|
+
class RequestManager {
|
|
11
|
+
/**
|
|
12
|
+
* A Map to store active requests, with each key being a combination of URL and HTTP method and its corresponding
|
|
13
|
+
* value being the Promise of the ongoing HTTP request.
|
|
14
|
+
*/
|
|
15
|
+
activeRequests = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration. It
|
|
18
|
+
* de-duplicates requests to the same URL with the same method by returning a previously stored promise if available.
|
|
19
|
+
* Upon completion of the request, it optionally invokes an onSuccess callback if the request is successful, or an
|
|
20
|
+
* onFailure callback if the request fails.
|
|
21
|
+
*/
|
|
22
|
+
async call(client, method, url, data = {}, config = {}, onSuccess, onError) {
|
|
23
|
+
data ??= {};
|
|
24
|
+
config ??= {};
|
|
25
|
+
const key = `${method.toLowerCase()}:${url}`;
|
|
26
|
+
if (this.activeRequests.has(key)) {
|
|
27
|
+
return this.activeRequests.get(key).processed;
|
|
28
|
+
}
|
|
29
|
+
const processRequest = async (responsePromise) => {
|
|
30
|
+
try {
|
|
31
|
+
const requestResult = await responsePromise;
|
|
32
|
+
const callbackResult = onSuccess && typeof onSuccess === 'function'
|
|
33
|
+
? onSuccess(requestResult)
|
|
34
|
+
: undefined;
|
|
35
|
+
return callbackResult !== undefined
|
|
36
|
+
? callbackResult
|
|
37
|
+
: requestResult;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (onError && typeof onError === 'function') {
|
|
41
|
+
onError(err);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const requestPromise = client.request({ ...config, method, url, data }).finally(() => {
|
|
48
|
+
this.activeRequests.delete(key);
|
|
49
|
+
});
|
|
50
|
+
const processedPromise = processRequest(requestPromise);
|
|
51
|
+
this.activeRequests.set(key, { original: requestPromise, processed: processedPromise });
|
|
52
|
+
return processedPromise;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.default = new RequestManager();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, Method } from 'axios';
|
|
2
|
+
type RequestKey = string;
|
|
3
|
+
interface RequestPromise<T = any> {
|
|
4
|
+
original: Promise<AxiosResponse<T, any>>;
|
|
5
|
+
processed: Promise<AxiosResponse<T, any> | void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* RequestManager
|
|
9
|
+
*
|
|
10
|
+
* Manages and regulates HTTP requests to ensure that multiple requests to the same URL and method are not made
|
|
11
|
+
* concurrently. When a request to a specific URL and method is in progress, subsequent requests to the same URL and
|
|
12
|
+
* method will return the same promise, preventing duplicate requests.
|
|
13
|
+
*/
|
|
14
|
+
declare class RequestManager<T = any> {
|
|
15
|
+
/**
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
activeRequests: Map<RequestKey, RequestPromise<T>>;
|
|
20
|
+
/**
|
|
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.
|
|
25
|
+
*/
|
|
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>;
|
|
27
|
+
}
|
|
28
|
+
declare const _default: RequestManager<any>;
|
|
29
|
+
export default _default;
|
|
@@ -6,31 +6,18 @@
|
|
|
6
6
|
* method will return the same promise, preventing duplicate requests.
|
|
7
7
|
*/
|
|
8
8
|
class RequestManager {
|
|
9
|
-
|
|
10
9
|
/**
|
|
11
10
|
* A Map to store active requests, with each key being a combination of URL and HTTP method and its corresponding
|
|
12
11
|
* value being the Promise of the ongoing HTTP request.
|
|
13
|
-
*
|
|
14
|
-
* @type {Map<string, Promise>}
|
|
15
12
|
*/
|
|
16
13
|
activeRequests = new Map();
|
|
17
|
-
|
|
18
14
|
/**
|
|
19
15
|
* Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration. It
|
|
20
16
|
* de-duplicates requests to the same URL with the same method by returning a previously stored promise if available.
|
|
21
17
|
* Upon completion of the request, it optionally invokes an onSuccess callback if the request is successful, or an
|
|
22
18
|
* onFailure callback if the request fails.
|
|
23
|
-
*
|
|
24
|
-
* @param {AxiosInstance} client
|
|
25
|
-
* @param {string} method
|
|
26
|
-
* @param {string} url
|
|
27
|
-
* @param {Object|null} [data=null]
|
|
28
|
-
* @param {Object|null} [config=null]
|
|
29
|
-
* @param {Function|null} [onSuccess=null]
|
|
30
|
-
* @param {Function|null} [onError=null]
|
|
31
|
-
* @returns {Promise}
|
|
32
19
|
*/
|
|
33
|
-
async call(client, method, url, data =
|
|
20
|
+
async call(client, method, url, data = {}, config = {}, onSuccess, onError) {
|
|
34
21
|
data ??= {};
|
|
35
22
|
config ??= {};
|
|
36
23
|
const key = `${method.toLowerCase()}:${url}`;
|
|
@@ -63,5 +50,4 @@ class RequestManager {
|
|
|
63
50
|
return processedPromise;
|
|
64
51
|
}
|
|
65
52
|
}
|
|
66
|
-
|
|
67
53
|
export default new RequestManager();
|
package/package.json
CHANGED
|
@@ -1,24 +1,44 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
"name": "@jaseeey/request-manager",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A library to prevent multiple API requests when called simultaneously",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Jason Ilicic <me@jasey.io>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+ssh://git@github.com/jaseeey/request-manager.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/jaseeey/request-manager/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/jaseeey/request-manager#readme",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
20
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
21
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
22
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"axios": "^1.6.8"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@jest/globals": "^29.7.0",
|
|
29
|
+
"@types/axios": "^0.14.0",
|
|
30
|
+
"@types/jest": "^29.5.12",
|
|
31
|
+
"jest": "^29.7.0",
|
|
32
|
+
"ts-jest": "^29.1.2",
|
|
33
|
+
"typescript": "^5.4.3"
|
|
34
|
+
},
|
|
35
|
+
"main": "./dist/cjs/index.js",
|
|
36
|
+
"module": "./dist/esm/index.js",
|
|
37
|
+
"types": "./dist/esm/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"require": "./dist/cjs/index.js",
|
|
41
|
+
"import": "./dist/esm/index.js"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
24
44
|
}
|
package/index.js
DELETED
package/jest.config.mjs
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import RequestManager from '../request-manager.js';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
3
|
-
|
|
4
|
-
describe('RequestManager', () => {
|
|
5
|
-
|
|
6
|
-
let mockClient;
|
|
7
|
-
|
|
8
|
-
const mockURL1 = 'https://example.com';
|
|
9
|
-
const mockURL2 = 'https://example2.com';
|
|
10
|
-
const mockResponse1 = { data: 'response' };
|
|
11
|
-
const mockResponse2 = { data: 'response2' };
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
const mockRequests = new Map([
|
|
15
|
-
[ mockURL1, mockResponse1 ],
|
|
16
|
-
[ mockURL2, mockResponse2 ]
|
|
17
|
-
]);
|
|
18
|
-
mockClient = {
|
|
19
|
-
request: jest.fn().mockImplementation(({ url }) => {
|
|
20
|
-
if (mockRequests.has(url)) {
|
|
21
|
-
return Promise.resolve(mockRequests.get(url));
|
|
22
|
-
}
|
|
23
|
-
return Promise.reject(new Error('Mock URL not registered'));
|
|
24
|
-
})
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
RequestManager.activeRequests.clear();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('prevents duplicate simultaneous requests', async () => {
|
|
33
|
-
const [ firstResponse, secondResponse, thirdResponse ] = await Promise.all([
|
|
34
|
-
RequestManager.call(mockClient, 'GET', mockURL1),
|
|
35
|
-
RequestManager.call(mockClient, 'GET', mockURL1),
|
|
36
|
-
RequestManager.call(mockClient, 'GET', mockURL1)
|
|
37
|
-
]);
|
|
38
|
-
expect(mockClient.request).toHaveBeenCalledTimes(1);
|
|
39
|
-
expect(firstResponse).toBe(secondResponse);
|
|
40
|
-
expect(firstResponse).toBe(thirdResponse);
|
|
41
|
-
expect(RequestManager.activeRequests.has(`post:${ mockURL1 }`)).toBeFalsy();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('handles different requests independently', async () => {
|
|
45
|
-
const [ firstResponse, secondResponse, thirdResponse ] = await Promise.all([
|
|
46
|
-
RequestManager.call(mockClient, 'GET', mockURL1),
|
|
47
|
-
RequestManager.call(mockClient, 'GET', mockURL2),
|
|
48
|
-
RequestManager.call(mockClient, 'GET', mockURL2)
|
|
49
|
-
])
|
|
50
|
-
expect(firstResponse).toBe(mockResponse1);
|
|
51
|
-
expect(secondResponse).toBe(mockResponse2);
|
|
52
|
-
expect(secondResponse).toBe(thirdResponse);
|
|
53
|
-
expect(mockClient.request).toHaveBeenCalledTimes(2);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('calls onSuccess callback on successful request', async () => {
|
|
57
|
-
const onSuccessCb = jest.fn();
|
|
58
|
-
await RequestManager.call(mockClient, 'GET', mockURL1, null, null, onSuccessCb);
|
|
59
|
-
expect(onSuccessCb).toHaveBeenCalledWith(mockResponse1);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('calls onError callback on request failure', async () => {
|
|
63
|
-
const error = new Error('Request failed');
|
|
64
|
-
mockClient.request.mockRejectedValueOnce(error);
|
|
65
|
-
const onErrorCb = jest.fn();
|
|
66
|
-
await RequestManager.call(mockClient, 'GET', mockURL1, null, null, null, onErrorCb);
|
|
67
|
-
expect(onErrorCb).toHaveBeenCalledWith(error);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('returns the response when no callbacks are provided', async () => {
|
|
71
|
-
const result = await RequestManager.call(mockClient, 'GET', mockURL1);
|
|
72
|
-
expect(result).toBe(mockResponse1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('throws an error when the request fails and no onError callback is provided', async () => {
|
|
76
|
-
const error = new Error('Request failed');
|
|
77
|
-
mockClient.request.mockRejectedValueOnce(error);
|
|
78
|
-
await expect(RequestManager.call(mockClient, 'GET', mockURL1)).rejects.toThrow(error);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('allows a new request after the previous one completes', async () => {
|
|
82
|
-
await RequestManager.call(mockClient, 'GET', mockURL1);
|
|
83
|
-
mockClient.request.mockClear();
|
|
84
|
-
await RequestManager.call(mockClient, 'GET', mockURL1);
|
|
85
|
-
expect(mockClient.request).toHaveBeenCalledTimes(1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('handles requests with case-insensitive method', async () => {
|
|
89
|
-
const [ firstResponse, secondResponse ] = await Promise.all([
|
|
90
|
-
RequestManager.call(mockClient, 'gET', mockURL1),
|
|
91
|
-
RequestManager.call(mockClient, 'gEt', mockURL1)
|
|
92
|
-
])
|
|
93
|
-
expect(firstResponse).toBe(mockResponse1);
|
|
94
|
-
expect(secondResponse).toBe(secondResponse);
|
|
95
|
-
expect(mockClient.request).toHaveBeenCalledTimes(1);
|
|
96
|
-
});
|
|
97
|
-
});
|