@ahoo-wang/fetcher 2.8.8 → 2.9.1
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 +498 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -284,6 +284,504 @@ fetcher.interceptors.request.use({
|
|
|
284
284
|
// 3. auth-interceptor (order: 50)
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
+
## 🚀 Advanced Usage Examples
|
|
288
|
+
|
|
289
|
+
### Custom Result Extractors
|
|
290
|
+
|
|
291
|
+
Create custom result extractors for different response formats:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { Fetcher, ResultExtractor } from '@ahoo-wang/fetcher';
|
|
295
|
+
|
|
296
|
+
// Custom XML extractor
|
|
297
|
+
class XmlResultExtractor implements ResultExtractor<string> {
|
|
298
|
+
async extract(response: Response): Promise<string> {
|
|
299
|
+
const text = await response.text();
|
|
300
|
+
// Parse XML and return as string (simplified example)
|
|
301
|
+
return text;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Custom GraphQL extractor
|
|
306
|
+
class GraphQLErrorExtractor implements ResultExtractor<any> {
|
|
307
|
+
async extract(response: Response): Promise<any> {
|
|
308
|
+
const data = await response.json();
|
|
309
|
+
|
|
310
|
+
if (data.errors && data.errors.length > 0) {
|
|
311
|
+
throw new Error(`GraphQL Error: ${data.errors[0].message}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return data.data;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Usage
|
|
319
|
+
const fetcher = new Fetcher({
|
|
320
|
+
baseURL: 'https://api.example.com',
|
|
321
|
+
resultExtractor: new GraphQLErrorExtractor(),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// GraphQL query
|
|
325
|
+
const result = await fetcher.post('/graphql', {
|
|
326
|
+
body: {
|
|
327
|
+
query: `query { user(id: "123") { name email } }`,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Advanced Interceptor Patterns
|
|
333
|
+
|
|
334
|
+
Implement complex interceptor patterns for enterprise applications:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { Fetcher, FetchInterceptor } from '@ahoo-wang/fetcher';
|
|
338
|
+
|
|
339
|
+
// Request signing interceptor (e.g., AWS Signature V4)
|
|
340
|
+
class RequestSigningInterceptor implements FetchInterceptor {
|
|
341
|
+
name = 'request-signer';
|
|
342
|
+
order = 50;
|
|
343
|
+
|
|
344
|
+
async intercept(exchange: any) {
|
|
345
|
+
const { request } = exchange;
|
|
346
|
+
|
|
347
|
+
// Generate signature based on request
|
|
348
|
+
const signature = await this.generateSignature(request);
|
|
349
|
+
|
|
350
|
+
// Add signature to headers
|
|
351
|
+
request.headers['Authorization'] = `AWS4-HMAC-SHA256 ${signature}`;
|
|
352
|
+
|
|
353
|
+
return exchange;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async generateSignature(request: any): Promise<string> {
|
|
357
|
+
// Implementation of AWS Signature V4 or similar
|
|
358
|
+
// This is a simplified example
|
|
359
|
+
const timestamp = new Date().toISOString();
|
|
360
|
+
return `Credential=key/${timestamp}/region/service/aws4_request`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Circuit breaker interceptor
|
|
365
|
+
class CircuitBreakerInterceptor implements FetchInterceptor {
|
|
366
|
+
name = 'circuit-breaker';
|
|
367
|
+
order = 25;
|
|
368
|
+
|
|
369
|
+
private failures = 0;
|
|
370
|
+
private lastFailureTime = 0;
|
|
371
|
+
private readonly threshold = 5;
|
|
372
|
+
private readonly timeout = 60000; // 1 minute
|
|
373
|
+
|
|
374
|
+
async intercept(exchange: any) {
|
|
375
|
+
// Check if circuit is open
|
|
376
|
+
if (this.isCircuitOpen()) {
|
|
377
|
+
throw new Error('Circuit breaker is open');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// Proceed with request
|
|
382
|
+
const result = await exchange.proceed();
|
|
383
|
+
|
|
384
|
+
// Reset on success
|
|
385
|
+
this.failures = 0;
|
|
386
|
+
return result;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
// Record failure
|
|
389
|
+
this.failures++;
|
|
390
|
+
this.lastFailureTime = Date.now();
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private isCircuitOpen(): boolean {
|
|
396
|
+
if (this.failures < this.threshold) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Check if timeout has passed
|
|
401
|
+
return Date.now() - this.lastFailureTime < this.timeout;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Usage
|
|
406
|
+
const fetcher = new Fetcher({
|
|
407
|
+
baseURL: 'https://api.example.com',
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
fetcher.interceptors.request.use(new RequestSigningInterceptor());
|
|
411
|
+
fetcher.interceptors.request.use(new CircuitBreakerInterceptor());
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Multi-Environment Configuration
|
|
415
|
+
|
|
416
|
+
Create environment-specific fetcher configurations:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { Fetcher, NamedFetcher } from '@ahoo-wang/fetcher';
|
|
420
|
+
|
|
421
|
+
type Environment = 'development' | 'staging' | 'production';
|
|
422
|
+
|
|
423
|
+
interface EnvironmentConfig {
|
|
424
|
+
baseURL: string;
|
|
425
|
+
timeout: number;
|
|
426
|
+
retryConfig?: {
|
|
427
|
+
maxRetries: number;
|
|
428
|
+
retryDelay: number;
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const environmentConfigs: Record<Environment, EnvironmentConfig> = {
|
|
433
|
+
development: {
|
|
434
|
+
baseURL: 'http://localhost:3000/api',
|
|
435
|
+
timeout: 30000,
|
|
436
|
+
retryConfig: { maxRetries: 0, retryDelay: 0 },
|
|
437
|
+
},
|
|
438
|
+
staging: {
|
|
439
|
+
baseURL: 'https://api-staging.example.com',
|
|
440
|
+
timeout: 10000,
|
|
441
|
+
retryConfig: { maxRetries: 2, retryDelay: 1000 },
|
|
442
|
+
},
|
|
443
|
+
production: {
|
|
444
|
+
baseURL: 'https://api.example.com',
|
|
445
|
+
timeout: 5000,
|
|
446
|
+
retryConfig: { maxRetries: 3, retryDelay: 2000 },
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
class EnvironmentAwareFetcher extends NamedFetcher {
|
|
451
|
+
constructor(name: string, environment: Environment) {
|
|
452
|
+
const config = environmentConfigs[environment];
|
|
453
|
+
|
|
454
|
+
super(name, {
|
|
455
|
+
baseURL: config.baseURL,
|
|
456
|
+
timeout: config.timeout,
|
|
457
|
+
// Add other environment-specific configurations
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Add environment-specific interceptors
|
|
461
|
+
if (environment === 'production') {
|
|
462
|
+
this.interceptors.request.use({
|
|
463
|
+
name: 'production-monitoring',
|
|
464
|
+
order: 1,
|
|
465
|
+
intercept(exchange) {
|
|
466
|
+
// Add production monitoring headers
|
|
467
|
+
exchange.request.headers['X-Environment'] = 'production';
|
|
468
|
+
exchange.request.headers['X-Request-Id'] = crypto.randomUUID();
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Usage
|
|
476
|
+
const currentEnv = (process.env.NODE_ENV as Environment) || 'development';
|
|
477
|
+
const apiFetcher = new EnvironmentAwareFetcher('api', currentEnv);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Request Batching and Deduplication
|
|
481
|
+
|
|
482
|
+
Implement request batching to reduce network overhead:
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
import { Fetcher } from '@ahoo-wang/fetcher';
|
|
486
|
+
|
|
487
|
+
class RequestBatcher {
|
|
488
|
+
private queue: Map<string, any[]> = new Map();
|
|
489
|
+
private timeoutId: NodeJS.Timeout | null = null;
|
|
490
|
+
private readonly batchDelay = 100; // ms
|
|
491
|
+
|
|
492
|
+
constructor(private fetcher: Fetcher) {}
|
|
493
|
+
|
|
494
|
+
async batchRequest<T>(
|
|
495
|
+
endpoint: string,
|
|
496
|
+
data: any,
|
|
497
|
+
batchKey?: string,
|
|
498
|
+
): Promise<T> {
|
|
499
|
+
const key = batchKey || endpoint;
|
|
500
|
+
|
|
501
|
+
return new Promise((resolve, reject) => {
|
|
502
|
+
if (!this.queue.has(key)) {
|
|
503
|
+
this.queue.set(key, []);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this.queue.get(key)!.push({ data, resolve, reject });
|
|
507
|
+
|
|
508
|
+
// Schedule batch execution
|
|
509
|
+
if (!this.timeoutId) {
|
|
510
|
+
this.timeoutId = setTimeout(
|
|
511
|
+
() => this.executeBatch(key),
|
|
512
|
+
this.batchDelay,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private async executeBatch(key: string) {
|
|
519
|
+
const requests = this.queue.get(key);
|
|
520
|
+
if (!requests || requests.length === 0) return;
|
|
521
|
+
|
|
522
|
+
this.queue.delete(key);
|
|
523
|
+
this.timeoutId = null;
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
// Execute batch request
|
|
527
|
+
const batchData = requests.map(r => r.data);
|
|
528
|
+
const response = await this.fetcher.post(`/${key}/batch`, {
|
|
529
|
+
body: { requests: batchData },
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const results = await response.json();
|
|
533
|
+
|
|
534
|
+
// Resolve individual promises
|
|
535
|
+
requests.forEach((request, index) => {
|
|
536
|
+
if (results[index]?.success) {
|
|
537
|
+
request.resolve(results[index].data);
|
|
538
|
+
} else {
|
|
539
|
+
request.reject(
|
|
540
|
+
new Error(results[index]?.error || 'Batch request failed'),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
} catch (error) {
|
|
545
|
+
// Reject all promises in batch
|
|
546
|
+
requests.forEach(request => request.reject(error));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Usage
|
|
552
|
+
const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });
|
|
553
|
+
const batcher = new RequestBatcher(fetcher);
|
|
554
|
+
|
|
555
|
+
// Batch multiple user updates
|
|
556
|
+
const results = await Promise.all([
|
|
557
|
+
batcher.batchRequest('users', { id: 1, name: 'John' }),
|
|
558
|
+
batcher.batchRequest('users', { id: 2, name: 'Jane' }),
|
|
559
|
+
batcher.batchRequest('users', { id: 3, name: 'Bob' }),
|
|
560
|
+
]);
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Custom Error Handling and Retry Logic
|
|
564
|
+
|
|
565
|
+
Implement sophisticated error handling with exponential backoff:
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
import { Fetcher, FetcherError } from '@ahoo-wang/fetcher';
|
|
569
|
+
|
|
570
|
+
class ExponentialBackoffRetry {
|
|
571
|
+
constructor(
|
|
572
|
+
private maxRetries: number = 3,
|
|
573
|
+
private baseDelay: number = 1000,
|
|
574
|
+
private maxDelay: number = 30000,
|
|
575
|
+
) {}
|
|
576
|
+
|
|
577
|
+
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
578
|
+
let lastError: Error;
|
|
579
|
+
|
|
580
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
581
|
+
try {
|
|
582
|
+
return await operation();
|
|
583
|
+
} catch (error) {
|
|
584
|
+
lastError = error as Error;
|
|
585
|
+
|
|
586
|
+
if (attempt === this.maxRetries) {
|
|
587
|
+
break; // Don't retry on last attempt
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Check if error is retryable
|
|
591
|
+
if (!this.isRetryableError(error)) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Calculate delay with exponential backoff and jitter
|
|
596
|
+
const delay = Math.min(
|
|
597
|
+
this.baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
|
|
598
|
+
this.maxDelay,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
console.log(
|
|
602
|
+
`Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
|
603
|
+
);
|
|
604
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
throw lastError!;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private isRetryableError(error: any): boolean {
|
|
612
|
+
// Retry on network errors, 5xx server errors, and timeouts
|
|
613
|
+
if (error instanceof FetcherError) {
|
|
614
|
+
return (
|
|
615
|
+
error.name === 'FetchTimeoutError' ||
|
|
616
|
+
(error.response && error.response.status >= 500) ||
|
|
617
|
+
!error.response // Network error
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Retry on network-related errors
|
|
622
|
+
return error.name === 'TypeError' || error.message.includes('fetch');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Enhanced fetcher with retry logic
|
|
627
|
+
class ResilientFetcher extends Fetcher {
|
|
628
|
+
private retryHandler = new ExponentialBackoffRetry();
|
|
629
|
+
|
|
630
|
+
async request<T = any>(request: any): Promise<T> {
|
|
631
|
+
return this.retryHandler.execute(() => super.request<T>(request));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Usage
|
|
636
|
+
const fetcher = new ResilientFetcher({
|
|
637
|
+
baseURL: 'https://api.example.com',
|
|
638
|
+
timeout: 5000,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// This will automatically retry on failures
|
|
642
|
+
try {
|
|
643
|
+
const data = await fetcher.get('/unreliable-endpoint');
|
|
644
|
+
console.log('Success:', data);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.error('All retries failed:', error);
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Integration with Popular Libraries
|
|
651
|
+
|
|
652
|
+
Examples of integrating Fetcher with popular JavaScript libraries:
|
|
653
|
+
|
|
654
|
+
#### With Axios Compatibility Layer
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
import { Fetcher } from '@ahoo-wang/fetcher';
|
|
658
|
+
|
|
659
|
+
// Axios-compatible wrapper
|
|
660
|
+
class AxiosCompatibleFetcher {
|
|
661
|
+
constructor(private fetcher: Fetcher) {}
|
|
662
|
+
|
|
663
|
+
async get(url: string, config?: any) {
|
|
664
|
+
return this.fetcher.get(url, config);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async post(url: string, data?: any, config?: any) {
|
|
668
|
+
return this.fetcher.post(url, { ...config, body: data });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async put(url: string, data?: any, config?: any) {
|
|
672
|
+
return this.fetcher.put(url, { ...config, body: data });
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async delete(url: string, config?: any) {
|
|
676
|
+
return this.fetcher.delete(url, config);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Add interceptors in Axios style
|
|
680
|
+
interceptors = {
|
|
681
|
+
request: {
|
|
682
|
+
use: (interceptor: any) => {
|
|
683
|
+
this.fetcher.interceptors.request.use({
|
|
684
|
+
name: 'axios-compat',
|
|
685
|
+
order: 100,
|
|
686
|
+
intercept: interceptor,
|
|
687
|
+
});
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
response: {
|
|
691
|
+
use: (interceptor: any) => {
|
|
692
|
+
this.fetcher.interceptors.response.use({
|
|
693
|
+
name: 'axios-compat',
|
|
694
|
+
order: 100,
|
|
695
|
+
intercept: interceptor,
|
|
696
|
+
});
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Usage
|
|
703
|
+
const axiosLike = new AxiosCompatibleFetcher(
|
|
704
|
+
new Fetcher({ baseURL: 'https://api.example.com' }),
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Use like Axios
|
|
708
|
+
const response = await axiosLike.get('/users');
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
#### With React Query Integration
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
715
|
+
import { Fetcher } from '@ahoo-wang/fetcher';
|
|
716
|
+
|
|
717
|
+
const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });
|
|
718
|
+
|
|
719
|
+
// Custom hooks for React Query
|
|
720
|
+
function useApiQuery(key: string, endpoint: string) {
|
|
721
|
+
return useQuery({
|
|
722
|
+
queryKey: [key],
|
|
723
|
+
queryFn: async () => {
|
|
724
|
+
const response = await fetcher.get(endpoint);
|
|
725
|
+
return response.json();
|
|
726
|
+
},
|
|
727
|
+
retry: (failureCount, error) => {
|
|
728
|
+
// Custom retry logic based on error type
|
|
729
|
+
if (error instanceof FetcherError && error.response?.status === 404) {
|
|
730
|
+
return false; // Don't retry 404s
|
|
731
|
+
}
|
|
732
|
+
return failureCount < 3;
|
|
733
|
+
},
|
|
734
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function useApiMutation(endpoint: string, method: 'POST' | 'PUT' | 'DELETE' = 'POST') {
|
|
739
|
+
const queryClient = useQueryClient();
|
|
740
|
+
|
|
741
|
+
return useMutation({
|
|
742
|
+
mutationFn: async (data: any) => {
|
|
743
|
+
const response = await fetcher.request({
|
|
744
|
+
url: endpoint,
|
|
745
|
+
method,
|
|
746
|
+
body: data,
|
|
747
|
+
});
|
|
748
|
+
return response.json();
|
|
749
|
+
},
|
|
750
|
+
onSuccess: () => {
|
|
751
|
+
// Invalidate related queries
|
|
752
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
753
|
+
},
|
|
754
|
+
onError: (error) => {
|
|
755
|
+
// Custom error handling
|
|
756
|
+
console.error('Mutation failed:', error);
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Usage in React component
|
|
762
|
+
function UserManagement() {
|
|
763
|
+
const { data: users, isLoading } = useApiQuery('users', '/users');
|
|
764
|
+
const createUser = useApiMutation('/users', 'POST');
|
|
765
|
+
|
|
766
|
+
const handleCreateUser = async (userData: any) => {
|
|
767
|
+
try {
|
|
768
|
+
await createUser.mutateAsync(userData);
|
|
769
|
+
message.success('User created successfully');
|
|
770
|
+
} catch (error) {
|
|
771
|
+
message.error('Failed to create user');
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
if (isLoading) return <div>Loading...</div>;
|
|
776
|
+
|
|
777
|
+
return (
|
|
778
|
+
<div>
|
|
779
|
+
{/* User management UI */}
|
|
780
|
+
</div>
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
287
785
|
## 📚 API Reference
|
|
288
786
|
|
|
289
787
|
### Fetcher Class
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahoo-wang/fetcher",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "Fetcher is not just another HTTP client—it's a complete ecosystem designed for modern web development with native LLM\nstreaming API support. Built on the native Fetch API, Fetcher provides an Axios-like experience with powerful features\nwhile maintaining an incredibly small footprint.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|