@indra211/httpease 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 +40 -0
- package/README.md +425 -0
- package/package.json +35 -0
- package/src/HttpEase.js +372 -0
- package/src/errors/HttpError.js +27 -0
- package/src/index.js +19 -0
- package/src/interceptors/InterceptorManager.js +67 -0
- package/src/types/index.d.ts +221 -0
- package/src/utils/helpers.js +86 -0
- package/src/utils/progress.js +166 -0
- package/src/utils/transformers.js +45 -0
- package/src/utils/validators.js +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deep merge two objects
|
|
7
|
+
*/
|
|
8
|
+
function mergeConfig(config1, config2) {
|
|
9
|
+
const merged = { ...config1 };
|
|
10
|
+
|
|
11
|
+
Object.keys(config2).forEach(key => {
|
|
12
|
+
if (key === 'headers') {
|
|
13
|
+
merged.headers = { ...config1.headers, ...config2.headers };
|
|
14
|
+
} else if (config2[key] !== undefined) {
|
|
15
|
+
merged[key] = config2[key];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return merged;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build URL with query parameters
|
|
24
|
+
*/
|
|
25
|
+
function buildURL(url, params) {
|
|
26
|
+
if (!params) return url;
|
|
27
|
+
|
|
28
|
+
const searchParams = new URLSearchParams();
|
|
29
|
+
|
|
30
|
+
Object.keys(params).forEach(key => {
|
|
31
|
+
const value = params[key];
|
|
32
|
+
if (value === null || value === undefined) return;
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
value.forEach(v => searchParams.append(key, v));
|
|
36
|
+
} else {
|
|
37
|
+
searchParams.append(key, value);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const queryString = searchParams.toString();
|
|
42
|
+
if (!queryString) return url;
|
|
43
|
+
|
|
44
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
45
|
+
return url + separator + queryString;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if value is an object
|
|
50
|
+
*/
|
|
51
|
+
function isObject(val) {
|
|
52
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if running in browser
|
|
57
|
+
*/
|
|
58
|
+
function isBrowser() {
|
|
59
|
+
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sleep/delay utility
|
|
64
|
+
*/
|
|
65
|
+
function sleep(ms) {
|
|
66
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Calculate exponential backoff delay
|
|
71
|
+
*/
|
|
72
|
+
function calculateBackoff(attempt, baseDelay = 1000, maxDelay = 30000) {
|
|
73
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
74
|
+
// Add jitter (randomness) to prevent thundering herd
|
|
75
|
+
const jitter = delay * 0.1 * Math.random();
|
|
76
|
+
return delay + jitter;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
mergeConfig,
|
|
81
|
+
buildURL,
|
|
82
|
+
isObject,
|
|
83
|
+
isBrowser,
|
|
84
|
+
sleep,
|
|
85
|
+
calculateBackoff
|
|
86
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpEase Progress Tracking Utilities
|
|
3
|
+
*
|
|
4
|
+
* Zero-dependency upload and download progress using native
|
|
5
|
+
* ReadableStream / TransformStream APIs (Node.js 18+, modern browsers).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a ProgressEvent-like object.
|
|
10
|
+
* @param {number} loaded
|
|
11
|
+
* @param {number} total
|
|
12
|
+
* @returns {{ loaded: number, total: number, percentage: number }}
|
|
13
|
+
*/
|
|
14
|
+
function makeProgressEvent(loaded, total) {
|
|
15
|
+
return {
|
|
16
|
+
loaded,
|
|
17
|
+
total,
|
|
18
|
+
percentage: total > 0 ? Math.min(100, (loaded / total) * 100) : 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert a body value to a Uint8Array so we know its total byte size.
|
|
24
|
+
* Supports: string, Uint8Array/Buffer, ArrayBuffer.
|
|
25
|
+
* Returns null if the body type is not supported (e.g. FormData, ReadableStream).
|
|
26
|
+
*
|
|
27
|
+
* @param {unknown} body
|
|
28
|
+
* @returns {Uint8Array | null}
|
|
29
|
+
*/
|
|
30
|
+
function toUint8Array(body) {
|
|
31
|
+
if (body == null) return null;
|
|
32
|
+
|
|
33
|
+
if (body instanceof Uint8Array) return body;
|
|
34
|
+
|
|
35
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(body)) {
|
|
36
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (body instanceof ArrayBuffer) {
|
|
40
|
+
return new Uint8Array(body);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof body === 'string') {
|
|
44
|
+
return new TextEncoder().encode(body);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// FormData, Blob, ReadableStream — cannot easily track bytes
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wrap a request body as a ReadableStream that fires `onProgress` as bytes
|
|
53
|
+
* are consumed by the fetch engine.
|
|
54
|
+
*
|
|
55
|
+
* Falls back to the raw body (no progress) if:
|
|
56
|
+
* - The body type is unsupported (FormData, Blob, etc.)
|
|
57
|
+
* - TransformStream is not available in the environment
|
|
58
|
+
*
|
|
59
|
+
* @param {unknown} body - The serialized request body
|
|
60
|
+
* @param {Function} onProgress - `(event: ProgressEvent) => void`
|
|
61
|
+
* @returns {{ body: unknown, duplex: string | undefined }}
|
|
62
|
+
* Returns the (possibly wrapped) body and whether `duplex: 'half'` is needed.
|
|
63
|
+
*/
|
|
64
|
+
function wrapBodyWithProgress(body, onProgress) {
|
|
65
|
+
// Guard: requires TransformStream
|
|
66
|
+
if (typeof TransformStream === 'undefined') {
|
|
67
|
+
return { body, duplex: undefined };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const bytes = toUint8Array(body);
|
|
71
|
+
|
|
72
|
+
// Cannot determine size or not a byte-convertible type
|
|
73
|
+
if (bytes === null) {
|
|
74
|
+
return { body, duplex: undefined };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const total = bytes.byteLength;
|
|
78
|
+
let loaded = 0;
|
|
79
|
+
|
|
80
|
+
// Fire initial 0% event
|
|
81
|
+
onProgress(makeProgressEvent(0, total));
|
|
82
|
+
|
|
83
|
+
const { readable, writable } = new TransformStream({
|
|
84
|
+
transform(chunk, controller) {
|
|
85
|
+
controller.enqueue(chunk);
|
|
86
|
+
loaded += chunk.byteLength;
|
|
87
|
+
onProgress(makeProgressEvent(loaded, total));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Pipe bytes into the transform stream
|
|
92
|
+
const writer = writable.getWriter();
|
|
93
|
+
// Write in chunks to allow progress events to fire incrementally
|
|
94
|
+
const CHUNK_SIZE = 16 * 1024; // 16 KB chunks
|
|
95
|
+
let offset = 0;
|
|
96
|
+
|
|
97
|
+
(async () => {
|
|
98
|
+
while (offset < bytes.byteLength) {
|
|
99
|
+
const end = Math.min(offset + CHUNK_SIZE, bytes.byteLength);
|
|
100
|
+
await writer.write(bytes.slice(offset, end));
|
|
101
|
+
offset = end;
|
|
102
|
+
}
|
|
103
|
+
await writer.close();
|
|
104
|
+
})().catch(() => writer.abort());
|
|
105
|
+
|
|
106
|
+
// Return ReadableStream — fetch needs duplex: 'half' for streaming bodies
|
|
107
|
+
return { body: readable, duplex: 'half' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read a Response body as a stream, firing `onProgress` for each chunk.
|
|
112
|
+
* Returns the full body text (already consumed for you).
|
|
113
|
+
*
|
|
114
|
+
* Falls back to `response.text()` if ReadableStream not supported.
|
|
115
|
+
*
|
|
116
|
+
* @param {Response} response - The native fetch Response
|
|
117
|
+
* @param {Function} onProgress - `(event: ProgressEvent) => void`
|
|
118
|
+
* @returns {Promise<string>} The raw response text
|
|
119
|
+
*/
|
|
120
|
+
async function readBodyWithProgress(response, onProgress) {
|
|
121
|
+
// Guard: requires response.body ReadableStream
|
|
122
|
+
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
123
|
+
const text = await response.text();
|
|
124
|
+
onProgress(makeProgressEvent(text.length, text.length));
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const contentLength = response.headers.get('content-length');
|
|
129
|
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
130
|
+
|
|
131
|
+
const reader = response.body.getReader();
|
|
132
|
+
const chunks = [];
|
|
133
|
+
let loaded = 0;
|
|
134
|
+
|
|
135
|
+
// Fire initial 0% event
|
|
136
|
+
onProgress(makeProgressEvent(0, total));
|
|
137
|
+
|
|
138
|
+
while (true) {
|
|
139
|
+
const { done, value } = await reader.read();
|
|
140
|
+
|
|
141
|
+
if (done) break;
|
|
142
|
+
|
|
143
|
+
chunks.push(value);
|
|
144
|
+
loaded += value.byteLength;
|
|
145
|
+
onProgress(makeProgressEvent(loaded, total));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Ensure a final 100% event is fired
|
|
149
|
+
onProgress(makeProgressEvent(loaded, total > 0 ? total : loaded));
|
|
150
|
+
|
|
151
|
+
// Combine chunks and decode
|
|
152
|
+
const allBytes = new Uint8Array(loaded);
|
|
153
|
+
let position = 0;
|
|
154
|
+
for (const chunk of chunks) {
|
|
155
|
+
allBytes.set(chunk, position);
|
|
156
|
+
position += chunk.byteLength;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return new TextDecoder().decode(allBytes);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
wrapBodyWithProgress,
|
|
164
|
+
readBodyWithProgress,
|
|
165
|
+
makeProgressEvent
|
|
166
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { isObject } = require('./helpers');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default request data transformers
|
|
5
|
+
*/
|
|
6
|
+
const transformRequest = [
|
|
7
|
+
function (data, headers) {
|
|
8
|
+
// Auto-set Content-Type for objects
|
|
9
|
+
if (isObject(data) && !headers['Content-Type']) {
|
|
10
|
+
headers['Content-Type'] = 'application/json';
|
|
11
|
+
return JSON.stringify(data);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Handle FormData
|
|
15
|
+
if (typeof FormData !== 'undefined' && data instanceof FormData) {
|
|
16
|
+
// Let browser set Content-Type with boundary
|
|
17
|
+
delete headers['Content-Type'];
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default response data transformers
|
|
27
|
+
*/
|
|
28
|
+
const transformResponse = [
|
|
29
|
+
async function (response) {
|
|
30
|
+
const contentType = response.headers.get('content-type');
|
|
31
|
+
|
|
32
|
+
// Auto-parse JSON
|
|
33
|
+
if (contentType && contentType.includes('application/json')) {
|
|
34
|
+
return await response.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return text for other types
|
|
38
|
+
return await response.text();
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
transformRequest,
|
|
44
|
+
transformResponse
|
|
45
|
+
};
|
|
File without changes
|