@jaseeey/request-manager 1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/index.js +3 -0
- package/jest.config.mjs +9 -0
- package/package.json +24 -0
- package/request-manager.js +67 -0
- package/test/request-manager.spec.js +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jason Ilicic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# RequestManager
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Background and Scope
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
Users are encouraged to understand these known limitations and consider how they align with their specific needs before integrating the library into their projects.
|
|
14
|
+
|
|
15
|
+
Should you wish to extend the behaviour, or modify it for your own purposes, then you are free to do so.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Prevents duplicate simultaneous requests to the same URL and method.
|
|
20
|
+
- Supports optional onSuccess and onError callbacks for regulated response handling.
|
|
21
|
+
- Easy integration with any project that uses HTTP requests via Axios.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Here's a basic example of how to use the RequestManager library:
|
|
26
|
+
|
|
27
|
+
### With Callbacks
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import RequestManager from '@jaseeey/request-manager';
|
|
31
|
+
|
|
32
|
+
const client = axios.create();
|
|
33
|
+
const url = 'https://example.com';
|
|
34
|
+
const method = 'GET';
|
|
35
|
+
|
|
36
|
+
RequestManager.call(client, method, url).then(res => {
|
|
37
|
+
// Handle your response here, any processing will only be run once.
|
|
38
|
+
}).catch(err => {
|
|
39
|
+
// Handle your error here
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Without Callbacks
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
import RequestManager from '@jaseeey/request-manager';
|
|
47
|
+
|
|
48
|
+
const client = axios.create();
|
|
49
|
+
const url = 'https://example.com';
|
|
50
|
+
const method = 'GET';
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await RequestManager.call(client, method, url);
|
|
54
|
+
// Handle your response here, any processing will be run multiple times upon multiple requests.
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// Handle your error here
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API Reference
|
|
62
|
+
|
|
63
|
+
### `call(client, method, url, data, config, onSuccess, onError)`
|
|
64
|
+
|
|
65
|
+
- `client`: The HTTP client instance used for making requests.
|
|
66
|
+
- `method`: The HTTP method (e.g., 'GET', 'POST').
|
|
67
|
+
- `url`: The URL to which the request is sent.
|
|
68
|
+
- `data` (optional): The data to be sent as the request body.
|
|
69
|
+
- `config` (optional): The configuration options for the request.
|
|
70
|
+
- `onSuccess` (optional): A callback function that is called when the request is successful.
|
|
71
|
+
- `onError` (optional): A callback function that is called when the request fails.
|
|
72
|
+
|
|
73
|
+
## Known Limitations
|
|
74
|
+
|
|
75
|
+
### Unified Callback Execution
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
### State and Error Handling
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
### Request Differentiation
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
### Lifecycle Management
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
### Singleton Design
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
## Contributing
|
|
95
|
+
|
|
96
|
+
Contributions to the RequestManager library are welcome. If you have suggestions or improvements, feel free to fork the repository and submit a pull request.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
RequestManager is released under the [MIT License](LICENSE).
|
package/index.js
ADDED
package/jest.config.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jaseeey/request-manager",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A library to prevent multiple API requests when called simultaneously",
|
|
6
|
+
"main": "request-manager.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Jason Ilicic <me@jasey.io>",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+ssh://git@github.com/jaseeey/request-manager.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/jaseeey/request-manager/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/jaseeey/request-manager#readme",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@jest/globals": "^29.7.0",
|
|
22
|
+
"jest": "^29.7.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestManager
|
|
3
|
+
*
|
|
4
|
+
* Manages and regulates HTTP requests to ensure that multiple requests to the same URL and method are not made
|
|
5
|
+
* concurrently. When a request to a specific URL and method is in progress, subsequent requests to the same URL and
|
|
6
|
+
* method will return the same promise, preventing duplicate requests.
|
|
7
|
+
*/
|
|
8
|
+
class RequestManager {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A Map to store active requests, with each key being a combination of URL and HTTP method and its corresponding
|
|
12
|
+
* value being the Promise of the ongoing HTTP request.
|
|
13
|
+
*
|
|
14
|
+
* @type {Map<string, Promise>}
|
|
15
|
+
*/
|
|
16
|
+
activeRequests = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration. It
|
|
20
|
+
* de-duplicates requests to the same URL with the same method by returning a previously stored promise if available.
|
|
21
|
+
* Upon completion of the request, it optionally invokes an onSuccess callback if the request is successful, or an
|
|
22
|
+
* 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
|
+
*/
|
|
33
|
+
async call(client, method, url, data = null, config = null, onSuccess = null, onError = null) {
|
|
34
|
+
data ??= {};
|
|
35
|
+
config ??= {};
|
|
36
|
+
const key = `${method.toLowerCase()}:${url}`;
|
|
37
|
+
if (this.activeRequests.has(key)) {
|
|
38
|
+
return this.activeRequests.get(key).processed;
|
|
39
|
+
}
|
|
40
|
+
const processRequest = async (responsePromise) => {
|
|
41
|
+
try {
|
|
42
|
+
const requestResult = await responsePromise;
|
|
43
|
+
const callbackResult = onSuccess && typeof onSuccess === 'function'
|
|
44
|
+
? onSuccess(requestResult)
|
|
45
|
+
: undefined;
|
|
46
|
+
return callbackResult !== undefined
|
|
47
|
+
? callbackResult
|
|
48
|
+
: requestResult;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (onError && typeof onError === 'function') {
|
|
52
|
+
onError(err);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const requestPromise = client.request({ ...config, method, url, data }).finally(() => {
|
|
59
|
+
this.activeRequests.delete(key);
|
|
60
|
+
});
|
|
61
|
+
const processedPromise = processRequest(requestPromise);
|
|
62
|
+
this.activeRequests.set(key, { original: requestPromise, processed: processedPromise });
|
|
63
|
+
return processedPromise;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default new RequestManager();
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
});
|