@jaseeey/request-manager 2.1.4 → 2.2.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 +599 -61
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/request-manager/index.d.ts +1 -0
- package/dist/cjs/request-manager/request-manager.d.ts +54 -8
- package/dist/cjs/request-manager/request-manager.js +109 -21
- package/dist/cjs/request-manager/sha256.d.ts +14 -0
- package/dist/cjs/request-manager/sha256.js +106 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/request-manager/index.d.ts +1 -0
- package/dist/esm/request-manager/request-manager.d.ts +54 -8
- package/dist/esm/request-manager/request-manager.js +109 -21
- package/dist/esm/request-manager/sha256.d.ts +14 -0
- package/dist/esm/request-manager/sha256.js +102 -0
- package/package.json +37 -23
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sha256Hex } from './sha256.js';
|
|
1
2
|
/**
|
|
2
3
|
* RequestManager
|
|
3
4
|
*
|
|
@@ -7,8 +8,8 @@
|
|
|
7
8
|
*/
|
|
8
9
|
export class RequestManager {
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* In-flight requests keyed by a short SHA-256 hash of the request identity.
|
|
12
|
+
* Entry values expose `identity` for debugging plus `original` / `processed` promises.
|
|
12
13
|
*/
|
|
13
14
|
activeRequests = new Map();
|
|
14
15
|
clientIds = new WeakMap();
|
|
@@ -24,7 +25,8 @@ export class RequestManager {
|
|
|
24
25
|
/**
|
|
25
26
|
* Executes or retrieves an ongoing HTTP request based on the provided URL, method, payload, and configuration.
|
|
26
27
|
*
|
|
27
|
-
* Requests are de-duplicated
|
|
28
|
+
* Requests are de-duplicated when they target the same client instance, method, URL, and `config.params`.
|
|
29
|
+
* Request bodies and other config fields (headers, timeout, signal, etc.) are not part of the key.
|
|
28
30
|
* If `onSuccess` returns a non-`undefined` value, that value becomes the resolved result.
|
|
29
31
|
*
|
|
30
32
|
* @param client Axios client used to execute the request.
|
|
@@ -33,50 +35,136 @@ export class RequestManager {
|
|
|
33
35
|
* @param data Optional request payload.
|
|
34
36
|
* @param config Optional Axios request configuration.
|
|
35
37
|
* @param onSuccess Optional callback invoked on success. Returning a value overrides the resolved response.
|
|
36
|
-
* @param onError Optional callback
|
|
38
|
+
* @param onError Optional callback for request failures only. If it throws, the original request error is rethrown.
|
|
37
39
|
*/
|
|
38
|
-
|
|
40
|
+
call(client, method, url, data = {}, config = {}, onSuccess, onError) {
|
|
41
|
+
if (typeof method !== 'string' || method.length === 0) {
|
|
42
|
+
return Promise.reject(new TypeError('RequestManager.call requires a non-empty HTTP method string'));
|
|
43
|
+
}
|
|
44
|
+
if (typeof url !== 'string') {
|
|
45
|
+
return Promise.reject(new TypeError('RequestManager.call requires a URL string'));
|
|
46
|
+
}
|
|
39
47
|
data ??= {};
|
|
40
48
|
config ??= {};
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
const identity = this.buildRequestIdentity(client, method, url, config.params);
|
|
50
|
+
const hash = this.hashIdentity(identity);
|
|
51
|
+
const existing = this.activeRequests.get(hash);
|
|
52
|
+
if (existing) {
|
|
53
|
+
return existing.processed;
|
|
44
54
|
}
|
|
45
55
|
const processRequest = async (responsePromise) => {
|
|
56
|
+
let requestResult;
|
|
46
57
|
try {
|
|
47
|
-
|
|
48
|
-
const callbackResult = onSuccess && typeof onSuccess === 'function'
|
|
49
|
-
? await onSuccess(requestResult)
|
|
50
|
-
: undefined;
|
|
51
|
-
return callbackResult !== undefined
|
|
52
|
-
? callbackResult
|
|
53
|
-
: requestResult;
|
|
58
|
+
requestResult = await responsePromise;
|
|
54
59
|
}
|
|
55
60
|
catch (err) {
|
|
56
61
|
if (onError && typeof onError === 'function') {
|
|
57
|
-
|
|
62
|
+
try {
|
|
63
|
+
onError(err);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
58
68
|
return;
|
|
59
69
|
}
|
|
60
70
|
throw err;
|
|
61
71
|
}
|
|
72
|
+
if (!(onSuccess && typeof onSuccess === 'function')) {
|
|
73
|
+
return requestResult;
|
|
74
|
+
}
|
|
75
|
+
const callbackResult = await onSuccess(requestResult);
|
|
76
|
+
return callbackResult !== undefined
|
|
77
|
+
? callbackResult
|
|
78
|
+
: requestResult;
|
|
62
79
|
};
|
|
63
80
|
const requestPromise = client.request({ ...config, method, url, data }).finally(() => {
|
|
64
|
-
this.activeRequests.delete(
|
|
81
|
+
this.activeRequests.delete(hash);
|
|
65
82
|
});
|
|
66
83
|
const processedPromise = processRequest(requestPromise);
|
|
67
|
-
this.activeRequests.set(
|
|
84
|
+
this.activeRequests.set(hash, {
|
|
85
|
+
hash,
|
|
86
|
+
identity,
|
|
87
|
+
original: requestPromise,
|
|
88
|
+
processed: processedPromise
|
|
89
|
+
});
|
|
68
90
|
return processedPromise;
|
|
69
91
|
}
|
|
70
92
|
/**
|
|
71
|
-
*
|
|
93
|
+
* Builds the stable human-readable identity for an in-flight request.
|
|
94
|
+
*
|
|
95
|
+
* @param client Axios client instance used for the request.
|
|
96
|
+
* @param method HTTP method.
|
|
97
|
+
* @param url Request URL string as passed to `call`.
|
|
98
|
+
* @param params Axios `config.params` value, if any.
|
|
99
|
+
* @returns Identity fields used for hashing and inspection.
|
|
72
100
|
*/
|
|
73
|
-
|
|
101
|
+
buildRequestIdentity(client, method, url, params) {
|
|
74
102
|
let clientId = this.clientIds.get(client);
|
|
75
103
|
if (!clientId) {
|
|
76
104
|
clientId = this.nextClientId++;
|
|
77
105
|
this.clientIds.set(client, clientId);
|
|
78
106
|
}
|
|
79
|
-
|
|
107
|
+
const normalisedMethod = method.toLowerCase();
|
|
108
|
+
const paramsSerialised = this.serializeParams(params);
|
|
109
|
+
return {
|
|
110
|
+
clientId,
|
|
111
|
+
method: normalisedMethod,
|
|
112
|
+
url,
|
|
113
|
+
params: params ?? null,
|
|
114
|
+
paramsSerialised
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Hashes a request identity into a fixed-length map key.
|
|
119
|
+
*
|
|
120
|
+
* @param identity Stable request identity.
|
|
121
|
+
* @returns Lowercase hex SHA-256 digest of the identity string.
|
|
122
|
+
*/
|
|
123
|
+
hashIdentity(identity) {
|
|
124
|
+
const material = `${identity.clientId}:${identity.method}:${identity.url}:${identity.paramsSerialised}`;
|
|
125
|
+
return sha256Hex(material);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Serialises query params into a deterministic string for request keys.
|
|
129
|
+
*
|
|
130
|
+
* @param params Axios params value (`undefined`, plain object, array, or `URLSearchParams`).
|
|
131
|
+
* @returns A stable serialisation, or an empty string when params are absent.
|
|
132
|
+
*/
|
|
133
|
+
serializeParams(params) {
|
|
134
|
+
if (params == null) {
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
if (typeof URLSearchParams !== 'undefined' && params instanceof URLSearchParams) {
|
|
138
|
+
if ([...params.keys()].length === 0) {
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
const asObject = {};
|
|
142
|
+
for (const key of [...params.keys()].sort()) {
|
|
143
|
+
asObject[key] = params.getAll(key);
|
|
144
|
+
}
|
|
145
|
+
return this.stableStringify(asObject);
|
|
146
|
+
}
|
|
147
|
+
if (typeof params === 'object' && !Array.isArray(params) && Object.keys(params).length === 0) {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
return this.stableStringify(params);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* JSON-like serialisation with sorted object keys so param order does not affect identity.
|
|
154
|
+
*
|
|
155
|
+
* @param value Value to serialise.
|
|
156
|
+
* @returns Deterministic string form of `value`.
|
|
157
|
+
*/
|
|
158
|
+
stableStringify(value) {
|
|
159
|
+
if (value === null || typeof value !== 'object') {
|
|
160
|
+
return JSON.stringify(value);
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(value)) {
|
|
163
|
+
return `[${value.map(entry => this.stableStringify(entry)).join(',')}]`;
|
|
164
|
+
}
|
|
165
|
+
const record = value;
|
|
166
|
+
const keys = Object.keys(record).sort();
|
|
167
|
+
return `{${keys.map(key => `${JSON.stringify(key)}:${this.stableStringify(record[key])}`).join(',')}}`;
|
|
80
168
|
}
|
|
81
169
|
}
|
|
82
170
|
const requestManager = new RequestManager();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous SHA-256 hex digest for browser and Node without async WebCrypto.
|
|
3
|
+
*
|
|
4
|
+
* @param message UTF-8 string to hash.
|
|
5
|
+
* @returns Lowercase hex digest.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sha256Hex(message: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Encodes a string as UTF-8 bytes.
|
|
10
|
+
*
|
|
11
|
+
* @param message String to encode.
|
|
12
|
+
* @returns UTF-8 byte array.
|
|
13
|
+
*/
|
|
14
|
+
export declare function utf8ToBytes(message: string): Uint8Array;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous SHA-256 hex digest for browser and Node without async WebCrypto.
|
|
3
|
+
*
|
|
4
|
+
* @param message UTF-8 string to hash.
|
|
5
|
+
* @returns Lowercase hex digest.
|
|
6
|
+
*/
|
|
7
|
+
export function sha256Hex(message) {
|
|
8
|
+
const bytes = utf8ToBytes(message);
|
|
9
|
+
const bitLength = bytes.length * 8;
|
|
10
|
+
const withOne = new Uint8Array(((bytes.length + 9 + 63) & ~63));
|
|
11
|
+
withOne.set(bytes);
|
|
12
|
+
withOne[bytes.length] = 0x80;
|
|
13
|
+
const view = new DataView(withOne.buffer);
|
|
14
|
+
view.setUint32(withOne.length - 8, Math.floor(bitLength / 0x100000000), false);
|
|
15
|
+
view.setUint32(withOne.length - 4, bitLength >>> 0, false);
|
|
16
|
+
let h0 = 0x6a09e667;
|
|
17
|
+
let h1 = 0xbb67ae85;
|
|
18
|
+
let h2 = 0x3c6ef372;
|
|
19
|
+
let h3 = 0xa54ff53a;
|
|
20
|
+
let h4 = 0x510e527f;
|
|
21
|
+
let h5 = 0x9b05688c;
|
|
22
|
+
let h6 = 0x1f83d9ab;
|
|
23
|
+
let h7 = 0x5be0cd19;
|
|
24
|
+
const w = new Int32Array(64);
|
|
25
|
+
const k = SHA256_K;
|
|
26
|
+
for (let offset = 0; offset < withOne.length; offset += 64) {
|
|
27
|
+
for (let i = 0; i < 16; i++) {
|
|
28
|
+
w[i] = view.getInt32(offset + i * 4, false);
|
|
29
|
+
}
|
|
30
|
+
for (let i = 16; i < 64; i++) {
|
|
31
|
+
const s0 = rightRotate(w[i - 15], 7) ^ rightRotate(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
32
|
+
const s1 = rightRotate(w[i - 2], 17) ^ rightRotate(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
33
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
|
|
34
|
+
}
|
|
35
|
+
let a = h0;
|
|
36
|
+
let b = h1;
|
|
37
|
+
let c = h2;
|
|
38
|
+
let d = h3;
|
|
39
|
+
let e = h4;
|
|
40
|
+
let f = h5;
|
|
41
|
+
let g = h6;
|
|
42
|
+
let h = h7;
|
|
43
|
+
for (let i = 0; i < 64; i++) {
|
|
44
|
+
const S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
|
|
45
|
+
const ch = (e & f) ^ (~e & g);
|
|
46
|
+
const temp1 = (h + S1 + ch + k[i] + w[i]) | 0;
|
|
47
|
+
const S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
|
|
48
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
49
|
+
const temp2 = (S0 + maj) | 0;
|
|
50
|
+
h = g;
|
|
51
|
+
g = f;
|
|
52
|
+
f = e;
|
|
53
|
+
e = (d + temp1) | 0;
|
|
54
|
+
d = c;
|
|
55
|
+
c = b;
|
|
56
|
+
b = a;
|
|
57
|
+
a = (temp1 + temp2) | 0;
|
|
58
|
+
}
|
|
59
|
+
h0 = (h0 + a) | 0;
|
|
60
|
+
h1 = (h1 + b) | 0;
|
|
61
|
+
h2 = (h2 + c) | 0;
|
|
62
|
+
h3 = (h3 + d) | 0;
|
|
63
|
+
h4 = (h4 + e) | 0;
|
|
64
|
+
h5 = (h5 + f) | 0;
|
|
65
|
+
h6 = (h6 + g) | 0;
|
|
66
|
+
h7 = (h7 + h) | 0;
|
|
67
|
+
}
|
|
68
|
+
return [h0, h1, h2, h3, h4, h5, h6, h7].map(wordToHex).join('');
|
|
69
|
+
}
|
|
70
|
+
function rightRotate(value, amount) {
|
|
71
|
+
return (value >>> amount) | (value << (32 - amount));
|
|
72
|
+
}
|
|
73
|
+
function wordToHex(value) {
|
|
74
|
+
return (value >>> 0).toString(16).padStart(8, '0');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Encodes a string as UTF-8 bytes.
|
|
78
|
+
*
|
|
79
|
+
* @param message String to encode.
|
|
80
|
+
* @returns UTF-8 byte array.
|
|
81
|
+
*/
|
|
82
|
+
export function utf8ToBytes(message) {
|
|
83
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
84
|
+
return new TextEncoder().encode(message);
|
|
85
|
+
}
|
|
86
|
+
const encoded = unescape(encodeURIComponent(message));
|
|
87
|
+
const bytes = new Uint8Array(encoded.length);
|
|
88
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
89
|
+
bytes[i] = encoded.charCodeAt(i);
|
|
90
|
+
}
|
|
91
|
+
return bytes;
|
|
92
|
+
}
|
|
93
|
+
const SHA256_K = new Int32Array([
|
|
94
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
95
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
96
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
97
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
98
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
99
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
100
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
101
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
102
|
+
]);
|
package/package.json
CHANGED
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaseeey/request-manager",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "De-duplicate concurrent Axios HTTP requests so independent callers share one in-flight promise and response",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"axios",
|
|
7
|
+
"http",
|
|
8
|
+
"request",
|
|
9
|
+
"dedupe",
|
|
10
|
+
"de-duplicate",
|
|
11
|
+
"inflight",
|
|
12
|
+
"promise",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
5
15
|
"license": "MIT",
|
|
6
16
|
"author": "Jason Ilicic <me@jasey.io>",
|
|
17
|
+
"homepage": "https://github.com/jaseeey/request-manager#readme",
|
|
7
18
|
"repository": {
|
|
8
19
|
"type": "git",
|
|
9
|
-
"url": "git+
|
|
20
|
+
"url": "git+https://github.com/jaseeey/request-manager.git"
|
|
10
21
|
},
|
|
11
22
|
"bugs": {
|
|
12
23
|
"url": "https://github.com/jaseeey/request-manager/issues"
|
|
13
24
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
"dist"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
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",
|
|
22
|
-
"build": "npm run build:esm && npm run build:cjs",
|
|
23
|
-
"prepack": "npm run build",
|
|
24
|
-
"test": "vitest run --coverage"
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"axios": "^1.16.0"
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@vitest/coverage-v8": "^4.1.4",
|
|
31
|
-
"braces": "^3.0.3",
|
|
32
|
-
"typescript": "^5.9.3",
|
|
33
|
-
"vitest": "^4.1.4"
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
34
27
|
},
|
|
35
28
|
"main": "./dist/cjs/index.js",
|
|
36
29
|
"module": "./dist/esm/index.js",
|
|
@@ -46,5 +39,26 @@
|
|
|
46
39
|
"require": "./dist/cjs/request-manager/index.js",
|
|
47
40
|
"import": "./dist/esm/request-manager/index.js"
|
|
48
41
|
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"scripts": {
|
|
48
|
+
"prebuild": "rm -rf dist",
|
|
49
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
50
|
+
"build:esm": "tsc -p tsconfig.esm.json && echo '{\"type\": \"module\"}' > dist/esm/package.json",
|
|
51
|
+
"build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
|
52
|
+
"prepack": "npm run build",
|
|
53
|
+
"test": "vitest run --coverage"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"axios": ">=1"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
60
|
+
"axios": "^1.18.1",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"vitest": "^4.1.10"
|
|
49
63
|
}
|
|
50
64
|
}
|