@ahoo-wang/fetcher 2.9.0 → 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.
Files changed (2) hide show
  1. package/README.md +498 -0
  2. 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.9.0",
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",