@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.
@@ -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