pulse-js-framework 1.7.8 → 1.7.9

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.
Files changed (2) hide show
  1. package/package.json +7 -2
  2. package/runtime/http.js +837 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.8",
3
+ "version": "1.7.9",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -64,6 +64,10 @@
64
64
  "types": "./types/form.d.ts",
65
65
  "default": "./runtime/form.js"
66
66
  },
67
+ "./runtime/http": {
68
+ "types": "./types/http.d.ts",
69
+ "default": "./runtime/http.js"
70
+ },
67
71
  "./runtime/devtools": "./runtime/devtools.js",
68
72
  "./compiler": {
69
73
  "types": "./types/index.d.ts",
@@ -93,7 +97,7 @@
93
97
  "LICENSE"
94
98
  ],
95
99
  "scripts": {
96
- "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:devtools && npm run test:native",
100
+ "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native",
97
101
  "test:compiler": "node test/compiler.test.js",
98
102
  "test:sourcemap": "node test/sourcemap.test.js",
99
103
  "test:pulse": "node test/pulse.test.js",
@@ -110,6 +114,7 @@
110
114
  "test:docs": "node test/docs.test.js",
111
115
  "test:async": "node test/async.test.js",
112
116
  "test:form": "node test/form.test.js",
117
+ "test:http": "node test/http.test.js",
113
118
  "test:devtools": "node test/devtools.test.js",
114
119
  "test:native": "node test/native.test.js",
115
120
  "build:netlify": "node scripts/build-netlify.js",
@@ -0,0 +1,837 @@
1
+ /**
2
+ * Pulse HTTP Client - Zero-dependency HTTP client for Pulse Framework
3
+ * @module pulse-js-framework/runtime/http
4
+ */
5
+
6
+ import { pulse, computed, batch } from './pulse.js';
7
+ import { useAsync, useResource } from './async.js';
8
+ import { RuntimeError, createErrorMessage, getDocsUrl } from './errors.js';
9
+
10
+ // ============================================================================
11
+ // HTTP Error Class
12
+ // ============================================================================
13
+
14
+ /**
15
+ * HTTP-specific error suggestions
16
+ */
17
+ const HTTP_SUGGESTIONS = {
18
+ TIMEOUT: 'Consider increasing the timeout or checking network conditions.',
19
+ NETWORK: 'Check internet connectivity and ensure the server is reachable.',
20
+ ABORT: 'Request was cancelled. This is usually intentional.',
21
+ HTTP_ERROR: 'Check the response status and server logs for details.',
22
+ PARSE_ERROR: 'The response could not be parsed. Check the Content-Type header.'
23
+ };
24
+
25
+ /**
26
+ * HTTP Error with request/response context
27
+ */
28
+ export class HttpError extends RuntimeError {
29
+ /**
30
+ * @param {string} message - Error message
31
+ * @param {Object} [options={}] - Error options
32
+ * @param {string} [options.code] - Error code (TIMEOUT, NETWORK, ABORT, HTTP_ERROR, PARSE_ERROR)
33
+ * @param {Object} [options.config] - Request configuration
34
+ * @param {Request} [options.request] - The Request object
35
+ * @param {Object} [options.response] - The HttpResponse object
36
+ */
37
+ constructor(message, options = {}) {
38
+ const code = options.code || 'HTTP_ERROR';
39
+ const formattedMessage = createErrorMessage({
40
+ code,
41
+ message,
42
+ context: options.context,
43
+ suggestion: options.suggestion || HTTP_SUGGESTIONS[code]
44
+ });
45
+
46
+ super(formattedMessage, { code });
47
+
48
+ this.name = 'HttpError';
49
+ this.config = options.config || null;
50
+ this.code = code;
51
+ this.request = options.request || null;
52
+ this.response = options.response || null;
53
+ this.status = options.response?.status || null;
54
+ this.isHttpError = true;
55
+ }
56
+
57
+ /**
58
+ * Check if an error is an HttpError
59
+ * @param {any} error - The error to check
60
+ * @returns {boolean} True if the error is an HttpError
61
+ */
62
+ static isHttpError(error) {
63
+ return error?.isHttpError === true;
64
+ }
65
+
66
+ /**
67
+ * Check if this is a timeout error
68
+ * @returns {boolean}
69
+ */
70
+ isTimeout() {
71
+ return this.code === 'TIMEOUT';
72
+ }
73
+
74
+ /**
75
+ * Check if this is a network error
76
+ * @returns {boolean}
77
+ */
78
+ isNetworkError() {
79
+ return this.code === 'NETWORK';
80
+ }
81
+
82
+ /**
83
+ * Check if this is an abort/cancellation error
84
+ * @returns {boolean}
85
+ */
86
+ isAborted() {
87
+ return this.code === 'ABORT';
88
+ }
89
+ }
90
+
91
+ // ============================================================================
92
+ // Interceptor Manager
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Manages request or response interceptors
97
+ */
98
+ class InterceptorManager {
99
+ #handlers = new Map();
100
+ #idCounter = 0;
101
+
102
+ /**
103
+ * Add an interceptor
104
+ * @param {Function} fulfilled - Function to run on success
105
+ * @param {Function} [rejected] - Function to run on error
106
+ * @returns {number} Interceptor ID (for removal)
107
+ */
108
+ use(fulfilled, rejected) {
109
+ const id = this.#idCounter++;
110
+ this.#handlers.set(id, { fulfilled, rejected });
111
+ return id;
112
+ }
113
+
114
+ /**
115
+ * Remove an interceptor by ID
116
+ * @param {number} id - The interceptor ID
117
+ */
118
+ eject(id) {
119
+ this.#handlers.delete(id);
120
+ }
121
+
122
+ /**
123
+ * Remove all interceptors
124
+ */
125
+ clear() {
126
+ this.#handlers.clear();
127
+ }
128
+
129
+ /**
130
+ * Iterate through handlers
131
+ * @yields {Object} Handler with fulfilled and rejected functions
132
+ */
133
+ *[Symbol.iterator]() {
134
+ for (const handler of this.#handlers.values()) {
135
+ yield handler;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Get the number of interceptors
141
+ * @returns {number}
142
+ */
143
+ get size() {
144
+ return this.#handlers.size;
145
+ }
146
+ }
147
+
148
+ // ============================================================================
149
+ // HTTP Client
150
+ // ============================================================================
151
+
152
+ /**
153
+ * Default configuration values
154
+ */
155
+ const DEFAULT_CONFIG = {
156
+ baseURL: '',
157
+ timeout: 10000,
158
+ headers: {},
159
+ withCredentials: false,
160
+ responseType: 'json',
161
+ validateStatus: (status) => status >= 200 && status < 300,
162
+ retries: 0,
163
+ retryDelay: 1000,
164
+ retryCondition: null
165
+ };
166
+
167
+ /**
168
+ * HTTP Client class
169
+ */
170
+ class HttpClient {
171
+ #config;
172
+ #requestInterceptors;
173
+ #responseInterceptors;
174
+
175
+ /**
176
+ * @param {Object} [config={}] - Default configuration
177
+ */
178
+ constructor(config = {}) {
179
+ this.#config = { ...DEFAULT_CONFIG, ...config };
180
+ this.#requestInterceptors = new InterceptorManager();
181
+ this.#responseInterceptors = new InterceptorManager();
182
+
183
+ // Public interceptors access
184
+ this.interceptors = {
185
+ request: this.#requestInterceptors,
186
+ response: this.#responseInterceptors
187
+ };
188
+
189
+ // Public defaults access
190
+ this.defaults = this.#config;
191
+ }
192
+
193
+ /**
194
+ * Build the full URL with base URL and query parameters
195
+ * @param {string} url - The URL path
196
+ * @param {Object} config - Request configuration
197
+ * @returns {string} Full URL
198
+ */
199
+ #buildURL(url, config) {
200
+ let fullURL = url;
201
+
202
+ // Prepend baseURL if url is relative
203
+ if (config.baseURL && !url.startsWith('http://') && !url.startsWith('https://')) {
204
+ fullURL = config.baseURL.replace(/\/+$/, '') + '/' + url.replace(/^\/+/, '');
205
+ }
206
+
207
+ // Add query parameters
208
+ if (config.params && Object.keys(config.params).length > 0) {
209
+ const searchParams = new URLSearchParams();
210
+ for (const [key, value] of Object.entries(config.params)) {
211
+ if (value !== undefined && value !== null) {
212
+ searchParams.append(key, String(value));
213
+ }
214
+ }
215
+ const separator = fullURL.includes('?') ? '&' : '?';
216
+ fullURL += separator + searchParams.toString();
217
+ }
218
+
219
+ return fullURL;
220
+ }
221
+
222
+ /**
223
+ * Merge configurations (defaults + request-specific)
224
+ * @param {Object} config - Request configuration
225
+ * @returns {Object} Merged configuration
226
+ */
227
+ #mergeConfig(config) {
228
+ return {
229
+ ...this.#config,
230
+ ...config,
231
+ headers: {
232
+ ...this.#config.headers,
233
+ ...config.headers
234
+ }
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Run request interceptors
240
+ * @param {Object} config - Request configuration
241
+ * @returns {Promise<Object>} Modified configuration
242
+ */
243
+ async #runRequestInterceptors(config) {
244
+ let currentConfig = config;
245
+
246
+ for (const { fulfilled, rejected } of this.#requestInterceptors) {
247
+ try {
248
+ if (fulfilled) {
249
+ currentConfig = await fulfilled(currentConfig);
250
+ }
251
+ } catch (error) {
252
+ if (rejected) {
253
+ currentConfig = await rejected(error);
254
+ } else {
255
+ throw error;
256
+ }
257
+ }
258
+ }
259
+
260
+ return currentConfig;
261
+ }
262
+
263
+ /**
264
+ * Run response interceptors
265
+ * @param {Object} response - HTTP response
266
+ * @returns {Promise<Object>} Modified response
267
+ */
268
+ async #runResponseInterceptors(response) {
269
+ let currentResponse = response;
270
+
271
+ for (const { fulfilled, rejected } of this.#responseInterceptors) {
272
+ try {
273
+ if (fulfilled) {
274
+ currentResponse = await fulfilled(currentResponse);
275
+ }
276
+ } catch (error) {
277
+ if (rejected) {
278
+ currentResponse = await rejected(error);
279
+ } else {
280
+ throw error;
281
+ }
282
+ }
283
+ }
284
+
285
+ return currentResponse;
286
+ }
287
+
288
+ /**
289
+ * Run error through response interceptors
290
+ * @param {Error} error - The error
291
+ * @returns {Promise} Rejected promise or transformed result
292
+ */
293
+ async #runErrorInterceptors(error) {
294
+ let currentError = error;
295
+
296
+ for (const { rejected } of this.#responseInterceptors) {
297
+ if (rejected) {
298
+ try {
299
+ return await rejected(currentError);
300
+ } catch (e) {
301
+ currentError = e;
302
+ }
303
+ }
304
+ }
305
+
306
+ throw currentError;
307
+ }
308
+
309
+ /**
310
+ * Parse response based on content type and responseType option
311
+ * @param {Response} response - Fetch Response object
312
+ * @param {Object} config - Request configuration
313
+ * @returns {Promise<any>} Parsed response data
314
+ */
315
+ async #parseResponse(response, config) {
316
+ const contentType = response.headers.get('content-type') || '';
317
+ const responseType = config.responseType || 'json';
318
+
319
+ try {
320
+ switch (responseType) {
321
+ case 'json':
322
+ // Auto-detect JSON
323
+ if (contentType.includes('application/json')) {
324
+ return await response.json();
325
+ }
326
+ // Try JSON parsing, fall back to text
327
+ const text = await response.text();
328
+ try {
329
+ return JSON.parse(text);
330
+ } catch {
331
+ return text;
332
+ }
333
+
334
+ case 'text':
335
+ return await response.text();
336
+
337
+ case 'blob':
338
+ return await response.blob();
339
+
340
+ case 'arrayBuffer':
341
+ return await response.arrayBuffer();
342
+
343
+ case 'formData':
344
+ return await response.formData();
345
+
346
+ default:
347
+ return await response.text();
348
+ }
349
+ } catch (parseError) {
350
+ throw new HttpError('Failed to parse response', {
351
+ code: 'PARSE_ERROR',
352
+ config,
353
+ context: `Expected ${responseType}, got ${contentType}`
354
+ });
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Execute request with timeout
360
+ * @param {string} url - Full URL
361
+ * @param {Object} fetchOptions - Fetch options
362
+ * @param {Object} config - Request configuration
363
+ * @returns {Promise<Response>} Fetch response
364
+ */
365
+ async #executeWithTimeout(url, fetchOptions, config) {
366
+ const controller = new AbortController();
367
+ const { signal } = controller;
368
+
369
+ // Merge with user-provided signal
370
+ if (config.signal) {
371
+ config.signal.addEventListener('abort', () => {
372
+ controller.abort();
373
+ });
374
+ // If already aborted, abort immediately
375
+ if (config.signal.aborted) {
376
+ controller.abort();
377
+ }
378
+ }
379
+
380
+ let timeoutId;
381
+
382
+ try {
383
+ const fetchPromise = fetch(url, { ...fetchOptions, signal });
384
+
385
+ // Add timeout if configured
386
+ if (config.timeout > 0) {
387
+ const timeoutPromise = new Promise((_, reject) => {
388
+ timeoutId = setTimeout(() => {
389
+ controller.abort();
390
+ reject(new HttpError(`Request timeout after ${config.timeout}ms`, {
391
+ code: 'TIMEOUT',
392
+ config
393
+ }));
394
+ }, config.timeout);
395
+ });
396
+
397
+ return await Promise.race([fetchPromise, timeoutPromise]);
398
+ }
399
+
400
+ return await fetchPromise;
401
+ } catch (error) {
402
+ // Handle abort
403
+ if (error.name === 'AbortError') {
404
+ if (config.signal?.aborted) {
405
+ throw new HttpError('Request aborted', {
406
+ code: 'ABORT',
407
+ config
408
+ });
409
+ }
410
+ // Re-throw timeout errors
411
+ if (HttpError.isHttpError(error)) {
412
+ throw error;
413
+ }
414
+ throw new HttpError('Request aborted', {
415
+ code: 'ABORT',
416
+ config
417
+ });
418
+ }
419
+
420
+ // Handle network errors
421
+ if (error instanceof TypeError) {
422
+ throw new HttpError(error.message || 'Network error', {
423
+ code: 'NETWORK',
424
+ config,
425
+ context: 'Failed to connect to server'
426
+ });
427
+ }
428
+
429
+ throw error;
430
+ } finally {
431
+ if (timeoutId) {
432
+ clearTimeout(timeoutId);
433
+ }
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Default retry condition
439
+ * @param {HttpError} error - The error
440
+ * @returns {boolean} Whether to retry
441
+ */
442
+ #defaultRetryCondition(error) {
443
+ // Retry on network errors and timeouts
444
+ if (error.code === 'NETWORK' || error.code === 'TIMEOUT') {
445
+ return true;
446
+ }
447
+ // Retry on 5xx server errors
448
+ if (error.status >= 500) {
449
+ return true;
450
+ }
451
+ // Retry on 429 (rate limit)
452
+ if (error.status === 429) {
453
+ return true;
454
+ }
455
+ return false;
456
+ }
457
+
458
+ /**
459
+ * Execute request with retry logic
460
+ * @param {string} url - Full URL
461
+ * @param {Object} fetchOptions - Fetch options
462
+ * @param {Object} config - Request configuration
463
+ * @returns {Promise<Object>} HTTP response
464
+ */
465
+ async #executeWithRetry(url, fetchOptions, config) {
466
+ const { retries = 0, retryDelay = 1000, retryCondition } = config;
467
+ let lastError;
468
+ let attempt = 0;
469
+
470
+ while (attempt <= retries) {
471
+ try {
472
+ const response = await this.#executeWithTimeout(url, fetchOptions, config);
473
+
474
+ // Validate status
475
+ if (!config.validateStatus(response.status)) {
476
+ const data = await this.#parseResponse(response.clone(), config).catch(() => null);
477
+ throw new HttpError(`Request failed with status ${response.status}`, {
478
+ code: 'HTTP_ERROR',
479
+ config,
480
+ response: {
481
+ data,
482
+ status: response.status,
483
+ statusText: response.statusText,
484
+ headers: response.headers
485
+ }
486
+ });
487
+ }
488
+
489
+ // Parse and return response
490
+ const data = await this.#parseResponse(response, config);
491
+
492
+ return {
493
+ data,
494
+ status: response.status,
495
+ statusText: response.statusText,
496
+ headers: response.headers,
497
+ config
498
+ };
499
+ } catch (error) {
500
+ lastError = HttpError.isHttpError(error)
501
+ ? error
502
+ : new HttpError(error.message, { code: 'NETWORK', config });
503
+
504
+ attempt++;
505
+
506
+ // Check if should retry
507
+ const shouldRetry = attempt <= retries && (
508
+ retryCondition
509
+ ? retryCondition(lastError)
510
+ : this.#defaultRetryCondition(lastError)
511
+ );
512
+
513
+ if (shouldRetry) {
514
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
515
+ continue;
516
+ }
517
+
518
+ throw lastError;
519
+ }
520
+ }
521
+
522
+ throw lastError;
523
+ }
524
+
525
+ /**
526
+ * Make an HTTP request
527
+ * @param {Object} config - Request configuration
528
+ * @param {string} config.url - Request URL
529
+ * @param {string} [config.method='GET'] - HTTP method
530
+ * @param {Object} [config.headers] - Request headers
531
+ * @param {any} [config.data] - Request body
532
+ * @param {Object} [config.params] - URL query parameters
533
+ * @returns {Promise<Object>} HTTP response
534
+ */
535
+ async request(config) {
536
+ // Merge with defaults
537
+ let mergedConfig = this.#mergeConfig(config);
538
+
539
+ // Run request interceptors
540
+ mergedConfig = await this.#runRequestInterceptors(mergedConfig);
541
+
542
+ // Build URL
543
+ const url = this.#buildURL(mergedConfig.url, mergedConfig);
544
+
545
+ // Prepare fetch options
546
+ const fetchOptions = {
547
+ method: (mergedConfig.method || 'GET').toUpperCase(),
548
+ headers: new Headers(mergedConfig.headers),
549
+ credentials: mergedConfig.withCredentials ? 'include' : 'same-origin'
550
+ };
551
+
552
+ // Add body for methods that support it
553
+ if (mergedConfig.data !== undefined && !['GET', 'HEAD'].includes(fetchOptions.method)) {
554
+ if (mergedConfig.data instanceof FormData ||
555
+ mergedConfig.data instanceof URLSearchParams ||
556
+ mergedConfig.data instanceof Blob) {
557
+ fetchOptions.body = mergedConfig.data;
558
+ } else if (typeof mergedConfig.data === 'object') {
559
+ fetchOptions.body = JSON.stringify(mergedConfig.data);
560
+ // Set Content-Type if not already set
561
+ if (!fetchOptions.headers.has('Content-Type')) {
562
+ fetchOptions.headers.set('Content-Type', 'application/json');
563
+ }
564
+ } else {
565
+ fetchOptions.body = mergedConfig.data;
566
+ }
567
+ }
568
+
569
+ try {
570
+ // Execute with retry
571
+ const response = await this.#executeWithRetry(url, fetchOptions, mergedConfig);
572
+
573
+ // Run response interceptors
574
+ return await this.#runResponseInterceptors(response);
575
+ } catch (error) {
576
+ // Run error through interceptors
577
+ return await this.#runErrorInterceptors(error);
578
+ }
579
+ }
580
+
581
+ /**
582
+ * GET request
583
+ * @param {string} url - Request URL
584
+ * @param {Object} [options] - Request options
585
+ * @returns {Promise<Object>} HTTP response
586
+ */
587
+ get(url, options = {}) {
588
+ return this.request({ ...options, url, method: 'GET' });
589
+ }
590
+
591
+ /**
592
+ * DELETE request
593
+ * @param {string} url - Request URL
594
+ * @param {Object} [options] - Request options
595
+ * @returns {Promise<Object>} HTTP response
596
+ */
597
+ delete(url, options = {}) {
598
+ return this.request({ ...options, url, method: 'DELETE' });
599
+ }
600
+
601
+ /**
602
+ * HEAD request
603
+ * @param {string} url - Request URL
604
+ * @param {Object} [options] - Request options
605
+ * @returns {Promise<Object>} HTTP response
606
+ */
607
+ head(url, options = {}) {
608
+ return this.request({ ...options, url, method: 'HEAD' });
609
+ }
610
+
611
+ /**
612
+ * OPTIONS request
613
+ * @param {string} url - Request URL
614
+ * @param {Object} [options] - Request options
615
+ * @returns {Promise<Object>} HTTP response
616
+ */
617
+ options(url, options = {}) {
618
+ return this.request({ ...options, url, method: 'OPTIONS' });
619
+ }
620
+
621
+ /**
622
+ * POST request
623
+ * @param {string} url - Request URL
624
+ * @param {any} [data] - Request body
625
+ * @param {Object} [options] - Request options
626
+ * @returns {Promise<Object>} HTTP response
627
+ */
628
+ post(url, data, options = {}) {
629
+ return this.request({ ...options, url, method: 'POST', data });
630
+ }
631
+
632
+ /**
633
+ * PUT request
634
+ * @param {string} url - Request URL
635
+ * @param {any} [data] - Request body
636
+ * @param {Object} [options] - Request options
637
+ * @returns {Promise<Object>} HTTP response
638
+ */
639
+ put(url, data, options = {}) {
640
+ return this.request({ ...options, url, method: 'PUT', data });
641
+ }
642
+
643
+ /**
644
+ * PATCH request
645
+ * @param {string} url - Request URL
646
+ * @param {any} [data] - Request body
647
+ * @param {Object} [options] - Request options
648
+ * @returns {Promise<Object>} HTTP response
649
+ */
650
+ patch(url, data, options = {}) {
651
+ return this.request({ ...options, url, method: 'PATCH', data });
652
+ }
653
+
654
+ /**
655
+ * Create a new HttpClient instance with merged config
656
+ * @param {Object} [config={}] - Configuration to merge
657
+ * @returns {HttpClient} New client instance
658
+ */
659
+ create(config = {}) {
660
+ return new HttpClient(this.#mergeConfig(config));
661
+ }
662
+
663
+ /**
664
+ * Get the full URI for a request config
665
+ * @param {Object} config - Request configuration
666
+ * @returns {string} Full URL
667
+ */
668
+ getUri(config) {
669
+ const mergedConfig = this.#mergeConfig(config);
670
+ return this.#buildURL(mergedConfig.url || '', mergedConfig);
671
+ }
672
+
673
+ /**
674
+ * Check if an error was caused by request cancellation
675
+ * @param {any} error - The error to check
676
+ * @returns {boolean} True if the error is a cancellation
677
+ */
678
+ isCancel(error) {
679
+ return HttpError.isHttpError(error) && error.code === 'ABORT';
680
+ }
681
+ }
682
+
683
+ // ============================================================================
684
+ // Factory Function
685
+ // ============================================================================
686
+
687
+ /**
688
+ * Create a new HTTP client instance
689
+ * @param {Object} [config={}] - Default configuration
690
+ * @param {string} [config.baseURL] - Base URL for all requests
691
+ * @param {number} [config.timeout=10000] - Request timeout in ms
692
+ * @param {Object} [config.headers] - Default headers
693
+ * @param {boolean} [config.withCredentials=false] - Include credentials
694
+ * @param {string} [config.responseType='json'] - Response type (json, text, blob, arrayBuffer)
695
+ * @param {Function} [config.validateStatus] - Function to validate response status
696
+ * @param {number} [config.retries=0] - Number of retry attempts
697
+ * @param {number} [config.retryDelay=1000] - Delay between retries in ms
698
+ * @param {Function} [config.retryCondition] - Custom retry condition function
699
+ * @returns {HttpClient} HTTP client instance
700
+ *
701
+ * @example
702
+ * const api = createHttp({
703
+ * baseURL: 'https://api.example.com',
704
+ * timeout: 5000,
705
+ * headers: { 'Authorization': 'Bearer token' }
706
+ * });
707
+ *
708
+ * const users = await api.get('/users');
709
+ * const user = await api.post('/users', { name: 'John' });
710
+ */
711
+ export function createHttp(config = {}) {
712
+ return new HttpClient(config);
713
+ }
714
+
715
+ // ============================================================================
716
+ // Reactive Integration
717
+ // ============================================================================
718
+
719
+ /**
720
+ * Reactive HTTP hook integrating with Pulse's useAsync
721
+ * @param {Function} requestFn - Function that returns a promise (from http.get, etc.)
722
+ * @param {Object} [options={}] - Hook options
723
+ * @param {boolean} [options.immediate=true] - Execute immediately
724
+ * @param {any} [options.initialData=null] - Initial data value
725
+ * @param {number} [options.retries=0] - Retry attempts
726
+ * @param {number} [options.retryDelay=1000] - Delay between retries
727
+ * @param {Function} [options.onSuccess] - Success callback
728
+ * @param {Function} [options.onError] - Error callback
729
+ * @returns {Object} Reactive state and controls
730
+ *
731
+ * @example
732
+ * const { data, loading, error, execute } = useHttp(
733
+ * () => api.get('/users'),
734
+ * { immediate: true, retries: 3 }
735
+ * );
736
+ *
737
+ * effect(() => {
738
+ * if (data.get()) console.log('Users:', data.get());
739
+ * });
740
+ */
741
+ export function useHttp(requestFn, options = {}) {
742
+ const {
743
+ immediate = true,
744
+ initialData = null,
745
+ retries = 0,
746
+ retryDelay = 1000,
747
+ onSuccess,
748
+ onError
749
+ } = options;
750
+
751
+ const asyncState = useAsync(
752
+ async (...args) => {
753
+ const response = await requestFn(...args);
754
+ return response;
755
+ },
756
+ {
757
+ immediate,
758
+ initialData: initialData !== null ? { data: initialData } : null,
759
+ retries,
760
+ retryDelay,
761
+ onSuccess: onSuccess ? (response) => onSuccess(response) : undefined,
762
+ onError: onError ? (error) => {
763
+ if (HttpError.isHttpError(error)) {
764
+ onError(error);
765
+ } else {
766
+ onError(new HttpError(error.message, { code: 'NETWORK' }));
767
+ }
768
+ } : undefined
769
+ }
770
+ );
771
+
772
+ // Convenience accessor for response data
773
+ const data = computed(() => asyncState.data.get()?.data ?? initialData);
774
+ const response = asyncState.data;
775
+
776
+ return {
777
+ data,
778
+ response,
779
+ loading: asyncState.loading,
780
+ error: asyncState.error,
781
+ status: asyncState.status,
782
+ execute: asyncState.execute,
783
+ reset: asyncState.reset,
784
+ abort: asyncState.abort
785
+ };
786
+ }
787
+
788
+ /**
789
+ * HTTP resource with caching (SWR pattern)
790
+ * Integrates with useResource from async.js
791
+ * @param {string|Function} key - Cache key or function returning key
792
+ * @param {Function} requestFn - Function that returns a promise
793
+ * @param {Object} [options={}] - Resource options
794
+ * @returns {Object} Resource state and controls
795
+ *
796
+ * @example
797
+ * const users = useHttpResource(
798
+ * 'users',
799
+ * () => api.get('/users'),
800
+ * { refreshInterval: 30000 }
801
+ * );
802
+ */
803
+ export function useHttpResource(key, requestFn, options = {}) {
804
+ return useResource(
805
+ key,
806
+ async () => {
807
+ const response = await requestFn();
808
+ return response.data;
809
+ },
810
+ options
811
+ );
812
+ }
813
+
814
+ // ============================================================================
815
+ // Default Instance
816
+ // ============================================================================
817
+
818
+ /**
819
+ * Pre-configured default HTTP client instance
820
+ */
821
+ export const http = createHttp();
822
+
823
+ // ============================================================================
824
+ // Exports
825
+ // ============================================================================
826
+
827
+ export { HttpClient, InterceptorManager };
828
+
829
+ export default {
830
+ createHttp,
831
+ http,
832
+ HttpClient,
833
+ HttpError,
834
+ InterceptorManager,
835
+ useHttp,
836
+ useHttpResource
837
+ };