zero-query 1.0.9 → 1.2.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 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +443 -0
- package/cli/commands/build.js +254 -216
- package/cli/commands/bundle.js +1228 -1183
- package/cli/commands/create.js +137 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -167
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
- package/cli/scaffold/webrtc/app/lib/room.js +252 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +7264 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10313 -6252
- package/dist/zquery.min.js +8 -601
- package/index.d.ts +570 -365
- package/index.js +311 -232
- package/package.json +76 -69
- package/src/component.js +1709 -1454
- package/src/core.js +921 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +922 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +255 -254
- package/src/router.js +843 -773
- package/src/ssr.js +418 -418
- package/src/store.js +318 -272
- package/src/utils.js +515 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/cli.test.js +1136 -1023
- package/tests/compare.test.js +497 -0
- package/tests/component.test.js +3969 -3938
- package/tests/core.test.js +1910 -1910
- package/tests/dev-server.test.js +489 -0
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -0
- package/tests/electron-features.test.js +864 -0
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1056 -1056
- package/tests/http.test.js +648 -648
- package/tests/reactive.test.js +819 -819
- package/tests/router.test.js +2327 -2327
- package/tests/ssr.test.js +870 -870
- package/tests/store.test.js +830 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1377 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -145
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
package/src/http.js
CHANGED
|
@@ -1,242 +1,242 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* zQuery HTTP - Lightweight fetch wrapper
|
|
3
|
-
*
|
|
4
|
-
* Clean API for GET/POST/PUT/PATCH/DELETE with:
|
|
5
|
-
* - Auto JSON serialization/deserialization
|
|
6
|
-
* - Request/response interceptors
|
|
7
|
-
* - Timeout support
|
|
8
|
-
* - Base URL configuration
|
|
9
|
-
* - Abort controller integration
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* $.http.get('/api/users');
|
|
13
|
-
* $.http.post('/api/users', { name: 'Tony' });
|
|
14
|
-
* $.http.configure({ baseURL: 'https://api.example.com', headers: { Authorization: 'Bearer ...' } });
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const _config = {
|
|
18
|
-
baseURL: '',
|
|
19
|
-
headers: { 'Content-Type': 'application/json' },
|
|
20
|
-
timeout: 30000,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const _interceptors = {
|
|
24
|
-
request: [],
|
|
25
|
-
response: [],
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Core request function
|
|
31
|
-
*/
|
|
32
|
-
async function request(method, url, data, options = {}) {
|
|
33
|
-
if (!url || typeof url !== 'string') {
|
|
34
|
-
throw new Error(`HTTP request requires a URL string, got ${typeof url}`);
|
|
35
|
-
}
|
|
36
|
-
let fullURL = url.startsWith('http') ? url : _config.baseURL + url;
|
|
37
|
-
let headers = { ..._config.headers, ...options.headers };
|
|
38
|
-
let body = undefined;
|
|
39
|
-
|
|
40
|
-
// Build fetch options
|
|
41
|
-
const fetchOpts = {
|
|
42
|
-
method: method.toUpperCase(),
|
|
43
|
-
headers,
|
|
44
|
-
...options,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Handle body
|
|
48
|
-
if (data !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
49
|
-
if (data instanceof FormData) {
|
|
50
|
-
body = data;
|
|
51
|
-
delete fetchOpts.headers['Content-Type']; // Let browser set multipart boundary
|
|
52
|
-
} else if (typeof data === 'object') {
|
|
53
|
-
body = JSON.stringify(data);
|
|
54
|
-
} else {
|
|
55
|
-
body = data;
|
|
56
|
-
}
|
|
57
|
-
fetchOpts.body = body;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Query params for GET
|
|
61
|
-
if (data && (method === 'GET' || method === 'HEAD') && typeof data === 'object') {
|
|
62
|
-
const params = new URLSearchParams(data).toString();
|
|
63
|
-
fullURL += (fullURL.includes('?') ? '&' : '?') + params;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Timeout via AbortController
|
|
67
|
-
const controller = new AbortController();
|
|
68
|
-
const timeout = options.timeout ?? _config.timeout;
|
|
69
|
-
let timer;
|
|
70
|
-
// Combine user signal with internal controller for proper timeout support
|
|
71
|
-
if (options.signal) {
|
|
72
|
-
// If AbortSignal.any is available, combine both signals
|
|
73
|
-
if (typeof AbortSignal.any === 'function') {
|
|
74
|
-
fetchOpts.signal = AbortSignal.any([options.signal, controller.signal]);
|
|
75
|
-
} else {
|
|
76
|
-
// Fallback: forward user signal's abort to our controller
|
|
77
|
-
fetchOpts.signal = controller.signal;
|
|
78
|
-
if (options.signal.aborted) {
|
|
79
|
-
controller.abort(options.signal.reason);
|
|
80
|
-
} else {
|
|
81
|
-
options.signal.addEventListener('abort', () => controller.abort(options.signal.reason), { once: true });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
fetchOpts.signal = controller.signal;
|
|
86
|
-
}
|
|
87
|
-
let _timedOut = false;
|
|
88
|
-
if (timeout > 0) {
|
|
89
|
-
timer = setTimeout(() => { _timedOut = true; controller.abort(); }, timeout);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Run request interceptors
|
|
93
|
-
for (const interceptor of _interceptors.request) {
|
|
94
|
-
const result = await interceptor(fetchOpts, fullURL);
|
|
95
|
-
if (result === false) throw new Error('Request blocked by interceptor');
|
|
96
|
-
if (result?.url) fullURL = result.url;
|
|
97
|
-
if (result?.options) Object.assign(fetchOpts, result.options);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const response = await fetch(fullURL, fetchOpts);
|
|
102
|
-
if (timer) clearTimeout(timer);
|
|
103
|
-
|
|
104
|
-
// Parse response
|
|
105
|
-
const contentType = response.headers.get('Content-Type') || '';
|
|
106
|
-
let responseData;
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
if (contentType.includes('application/json')) {
|
|
110
|
-
responseData = await response.json();
|
|
111
|
-
} else if (contentType.includes('text/')) {
|
|
112
|
-
responseData = await response.text();
|
|
113
|
-
} else if (contentType.includes('application/octet-stream') || contentType.includes('image/')) {
|
|
114
|
-
responseData = await response.blob();
|
|
115
|
-
} else {
|
|
116
|
-
// Try JSON first, fall back to text
|
|
117
|
-
const text = await response.text();
|
|
118
|
-
try { responseData = JSON.parse(text); } catch { responseData = text; }
|
|
119
|
-
}
|
|
120
|
-
} catch (parseErr) {
|
|
121
|
-
responseData = null;
|
|
122
|
-
console.warn(`[zQuery HTTP] Failed to parse response body from ${method} ${fullURL}:`, parseErr.message);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const result = {
|
|
126
|
-
ok: response.ok,
|
|
127
|
-
status: response.status,
|
|
128
|
-
statusText: response.statusText,
|
|
129
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
130
|
-
data: responseData,
|
|
131
|
-
response,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Run response interceptors
|
|
135
|
-
for (const interceptor of _interceptors.response) {
|
|
136
|
-
await interceptor(result);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!response.ok) {
|
|
140
|
-
const err = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
141
|
-
err.response = result;
|
|
142
|
-
throw err;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return result;
|
|
146
|
-
} catch (err) {
|
|
147
|
-
if (timer) clearTimeout(timer);
|
|
148
|
-
if (err.name === 'AbortError') {
|
|
149
|
-
if (_timedOut) {
|
|
150
|
-
throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
|
|
151
|
-
}
|
|
152
|
-
throw new Error(`Request aborted: ${method} ${fullURL}`);
|
|
153
|
-
}
|
|
154
|
-
throw err;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// Public API
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
export const http = {
|
|
163
|
-
get: (url, params, opts) => request('GET', url, params, opts),
|
|
164
|
-
post: (url, data, opts) => request('POST', url, data, opts),
|
|
165
|
-
put: (url, data, opts) => request('PUT', url, data, opts),
|
|
166
|
-
patch: (url, data, opts) => request('PATCH', url, data, opts),
|
|
167
|
-
delete: (url, data, opts) => request('DELETE', url, data, opts),
|
|
168
|
-
head: (url, opts) => request('HEAD', url, undefined, opts),
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Configure defaults
|
|
172
|
-
*/
|
|
173
|
-
configure(opts) {
|
|
174
|
-
if (opts.baseURL !== undefined) _config.baseURL = opts.baseURL;
|
|
175
|
-
if (opts.headers) Object.assign(_config.headers, opts.headers);
|
|
176
|
-
if (opts.timeout !== undefined) _config.timeout = opts.timeout;
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Read-only snapshot of current configuration
|
|
181
|
-
*/
|
|
182
|
-
getConfig() {
|
|
183
|
-
return {
|
|
184
|
-
baseURL: _config.baseURL,
|
|
185
|
-
headers: { ..._config.headers },
|
|
186
|
-
timeout: _config.timeout,
|
|
187
|
-
};
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Add request interceptor
|
|
192
|
-
* @param {Function} fn - (fetchOpts, url) → void | false | { url, options }
|
|
193
|
-
* @returns {Function} unsubscribe function
|
|
194
|
-
*/
|
|
195
|
-
onRequest(fn) {
|
|
196
|
-
_interceptors.request.push(fn);
|
|
197
|
-
return () => {
|
|
198
|
-
const idx = _interceptors.request.indexOf(fn);
|
|
199
|
-
if (idx !== -1) _interceptors.request.splice(idx, 1);
|
|
200
|
-
};
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Add response interceptor
|
|
205
|
-
* @param {Function} fn - (result) → void
|
|
206
|
-
* @returns {Function} unsubscribe function
|
|
207
|
-
*/
|
|
208
|
-
onResponse(fn) {
|
|
209
|
-
_interceptors.response.push(fn);
|
|
210
|
-
return () => {
|
|
211
|
-
const idx = _interceptors.response.indexOf(fn);
|
|
212
|
-
if (idx !== -1) _interceptors.response.splice(idx, 1);
|
|
213
|
-
};
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Clear interceptors - all, or just 'request' / 'response'
|
|
218
|
-
*/
|
|
219
|
-
clearInterceptors(type) {
|
|
220
|
-
if (!type || type === 'request') _interceptors.request.length = 0;
|
|
221
|
-
if (!type || type === 'response') _interceptors.response.length = 0;
|
|
222
|
-
},
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Run multiple requests in parallel
|
|
226
|
-
*/
|
|
227
|
-
all(requests) {
|
|
228
|
-
return Promise.all(requests);
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Create a standalone AbortController for manual cancellation
|
|
233
|
-
*/
|
|
234
|
-
createAbort() {
|
|
235
|
-
return new AbortController();
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Raw fetch pass-through (for edge cases)
|
|
240
|
-
*/
|
|
241
|
-
raw: (url, opts) => fetch(url, opts),
|
|
242
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* zQuery HTTP - Lightweight fetch wrapper
|
|
3
|
+
*
|
|
4
|
+
* Clean API for GET/POST/PUT/PATCH/DELETE with:
|
|
5
|
+
* - Auto JSON serialization/deserialization
|
|
6
|
+
* - Request/response interceptors
|
|
7
|
+
* - Timeout support
|
|
8
|
+
* - Base URL configuration
|
|
9
|
+
* - Abort controller integration
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* $.http.get('/api/users');
|
|
13
|
+
* $.http.post('/api/users', { name: 'Tony' });
|
|
14
|
+
* $.http.configure({ baseURL: 'https://api.example.com', headers: { Authorization: 'Bearer ...' } });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const _config = {
|
|
18
|
+
baseURL: '',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
timeout: 30000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const _interceptors = {
|
|
24
|
+
request: [],
|
|
25
|
+
response: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Core request function
|
|
31
|
+
*/
|
|
32
|
+
async function request(method, url, data, options = {}) {
|
|
33
|
+
if (!url || typeof url !== 'string') {
|
|
34
|
+
throw new Error(`HTTP request requires a URL string, got ${typeof url}`);
|
|
35
|
+
}
|
|
36
|
+
let fullURL = url.startsWith('http') ? url : _config.baseURL + url;
|
|
37
|
+
let headers = { ..._config.headers, ...options.headers };
|
|
38
|
+
let body = undefined;
|
|
39
|
+
|
|
40
|
+
// Build fetch options
|
|
41
|
+
const fetchOpts = {
|
|
42
|
+
method: method.toUpperCase(),
|
|
43
|
+
headers,
|
|
44
|
+
...options,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Handle body
|
|
48
|
+
if (data !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
49
|
+
if (data instanceof FormData) {
|
|
50
|
+
body = data;
|
|
51
|
+
delete fetchOpts.headers['Content-Type']; // Let browser set multipart boundary
|
|
52
|
+
} else if (typeof data === 'object') {
|
|
53
|
+
body = JSON.stringify(data);
|
|
54
|
+
} else {
|
|
55
|
+
body = data;
|
|
56
|
+
}
|
|
57
|
+
fetchOpts.body = body;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Query params for GET
|
|
61
|
+
if (data && (method === 'GET' || method === 'HEAD') && typeof data === 'object') {
|
|
62
|
+
const params = new URLSearchParams(data).toString();
|
|
63
|
+
fullURL += (fullURL.includes('?') ? '&' : '?') + params;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Timeout via AbortController
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timeout = options.timeout ?? _config.timeout;
|
|
69
|
+
let timer;
|
|
70
|
+
// Combine user signal with internal controller for proper timeout support
|
|
71
|
+
if (options.signal) {
|
|
72
|
+
// If AbortSignal.any is available, combine both signals
|
|
73
|
+
if (typeof AbortSignal.any === 'function') {
|
|
74
|
+
fetchOpts.signal = AbortSignal.any([options.signal, controller.signal]);
|
|
75
|
+
} else {
|
|
76
|
+
// Fallback: forward user signal's abort to our controller
|
|
77
|
+
fetchOpts.signal = controller.signal;
|
|
78
|
+
if (options.signal.aborted) {
|
|
79
|
+
controller.abort(options.signal.reason);
|
|
80
|
+
} else {
|
|
81
|
+
options.signal.addEventListener('abort', () => controller.abort(options.signal.reason), { once: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
fetchOpts.signal = controller.signal;
|
|
86
|
+
}
|
|
87
|
+
let _timedOut = false;
|
|
88
|
+
if (timeout > 0) {
|
|
89
|
+
timer = setTimeout(() => { _timedOut = true; controller.abort(); }, timeout);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Run request interceptors
|
|
93
|
+
for (const interceptor of _interceptors.request) {
|
|
94
|
+
const result = await interceptor(fetchOpts, fullURL);
|
|
95
|
+
if (result === false) throw new Error('Request blocked by interceptor');
|
|
96
|
+
if (result?.url) fullURL = result.url;
|
|
97
|
+
if (result?.options) Object.assign(fetchOpts, result.options);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(fullURL, fetchOpts);
|
|
102
|
+
if (timer) clearTimeout(timer);
|
|
103
|
+
|
|
104
|
+
// Parse response
|
|
105
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
106
|
+
let responseData;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (contentType.includes('application/json')) {
|
|
110
|
+
responseData = await response.json();
|
|
111
|
+
} else if (contentType.includes('text/')) {
|
|
112
|
+
responseData = await response.text();
|
|
113
|
+
} else if (contentType.includes('application/octet-stream') || contentType.includes('image/')) {
|
|
114
|
+
responseData = await response.blob();
|
|
115
|
+
} else {
|
|
116
|
+
// Try JSON first, fall back to text
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
try { responseData = JSON.parse(text); } catch { responseData = text; }
|
|
119
|
+
}
|
|
120
|
+
} catch (parseErr) {
|
|
121
|
+
responseData = null;
|
|
122
|
+
console.warn(`[zQuery HTTP] Failed to parse response body from ${method} ${fullURL}:`, parseErr.message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = {
|
|
126
|
+
ok: response.ok,
|
|
127
|
+
status: response.status,
|
|
128
|
+
statusText: response.statusText,
|
|
129
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
130
|
+
data: responseData,
|
|
131
|
+
response,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Run response interceptors
|
|
135
|
+
for (const interceptor of _interceptors.response) {
|
|
136
|
+
await interceptor(result);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const err = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
141
|
+
err.response = result;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (timer) clearTimeout(timer);
|
|
148
|
+
if (err.name === 'AbortError') {
|
|
149
|
+
if (_timedOut) {
|
|
150
|
+
throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`Request aborted: ${method} ${fullURL}`);
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Public API
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
export const http = {
|
|
163
|
+
get: (url, params, opts) => request('GET', url, params, opts),
|
|
164
|
+
post: (url, data, opts) => request('POST', url, data, opts),
|
|
165
|
+
put: (url, data, opts) => request('PUT', url, data, opts),
|
|
166
|
+
patch: (url, data, opts) => request('PATCH', url, data, opts),
|
|
167
|
+
delete: (url, data, opts) => request('DELETE', url, data, opts),
|
|
168
|
+
head: (url, opts) => request('HEAD', url, undefined, opts),
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Configure defaults
|
|
172
|
+
*/
|
|
173
|
+
configure(opts) {
|
|
174
|
+
if (opts.baseURL !== undefined) _config.baseURL = opts.baseURL;
|
|
175
|
+
if (opts.headers) Object.assign(_config.headers, opts.headers);
|
|
176
|
+
if (opts.timeout !== undefined) _config.timeout = opts.timeout;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read-only snapshot of current configuration
|
|
181
|
+
*/
|
|
182
|
+
getConfig() {
|
|
183
|
+
return {
|
|
184
|
+
baseURL: _config.baseURL,
|
|
185
|
+
headers: { ..._config.headers },
|
|
186
|
+
timeout: _config.timeout,
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Add request interceptor
|
|
192
|
+
* @param {Function} fn - (fetchOpts, url) → void | false | { url, options }
|
|
193
|
+
* @returns {Function} unsubscribe function
|
|
194
|
+
*/
|
|
195
|
+
onRequest(fn) {
|
|
196
|
+
_interceptors.request.push(fn);
|
|
197
|
+
return () => {
|
|
198
|
+
const idx = _interceptors.request.indexOf(fn);
|
|
199
|
+
if (idx !== -1) _interceptors.request.splice(idx, 1);
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Add response interceptor
|
|
205
|
+
* @param {Function} fn - (result) → void
|
|
206
|
+
* @returns {Function} unsubscribe function
|
|
207
|
+
*/
|
|
208
|
+
onResponse(fn) {
|
|
209
|
+
_interceptors.response.push(fn);
|
|
210
|
+
return () => {
|
|
211
|
+
const idx = _interceptors.response.indexOf(fn);
|
|
212
|
+
if (idx !== -1) _interceptors.response.splice(idx, 1);
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clear interceptors - all, or just 'request' / 'response'
|
|
218
|
+
*/
|
|
219
|
+
clearInterceptors(type) {
|
|
220
|
+
if (!type || type === 'request') _interceptors.request.length = 0;
|
|
221
|
+
if (!type || type === 'response') _interceptors.response.length = 0;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run multiple requests in parallel
|
|
226
|
+
*/
|
|
227
|
+
all(requests) {
|
|
228
|
+
return Promise.all(requests);
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create a standalone AbortController for manual cancellation
|
|
233
|
+
*/
|
|
234
|
+
createAbort() {
|
|
235
|
+
return new AbortController();
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Raw fetch pass-through (for edge cases)
|
|
240
|
+
*/
|
|
241
|
+
raw: (url, opts) => fetch(url, opts),
|
|
242
|
+
};
|
package/src/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "type": "module" }
|
|
1
|
+
{ "type": "module" }
|