@zogjs/http 0.4.8

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 ADDED
@@ -0,0 +1,850 @@
1
+ # ZogHttp Plugin v1.0.0
2
+
3
+ A powerful, production-ready HTTP client plugin for Zog.js with full support for all HTTP methods, file uploads with progress tracking, request/response interceptors, and reactive state management.
4
+
5
+ ## Features
6
+
7
+ - ✅ All HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
8
+ - ✅ Request/Response interceptors (before/after hooks)
9
+ - ✅ File upload with real-time progress tracking
10
+ - ✅ File download with progress tracking
11
+ - ✅ Authentication token management (Bearer, Basic)
12
+ - ✅ Custom headers support
13
+ - ✅ Automatic JSON parsing
14
+ - ✅ Request cancellation via AbortController
15
+ - ✅ Reactive loading/error states
16
+ - ✅ Timeout configuration
17
+ - ✅ Base URL configuration
18
+ - ✅ Automatic retry mechanism
19
+ - ✅ TypeScript-friendly API
20
+
21
+ ## Installation
22
+
23
+ ```html
24
+ <script type="module">
25
+ import { createApp } from 'zogjs';
26
+ import { ZogHttpPlugin } from '@zogjs/http';
27
+
28
+ createApp(() => ({
29
+ // your data
30
+ }))
31
+ .use(ZogHttpPlugin, {
32
+ baseURL: 'https://api.example.com',
33
+ timeout: 30000,
34
+ })
35
+ .mount('#app');
36
+ </script>
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```html
42
+ <div id="app">
43
+ <div z-if="$http.state.loading">Loading...</div>
44
+ <div z-if="$http.state.error">{{ $http.state.error }}</div>
45
+
46
+ <ul>
47
+ <li z-for="user in users" :key="user.id">{{ user.name }}</li>
48
+ </ul>
49
+
50
+ <button @click="loadUsers">Load Users</button>
51
+ </div>
52
+
53
+ <script type="module">
54
+ import { createApp, reactive } from './zog.js';
55
+ import { ZogHttpPlugin } from './zog-http.js';
56
+
57
+ createApp(() => {
58
+ const users = reactive([]);
59
+
60
+ async function loadUsers() {
61
+ try {
62
+ const response = await this.$http.get('/users');
63
+ users.splice(0, users.length, ...response.data);
64
+ } catch (error) {
65
+ console.error('Failed to load users:', error);
66
+ }
67
+ }
68
+
69
+ return { users, loadUsers };
70
+ })
71
+ .use(ZogHttpPlugin, { baseURL: 'https://api.example.com' })
72
+ .mount('#app');
73
+ </script>
74
+ ```
75
+
76
+ ## Configuration Options
77
+
78
+ ```javascript
79
+ .use(ZogHttpPlugin, {
80
+ // Base URL for all requests
81
+ baseURL: 'https://api.example.com',
82
+
83
+ // Default timeout in milliseconds
84
+ timeout: 30000,
85
+
86
+ // Default headers for all requests
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'X-Custom-Header': 'value',
90
+ },
91
+
92
+ // Include credentials (cookies) in requests
93
+ withCredentials: false,
94
+
95
+ // Number of automatic retries on failure
96
+ retries: 0,
97
+
98
+ // Delay between retries in milliseconds
99
+ retryDelay: 1000,
100
+ })
101
+ ```
102
+
103
+ ## API Reference
104
+
105
+ ### HTTP Methods
106
+
107
+ All methods return a Promise that resolves to a response object:
108
+
109
+ ```javascript
110
+ // Response object structure
111
+ {
112
+ data: any, // Parsed response body
113
+ status: number, // HTTP status code
114
+ statusText: string, // HTTP status text
115
+ headers: object, // Response headers
116
+ config: object, // Request configuration
117
+ request: object, // Original request info
118
+ }
119
+ ```
120
+
121
+ #### GET Request
122
+
123
+ ```javascript
124
+ // Simple GET
125
+ const response = await this.$http.get('/users');
126
+
127
+ // GET with query parameters
128
+ const response = await this.$http.get('/users', {
129
+ params: { page: 1, limit: 10, status: 'active' }
130
+ });
131
+
132
+ // GET with custom headers
133
+ const response = await this.$http.get('/users', {
134
+ headers: { 'X-Request-ID': '123' }
135
+ });
136
+ ```
137
+
138
+ #### POST Request
139
+
140
+ ```javascript
141
+ // Simple POST
142
+ const response = await this.$http.post('/users', {
143
+ name: 'John Doe',
144
+ email: 'john@example.com'
145
+ });
146
+
147
+ // POST with additional options
148
+ const response = await this.$http.post('/users',
149
+ { name: 'John' },
150
+ { headers: { 'X-Custom': 'value' } }
151
+ );
152
+ ```
153
+
154
+ #### PUT Request
155
+
156
+ ```javascript
157
+ const response = await this.$http.put('/users/123', {
158
+ name: 'Jane Doe',
159
+ email: 'jane@example.com'
160
+ });
161
+ ```
162
+
163
+ #### PATCH Request
164
+
165
+ ```javascript
166
+ const response = await this.$http.patch('/users/123', {
167
+ name: 'Updated Name'
168
+ });
169
+ ```
170
+
171
+ #### DELETE Request
172
+
173
+ ```javascript
174
+ const response = await this.$http.delete('/users/123');
175
+
176
+ // DELETE with body
177
+ const response = await this.$http.delete('/users', {
178
+ body: { ids: [1, 2, 3] }
179
+ });
180
+ ```
181
+
182
+ #### HEAD & OPTIONS
183
+
184
+ ```javascript
185
+ const headResponse = await this.$http.head('/users');
186
+ const optionsResponse = await this.$http.options('/users');
187
+ ```
188
+
189
+ ### Shorthand Methods
190
+
191
+ For convenience, shorthand methods are also injected into scope:
192
+
193
+ ```javascript
194
+ await this.$get('/users');
195
+ await this.$post('/users', data);
196
+ await this.$put('/users/1', data);
197
+ await this.$patch('/users/1', data);
198
+ await this.$delete('/users/1');
199
+ ```
200
+
201
+ ### Authentication
202
+
203
+ #### Bearer Token
204
+
205
+ ```javascript
206
+ // Set token
207
+ this.$http.setAuthToken('your-jwt-token');
208
+
209
+ // Clear token
210
+ this.$http.clearAuth();
211
+
212
+ // Or set directly
213
+ this.$http.setHeader('Authorization', 'Bearer your-token');
214
+ ```
215
+
216
+ #### Basic Authentication
217
+
218
+ ```javascript
219
+ this.$http.setBasicAuth('username', 'password');
220
+ ```
221
+
222
+ ### Headers Management
223
+
224
+ ```javascript
225
+ // Set single header
226
+ this.$http.setHeader('X-API-Key', 'your-api-key');
227
+
228
+ // Set multiple headers
229
+ this.$http.setHeaders({
230
+ 'X-API-Key': 'key',
231
+ 'X-Client-Version': '1.0.0'
232
+ });
233
+
234
+ // Remove header
235
+ this.$http.removeHeader('X-API-Key');
236
+ ```
237
+
238
+ ### Base URL
239
+
240
+ ```javascript
241
+ // Change base URL at runtime
242
+ this.$http.setBaseURL('https://api.newdomain.com');
243
+ ```
244
+
245
+ ### Timeout
246
+
247
+ ```javascript
248
+ // Set global timeout
249
+ this.$http.setTimeout(60000); // 60 seconds
250
+
251
+ // Per-request timeout
252
+ await this.$http.get('/slow-endpoint', { timeout: 120000 });
253
+ ```
254
+
255
+ ## Interceptors
256
+
257
+ Interceptors allow you to run code before requests are sent and after responses are received.
258
+
259
+ ### Request Interceptors
260
+
261
+ ```javascript
262
+ // Add request interceptor
263
+ const interceptorId = this.$http.addRequestInterceptor(
264
+ // Success handler (called before each request)
265
+ async (config) => {
266
+ // Add timestamp to all requests
267
+ config.headers['X-Request-Time'] = Date.now();
268
+
269
+ // Add token from storage
270
+ const token = localStorage.getItem('token');
271
+ if (token) {
272
+ config.headers['Authorization'] = `Bearer ${token}`;
273
+ }
274
+
275
+ console.log('Request:', config.method, config.url);
276
+ return config; // Must return config
277
+ },
278
+ // Error handler (optional)
279
+ async (error) => {
280
+ console.error('Request error:', error);
281
+ throw error;
282
+ }
283
+ );
284
+
285
+ // Remove interceptor later
286
+ this.$http.removeRequestInterceptor(interceptorId);
287
+ ```
288
+
289
+ ### Response Interceptors
290
+
291
+ ```javascript
292
+ // Add response interceptor
293
+ const interceptorId = this.$http.addResponseInterceptor(
294
+ // Success handler
295
+ async (response) => {
296
+ console.log('Response:', response.status, response.data);
297
+
298
+ // Transform response data
299
+ if (response.data?.items) {
300
+ response.data = response.data.items;
301
+ }
302
+
303
+ return response;
304
+ },
305
+ // Error handler
306
+ async (error) => {
307
+ if (error.status === 401) {
308
+ // Handle unauthorized - redirect to login
309
+ window.location.href = '/login';
310
+ }
311
+
312
+ if (error.status === 429) {
313
+ // Handle rate limiting - wait and retry
314
+ await new Promise(r => setTimeout(r, 5000));
315
+ return this.$http.request(error.request);
316
+ }
317
+
318
+ throw error;
319
+ }
320
+ );
321
+ ```
322
+
323
+ ### Clear All Interceptors
324
+
325
+ ```javascript
326
+ this.$http.clearInterceptors();
327
+ ```
328
+
329
+ ## File Upload
330
+
331
+ The upload method provides real-time progress tracking and supports single/multiple files.
332
+
333
+ ### Basic Upload
334
+
335
+ ```javascript
336
+ // Single file upload
337
+ const fileInput = document.querySelector('input[type="file"]');
338
+ const file = fileInput.files[0];
339
+
340
+ const { promise, tracker, abort } = this.$http.upload('/upload', file);
341
+
342
+ // Access reactive progress state
343
+ console.log(tracker.progress); // 0-100
344
+ console.log(tracker.status); // 'idle' | 'uploading' | 'completed' | 'error'
345
+
346
+ const response = await promise;
347
+ ```
348
+
349
+ ### Upload with Progress Callbacks
350
+
351
+ ```javascript
352
+ const { promise, tracker, abort } = this.$http.upload('/upload', file, {
353
+ // Field name for the file
354
+ fieldName: 'document',
355
+
356
+ // Additional form data
357
+ additionalData: {
358
+ description: 'My file',
359
+ category: 'documents'
360
+ },
361
+
362
+ // Custom headers
363
+ headers: {
364
+ 'X-Upload-ID': 'unique-id'
365
+ },
366
+
367
+ // Progress callback
368
+ onProgress: (info) => {
369
+ console.log(`Progress: ${info.progress}%`);
370
+ console.log(`Speed: ${(info.speed / 1024).toFixed(2)} KB/s`);
371
+ console.log(`Remaining: ${info.remainingTime}s`);
372
+ },
373
+
374
+ // Completion callback
375
+ onComplete: (response) => {
376
+ console.log('Upload complete!', response.data);
377
+ },
378
+
379
+ // Error callback
380
+ onError: (error) => {
381
+ console.error('Upload failed:', error.message);
382
+ }
383
+ });
384
+ ```
385
+
386
+ ### Multiple Files Upload
387
+
388
+ ```javascript
389
+ const files = document.querySelector('input[type="file"]').files;
390
+
391
+ const { promise, tracker } = this.$http.upload('/upload', Array.from(files), {
392
+ fieldName: 'files',
393
+ additionalData: { albumId: '123' }
394
+ });
395
+ ```
396
+
397
+ ### Upload with FormData
398
+
399
+ ```javascript
400
+ const formData = new FormData();
401
+ formData.append('file', file);
402
+ formData.append('name', 'custom name');
403
+
404
+ const { promise } = this.$http.upload('/upload', formData);
405
+ ```
406
+
407
+ ### Cancel Upload
408
+
409
+ ```javascript
410
+ const { promise, abort, requestId } = this.$http.upload('/upload', file);
411
+
412
+ // Cancel by abort function
413
+ abort();
414
+
415
+ // Or cancel by request ID
416
+ this.$http.cancelRequest(requestId);
417
+ ```
418
+
419
+ ### Reactive Upload State in Template
420
+
421
+ ```html
422
+ <div id="app">
423
+ <input type="file" @change="handleFileSelect" />
424
+
425
+ <div z-if="uploadState.status === 'uploading'">
426
+ <div class="progress-bar">
427
+ <div :style="{ width: uploadState.progress + '%' }"></div>
428
+ </div>
429
+ <p>{{ uploadState.progress }}% - {{ formatSpeed(uploadState.speed) }}</p>
430
+ <p>Remaining: {{ uploadState.remainingTime }}s</p>
431
+ <button @click="cancelUpload">Cancel</button>
432
+ </div>
433
+
434
+ <div z-if="uploadState.status === 'completed'">
435
+ Upload complete! ✓
436
+ </div>
437
+
438
+ <div z-if="uploadState.status === 'error'">
439
+ Error: {{ uploadState.error }}
440
+ </div>
441
+ </div>
442
+
443
+ <script type="module">
444
+ createApp(() => {
445
+ const uploadState = reactive({
446
+ progress: 0,
447
+ status: 'idle',
448
+ speed: 0,
449
+ remainingTime: 0,
450
+ error: null
451
+ });
452
+
453
+ let abortFn = null;
454
+
455
+ async function handleFileSelect(e) {
456
+ const file = e.target.files[0];
457
+ if (!file) return;
458
+
459
+ const { promise, tracker, abort } = this.$upload('/upload', file);
460
+
461
+ // Sync tracker state with local state
462
+ Object.assign(uploadState, tracker);
463
+ abortFn = abort;
464
+
465
+ try {
466
+ await promise;
467
+ } catch (err) {
468
+ // Error already handled via tracker
469
+ }
470
+ }
471
+
472
+ function cancelUpload() {
473
+ if (abortFn) abortFn();
474
+ }
475
+
476
+ function formatSpeed(bytesPerSec) {
477
+ return (bytesPerSec / 1024).toFixed(2) + ' KB/s';
478
+ }
479
+
480
+ return { uploadState, handleFileSelect, cancelUpload, formatSpeed };
481
+ }).use(ZogHttpPlugin).mount('#app');
482
+ </script>
483
+ ```
484
+
485
+ ## File Download
486
+
487
+ Download files with progress tracking and optional auto-save.
488
+
489
+ ### Basic Download
490
+
491
+ ```javascript
492
+ const { promise, tracker, abort } = this.$http.download('/files/report.pdf', {
493
+ filename: 'report.pdf', // Auto-triggers download
494
+
495
+ onProgress: (info) => {
496
+ console.log(`Downloaded: ${info.progress}%`);
497
+ },
498
+
499
+ onComplete: ({ blob, filename, size }) => {
500
+ console.log(`Downloaded ${filename} (${size} bytes)`);
501
+ }
502
+ });
503
+
504
+ const { blob } = await promise;
505
+ ```
506
+
507
+ ### Download without Auto-Save
508
+
509
+ ```javascript
510
+ const { promise } = this.$http.download('/files/image.jpg');
511
+ const { blob } = await promise;
512
+
513
+ // Process blob manually
514
+ const imageUrl = URL.createObjectURL(blob);
515
+ ```
516
+
517
+ ## Reactive State
518
+
519
+ The plugin provides reactive global state:
520
+
521
+ ```javascript
522
+ // Access in JavaScript
523
+ this.$http.state.loading // true when any request is pending
524
+ this.$http.state.error // Last error message
525
+ this.$http.state.pendingRequests // Number of pending requests
526
+ this.$http.state.lastRequest // Info about last request
527
+
528
+ // Access in template
529
+ <div z-show="$http.state.loading">Loading...</div>
530
+ <div z-if="$http.state.pendingRequests > 0">
531
+ {{ $http.state.pendingRequests }} requests in progress
532
+ </div>
533
+ ```
534
+
535
+ ## Request Cancellation
536
+
537
+ ### Cancel Single Request
538
+
539
+ ```javascript
540
+ const { requestId } = this.$http.upload('/upload', file);
541
+
542
+ // Cancel by ID
543
+ this.$http.cancelRequest(requestId);
544
+ ```
545
+
546
+ ### Cancel All Requests
547
+
548
+ ```javascript
549
+ // Cancel all pending requests
550
+ this.$http.cancelAll();
551
+ ```
552
+
553
+ ## Error Handling
554
+
555
+ ### HttpError Object
556
+
557
+ ```javascript
558
+ try {
559
+ await this.$http.get('/users');
560
+ } catch (error) {
561
+ if (error.isHttpError) {
562
+ console.log(error.status); // 404, 500, etc.
563
+ console.log(error.message); // Error message
564
+ console.log(error.response); // Full response object
565
+ console.log(error.request); // Original request info
566
+ }
567
+ }
568
+ ```
569
+
570
+ ### HTTP Status Codes
571
+
572
+ ```javascript
573
+ import { HttpStatus } from './zog-http.js';
574
+
575
+ if (error.status === HttpStatus.NOT_FOUND) {
576
+ // Handle 404
577
+ }
578
+
579
+ if (error.status === HttpStatus.UNAUTHORIZED) {
580
+ // Handle 401
581
+ }
582
+ ```
583
+
584
+ ## Creating Instances
585
+
586
+ Create multiple HTTP clients with different configurations:
587
+
588
+ ```javascript
589
+ // Create a new instance
590
+ const adminApi = this.$http.create({
591
+ baseURL: 'https://admin.api.com',
592
+ headers: { 'X-Admin-Key': 'secret' }
593
+ });
594
+
595
+ await adminApi.get('/dashboard');
596
+ ```
597
+
598
+ ## Standalone Usage
599
+
600
+ Use without Zog.js:
601
+
602
+ ```javascript
603
+ import { createHttpClient } from './zog-http.js';
604
+
605
+ const http = createHttpClient({
606
+ baseURL: 'https://api.example.com',
607
+ timeout: 30000
608
+ });
609
+
610
+ // Add interceptor
611
+ http.addRequestInterceptor(config => {
612
+ config.headers['Authorization'] = 'Bearer token';
613
+ return config;
614
+ });
615
+
616
+ // Make requests
617
+ const users = await http.get('/users');
618
+ ```
619
+
620
+ ## Complete Example
621
+
622
+ ```html
623
+ <!DOCTYPE html>
624
+ <html>
625
+ <head>
626
+ <title>ZogHttp Demo</title>
627
+ <style>
628
+ .loading { opacity: 0.5; pointer-events: none; }
629
+ .progress-bar { width: 100%; height: 20px; background: #eee; }
630
+ .progress-fill { height: 100%; background: #4caf50; transition: width 0.3s; }
631
+ .error { color: red; }
632
+ </style>
633
+ </head>
634
+ <body>
635
+ <div id="app">
636
+ <!-- Global loading indicator -->
637
+ <div z-show="$http.state.loading" class="loading-overlay">
638
+ Loading...
639
+ </div>
640
+
641
+ <!-- User list -->
642
+ <section>
643
+ <h2>Users</h2>
644
+ <button @click="fetchUsers" :disabled="$http.state.loading">
645
+ Refresh Users
646
+ </button>
647
+ <ul>
648
+ <li z-for="user in users" :key="user.id">
649
+ {{ user.name }} ({{ user.email }})
650
+ <button @click="deleteUser(user.id)">Delete</button>
651
+ </li>
652
+ </ul>
653
+ </section>
654
+
655
+ <!-- Add user form -->
656
+ <section>
657
+ <h2>Add User</h2>
658
+ <input z-model="newUser.name" placeholder="Name" />
659
+ <input z-model="newUser.email" placeholder="Email" />
660
+ <button @click="addUser">Add User</button>
661
+ </section>
662
+
663
+ <!-- File upload -->
664
+ <section>
665
+ <h2>Upload Avatar</h2>
666
+ <input type="file" @change="handleUpload" accept="image/*" />
667
+
668
+ <div z-if="upload.status === 'uploading'">
669
+ <div class="progress-bar">
670
+ <div class="progress-fill" :style="{ width: upload.progress + '%' }"></div>
671
+ </div>
672
+ <span>{{ upload.progress }}%</span>
673
+ <button @click="cancelUpload">Cancel</button>
674
+ </div>
675
+
676
+ <div z-if="upload.status === 'completed'" style="color: green;">
677
+ Upload complete! ✓
678
+ </div>
679
+
680
+ <div z-if="upload.status === 'error'" class="error">
681
+ {{ upload.error }}
682
+ </div>
683
+ </section>
684
+
685
+ <!-- Error display -->
686
+ <div z-if="error" class="error">
687
+ {{ error }}
688
+ </div>
689
+ </div>
690
+
691
+ <script type="module">
692
+ import { createApp, ref, reactive } from './zog.js';
693
+ import { ZogHttpPlugin } from './zog-http.js';
694
+
695
+ createApp(() => {
696
+ // Reactive data
697
+ const users = reactive([]);
698
+ const newUser = reactive({ name: '', email: '' });
699
+ const error = ref('');
700
+ const upload = reactive({
701
+ progress: 0,
702
+ status: 'idle',
703
+ error: null
704
+ });
705
+
706
+ let abortUpload = null;
707
+
708
+ // Fetch users
709
+ async function fetchUsers() {
710
+ try {
711
+ error.value = '';
712
+ const response = await this.$http.get('/users');
713
+ users.splice(0, users.length, ...response.data);
714
+ } catch (e) {
715
+ error.value = e.message;
716
+ }
717
+ }
718
+
719
+ // Add user
720
+ async function addUser() {
721
+ if (!newUser.name || !newUser.email) return;
722
+
723
+ try {
724
+ error.value = '';
725
+ const response = await this.$http.post('/users', {
726
+ name: newUser.name,
727
+ email: newUser.email
728
+ });
729
+ users.push(response.data);
730
+ newUser.name = '';
731
+ newUser.email = '';
732
+ } catch (e) {
733
+ error.value = e.message;
734
+ }
735
+ }
736
+
737
+ // Delete user
738
+ async function deleteUser(id) {
739
+ try {
740
+ error.value = '';
741
+ await this.$http.delete(`/users/${id}`);
742
+ const index = users.findIndex(u => u.id === id);
743
+ if (index > -1) users.splice(index, 1);
744
+ } catch (e) {
745
+ error.value = e.message;
746
+ }
747
+ }
748
+
749
+ // Handle file upload
750
+ function handleUpload(event) {
751
+ const file = event.target.files[0];
752
+ if (!file) return;
753
+
754
+ const { promise, tracker, abort } = this.$upload('/avatar', file, {
755
+ fieldName: 'avatar',
756
+ additionalData: { userId: 1 },
757
+ onProgress: (info) => {
758
+ upload.progress = info.progress;
759
+ },
760
+ onComplete: () => {
761
+ upload.status = 'completed';
762
+ },
763
+ onError: (err) => {
764
+ upload.status = 'error';
765
+ upload.error = err.message;
766
+ }
767
+ });
768
+
769
+ upload.status = 'uploading';
770
+ upload.progress = 0;
771
+ upload.error = null;
772
+ abortUpload = abort;
773
+ }
774
+
775
+ // Cancel upload
776
+ function cancelUpload() {
777
+ if (abortUpload) {
778
+ abortUpload();
779
+ upload.status = 'idle';
780
+ }
781
+ }
782
+
783
+ return {
784
+ users,
785
+ newUser,
786
+ error,
787
+ upload,
788
+ fetchUsers,
789
+ addUser,
790
+ deleteUser,
791
+ handleUpload,
792
+ cancelUpload
793
+ };
794
+ })
795
+ .use(ZogHttpPlugin, {
796
+ baseURL: 'https://jsonplaceholder.typicode.com',
797
+ timeout: 30000,
798
+ retries: 2,
799
+ retryDelay: 1000,
800
+ })
801
+ .mount('#app');
802
+ </script>
803
+ </body>
804
+ </html>
805
+ ```
806
+
807
+ ## TypeScript Support
808
+
809
+ The plugin is written in vanilla JavaScript but includes JSDoc comments for IDE support. For full TypeScript support, type definitions can be added:
810
+
811
+ ```typescript
812
+ interface HttpResponse<T = any> {
813
+ data: T;
814
+ status: number;
815
+ statusText: string;
816
+ headers: Record<string, string>;
817
+ config: RequestConfig;
818
+ request: RequestInfo;
819
+ }
820
+
821
+ interface UploadProgress {
822
+ loaded: number;
823
+ total: number;
824
+ progress: number;
825
+ speed: number;
826
+ remainingTime: number;
827
+ }
828
+
829
+ interface UploadOptions {
830
+ fieldName?: string;
831
+ additionalData?: Record<string, any>;
832
+ headers?: Record<string, string>;
833
+ onProgress?: (info: UploadProgress) => void;
834
+ onComplete?: (response: HttpResponse) => void;
835
+ onError?: (error: HttpError) => void;
836
+ }
837
+ ```
838
+
839
+ ## Browser Support
840
+
841
+ - Chrome 66+
842
+ - Firefox 57+
843
+ - Safari 11.1+
844
+ - Edge 79+
845
+
846
+ Requires: `fetch`, `AbortController`, `FormData`, `Blob`, `URL.createObjectURL`
847
+
848
+ ## License
849
+
850
+ MIT License - feel free to use in any project.
@@ -0,0 +1 @@
1
+ var t=Object.defineProperty,e=Object.defineProperties,s=Object.getOwnPropertyDescriptors,r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable,a=(e,s,r)=>s in e?t(e,s,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[s]=r,i=(t,e)=>{for(var s in e||(e={}))o.call(e,s)&&a(t,s,e[s]);if(r)for(var s of r(e))n.call(e,s)&&a(t,s,e[s]);return t},l=(t,r)=>e(t,s(r)),d=(t,e)=>{var s={};for(var a in t)o.call(t,a)&&e.indexOf(a)<0&&(s[a]=t[a]);if(null!=t&&r)for(var a of r(t))e.indexOf(a)<0&&n.call(t,a)&&(s[a]=t[a]);return s};const h={baseURL:"",timeout:3e4,headers:{"Content-Type":"application/json"},withCredentials:!1,retries:0,retryDelay:1e3},c={OK:200,CREATED:201,NO_CONTENT:204,BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,UNPROCESSABLE_ENTITY:422,INTERNAL_SERVER_ERROR:500,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503};class u extends Error{constructor(t,e,s,r){super(t),this.name="HttpError",this.status=e,this.response=s,this.request=r,this.isHttpError=!0}}class p{constructor(t){this.state=t({progress:0,loaded:0,total:0,status:"idle",error:null,startTime:null,speed:0,remainingTime:0})}start(t){this.state.progress=0,this.state.loaded=0,this.state.total=t,this.state.status="uploading",this.state.error=null,this.state.startTime=Date.now(),this.state.speed=0,this.state.remainingTime=0}update(t,e){this.state.loaded=t,this.state.total=e,this.state.progress=e>0?Math.round(t/e*100):0;const s=(Date.now()-this.state.startTime)/1e3;if(s>0){this.state.speed=Math.round(t/s);const r=e-t;this.state.remainingTime=this.state.speed>0?Math.round(r/this.state.speed):0}}complete(){this.state.progress=100,this.state.status="completed"}fail(t){this.state.status="error",this.state.error=t.message||"Upload failed"}reset(){this.state.progress=0,this.state.loaded=0,this.state.total=0,this.state.status="idle",this.state.error=null,this.state.startTime=null,this.state.speed=0,this.state.remainingTime=0}}class f{constructor(t,e,s){this.config=i(i({},h),t),this.reactive=e,this.ref=s,this.interceptors={request:[],response:[]},this.state=e({loading:!1,error:null,lastRequest:null,pendingRequests:0}),this.abortControllers=/* @__PURE__ */new Map}setBaseURL(t){return this.config.baseURL=t,this}setHeader(t,e){return this.config.headers[t]=e,this}setHeaders(t){return this.config.headers=i(i({},this.config.headers),t),this}removeHeader(t){return delete this.config.headers[t],this}setAuthToken(t){return t?this.config.headers.Authorization=`Bearer ${t}`:delete this.config.headers.Authorization,this}setBasicAuth(t,e){const s=btoa(`${t}:${e}`);return this.config.headers.Authorization=`Basic ${s}`,this}clearAuth(){return delete this.config.headers.Authorization,this}setTimeout(t){return this.config.timeout=t,this}addRequestInterceptor(t,e){const s=this.interceptors.request.length;return this.interceptors.request.push({fulfilled:t,rejected:e,id:s}),s}addResponseInterceptor(t,e){const s=this.interceptors.response.length;return this.interceptors.response.push({fulfilled:t,rejected:e,id:s}),s}removeRequestInterceptor(t){const e=this.interceptors.request.findIndex(e=>e.id===t);-1!==e&&this.interceptors.request.splice(e,1)}removeResponseInterceptor(t){const e=this.interceptors.response.findIndex(e=>e.id===t);-1!==e&&this.interceptors.response.splice(e,1)}clearInterceptors(){this.interceptors.request=[],this.interceptors.response=[]}buildURL(t,e={}){let s=t.startsWith("http")?t:`${this.config.baseURL}${t}`;const r=new URLSearchParams;for(const[n,a]of Object.entries(e))null!=a&&(Array.isArray(a)?a.forEach(t=>r.append(n,t)):r.append(n,a));const o=r.toString();return o&&(s+=(s.includes("?")?"&":"?")+o),s}async runRequestInterceptors(t){let e=i({},t);for(const r of this.interceptors.request)try{r.fulfilled&&(e=await r.fulfilled(e)||e)}catch(s){if(!r.rejected)throw s;e=await r.rejected(s)||e}return e}async runResponseInterceptors(t){let e=t;for(const r of this.interceptors.response)try{r.fulfilled&&(e=await r.fulfilled(e)||e)}catch(s){if(!r.rejected)throw s;e=await r.rejected(s)}return e}async runResponseErrorInterceptors(t){for(const s of this.interceptors.response)if(s.rejected)try{const e=await s.rejected(t);if(void 0!==e)return e}catch(e){t=e}throw t}generateRequestId(){return`req_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}async request(t){const e=this.generateRequestId(),s=new AbortController;this.abortControllers.set(e,s);let r=i({method:"GET",headers:i({},this.config.headers),timeout:this.config.timeout,withCredentials:this.config.withCredentials,retries:this.config.retries,retryDelay:this.config.retryDelay},t);t.headers&&(r.headers=i(i({},r.headers),t.headers));try{r=await this.runRequestInterceptors(r)}catch(d){throw this.abortControllers.delete(e),d}const o=this.buildURL(r.url,r.params),n={method:r.method.toUpperCase(),headers:r.headers,signal:s.signal,credentials:r.withCredentials?"include":"same-origin"};void 0===r.body||["GET","HEAD"].includes(n.method)||(r.body instanceof FormData?(delete n.headers["Content-Type"],n.body=r.body):"object"==typeof r.body?n.body=JSON.stringify(r.body):n.body=r.body),this.state.loading=!0,this.state.pendingRequests++,this.state.lastRequest={url:o,method:n.method,time:Date.now()},this.state.error=null;const a=setTimeout(()=>{s.abort()},r.timeout),l=async t=>{try{const s=await fetch(o,n);let a;const d=s.headers.get("content-type");if(d&&d.includes("application/json"))a=await s.json();else if(d&&d.includes("text/"))a=await s.text();else{const t=await s.text();try{a=JSON.parse(t)}catch(e){a=t}}const h={data:a,status:s.status,statusText:s.statusText,headers:Object.fromEntries(s.headers.entries()),config:r,request:i({url:o},n)};if(!s.ok){const e=new u((null==a?void 0:a.message)||s.statusText||`Request failed with status ${s.status}`,s.status,h,i({url:o},n));if(s.status>=500&&t>0)return await new Promise(t=>setTimeout(t,r.retryDelay)),l(t-1);throw e}return await this.runResponseInterceptors(h)}catch(d){if("AbortError"===d.name)throw new u("Request timeout",408,null,i({url:o},n));if("TypeError"===d.name&&t>0)return await new Promise(t=>setTimeout(t,r.retryDelay)),l(t-1);try{return await this.runResponseErrorInterceptors(d)}catch(e){throw e}}};try{return await l(r.retries)}finally{clearTimeout(a),this.abortControllers.delete(e),this.state.pendingRequests--,this.state.loading=this.state.pendingRequests>0}}async get(t,e={}){return this.request(l(i({},e),{method:"GET",url:t}))}async post(t,e={},s={}){return this.request(l(i({},s),{method:"POST",url:t,body:e}))}async put(t,e={},s={}){return this.request(l(i({},s),{method:"PUT",url:t,body:e}))}async patch(t,e={},s={}){return this.request(l(i({},s),{method:"PATCH",url:t,body:e}))}async delete(t,e={}){return this.request(l(i({},e),{method:"DELETE",url:t}))}async head(t,e={}){return this.request(l(i({},e),{method:"HEAD",url:t}))}async options(t,e={}){return this.request(l(i({},e),{method:"OPTIONS",url:t}))}upload(t,e,s={}){const r=new p(this.reactive),o=new AbortController,n=this.generateRequestId();this.abortControllers.set(n,o);const a=s,{fieldName:l="file",additionalData:h={},headers:c={},onProgress:f,onComplete:m,onError:g}=a,b=d(a,["fieldName","additionalData","headers","onProgress","onComplete","onError"]);let w;if(e instanceof FormData)w=e;else{w=new FormData,Array.isArray(e)?e.forEach((t,e)=>{w.append(`${l}[${e}]`,t)}):w.append(l,e);for(const[t,e]of Object.entries(h))null!=e&&w.append(t,"object"==typeof e?JSON.stringify(e):e)}let y=0;for(const[,i]of w.entries())i instanceof File?y+=i.size:"string"==typeof i&&(y+=new Blob([i]).size);r.start(y);return{promise:new Promise((e,s)=>{var a;const l=new XMLHttpRequest;l.upload.addEventListener("progress",t=>{t.lengthComputable&&(r.update(t.loaded,t.total),f&&f({loaded:t.loaded,total:t.total,progress:r.state.progress,speed:r.state.speed,remainingTime:r.state.remainingTime}))}),l.addEventListener("load",async()=>{let o;this.abortControllers.delete(n);try{o=JSON.parse(l.responseText)}catch(i){o=l.responseText}const a={data:o,status:l.status,statusText:l.statusText,headers:this.parseXHRHeaders(l.getAllResponseHeaders())};if(l.status>=200&&l.status<300){r.complete();try{const t=await this.runResponseInterceptors(a);m&&m(t),e(t)}catch(d){r.fail(d),g&&g(d),s(d)}}else{const e=new u((null==o?void 0:o.message)||l.statusText||"Upload failed",l.status,a,{url:t,method:"POST"});r.fail(e);try{await this.runResponseErrorInterceptors(e)}catch(i){g&&g(i),s(i)}}}),l.addEventListener("error",()=>{this.abortControllers.delete(n);const e=new u("Network error during upload",0,null,{url:t,method:"POST"});r.fail(e),g&&g(e),s(e)}),l.addEventListener("abort",()=>{this.abortControllers.delete(n);const e=new u("Upload cancelled",0,null,{url:t,method:"POST"});r.fail(e),g&&g(e),s(e)}),l.addEventListener("timeout",()=>{this.abortControllers.delete(n);const e=new u("Upload timeout",408,null,{url:t,method:"POST"});r.fail(e),g&&g(e),s(e)}),o.signal.addEventListener("abort",()=>{l.abort()});const d=this.buildURL(t);l.open("POST",d);const h=i(i({},this.config.headers),c);delete h["Content-Type"];for(const[t,r]of Object.entries(h))l.setRequestHeader(t,r);l.timeout=b.timeout||this.config.timeout,l.withCredentials=null!=(a=b.withCredentials)?a:this.config.withCredentials,l.send(w)}),tracker:r.state,abort:()=>o.abort(),requestId:n}}parseXHRHeaders(t){const e={};return t?(t.split("\r\n").forEach(t=>{const[s,...r]=t.split(":");s&&r.length&&(e[s.trim().toLowerCase()]=r.join(":").trim())}),e):e}download(t,e={}){const s=new p(this.reactive),r=new AbortController,o=this.generateRequestId();this.abortControllers.set(o,r);const n=e,{filename:a,onProgress:l,onComplete:h,onError:c}=n;d(n,["filename","onProgress","onComplete","onError"]);return{promise:(async()=>{try{const n=await fetch(this.buildURL(t),{method:"GET",headers:i(i({},this.config.headers),e.headers),signal:r.signal,credentials:this.config.withCredentials?"include":"same-origin"});if(!n.ok)throw new u(`Download failed with status ${n.status}`,n.status,null,{url:t,method:"GET"});const d=n.headers.get("content-length"),c=d?parseInt(d,10):0;s.start(c);const p=n.body.getReader(),f=[];let m=0;for(;;){const{done:t,value:e}=await p.read();if(t)break;f.push(e),m+=e.length,c>0&&(s.update(m,c),l&&l({loaded:m,total:c,progress:s.state.progress,speed:s.state.speed,remainingTime:s.state.remainingTime}))}s.complete();const g=new Blob(f);if(a){const t=URL.createObjectURL(g),e=document.createElement("a");e.href=t,e.download=a,document.body.appendChild(e),e.click(),document.body.removeChild(e),URL.revokeObjectURL(t)}const b={blob:g,filename:a,size:g.size};return h&&h(b),this.abortControllers.delete(o),b}catch(n){if(this.abortControllers.delete(o),"AbortError"===n.name){const e=new u("Download cancelled",0,null,{url:t,method:"GET"});throw s.fail(e),c&&c(e),e}throw s.fail(n),c&&c(n),n}})(),tracker:s.state,abort:()=>r.abort(),requestId:o}}cancelRequest(t){const e=this.abortControllers.get(t);e&&(e.abort(),this.abortControllers.delete(t))}cancelAll(){for(const[t,e]of this.abortControllers)e.abort();this.abortControllers.clear(),this.state.loading=!1,this.state.pendingRequests=0}create(t={}){return new f(i(i({},this.config),t),this.reactive,this.ref)}}const m={install(t,e={}){const{reactive:s,ref:r}=t,o=new f(e,s,r);m._instance=o,t.onHook("afterCompile",(t,e,s)=>{e.$http||(e.$http=o,e.$get=o.get.bind(o),e.$post=o.post.bind(o),e.$put=o.put.bind(o),e.$patch=o.patch.bind(o),e.$delete=o.delete.bind(o),e.$upload=o.upload.bind(o),e.$download=o.download.bind(o))}),t.onHook("onError",(t,e,s)=>{t.isHttpError})},getInstance:()=>m._instance||null};function g(t={}){return new f(t,t=>t,t=>({value:t}))}export{u as HttpError,c as HttpStatus,p as UploadTracker,f as ZogHttpClient,m as ZogHttpPlugin,g as createHttpClient,m as default};
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@zogjs/http",
3
+ "version": "0.4.8",
4
+ "description": "Powerful HTTP client plugin for Zog.js - Full-featured request library with interceptors, file upload/download progress tracking, and reactive state management",
5
+ "type": "module",
6
+ "main": "dist/zog-http.es.js",
7
+ "module": "dist/zog-http.es.js",
8
+ "exports": {
9
+ ".": "./dist/zog-http.es.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "dev": "vite",
21
+ "build": "vite build",
22
+ "preview": "vite preview",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/zogjs/zog-http.git"
28
+ },
29
+ "homepage": "https://github.com/zogjs/zog-http#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/zogjs/zog-http/issues"
32
+ },
33
+ "author": "ZogJS Team",
34
+ "license": "MIT",
35
+ "keywords": [
36
+ "zogjs",
37
+ "zog",
38
+ "zog-http",
39
+ "plugin",
40
+ "utilities",
41
+ "http-client",
42
+ "performance",
43
+ "reactive",
44
+ "frontend",
45
+ "javascript"
46
+ ],
47
+ "peerDependencies": {
48
+ "zogjs": "^0.4.8"
49
+ },
50
+ "devDependencies": {
51
+ "terser": "^5.44.1",
52
+ "vite": "^7.0.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=16.0.0"
56
+ }
57
+ }