feat: Implement comprehensive web request handling with caching, retry, and interceptors
- Added cache strategies: NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly, and CacheOnly. - Introduced InterceptorManager for managing request, response, and error interceptors. - Developed RetryManager for handling request retries with customizable backoff strategies. - Implemented RequestDeduplicator to prevent simultaneous identical requests. - Created timeout utilities for handling request timeouts. - Enhanced WebrequestClient to support global interceptors, caching, and retry logic. - Added convenience methods for common HTTP methods (GET, POST, PUT, DELETE) with JSON handling. - Established a fetch-compatible webrequest function for seamless integration. - Defined core type structures for caching, retry options, interceptors, and web request configurations.
This commit is contained in:
		
							
								
								
									
										339
									
								
								migration-v4.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										339
									
								
								migration-v4.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,339 @@ | ||||
| # Migration Guide: v3 → v4 | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| Version 4.0 is a complete modernization of `@push.rocks/webrequest`, bringing it in line with modern web standards while maintaining backward compatibility through a deprecated API layer. | ||||
|  | ||||
| ## What's New in v4 | ||||
|  | ||||
| ### Core Improvements | ||||
|  | ||||
| - **Fetch-Compatible API**: Drop-in replacement for native `fetch()` with enhanced features | ||||
| - **Intelligent HTTP Caching**: Respects `Cache-Control`, `ETag`, `Last-Modified`, and `Expires` headers | ||||
| - **Multiple Cache Strategies**: network-first, cache-first, stale-while-revalidate, network-only, cache-only | ||||
| - **Advanced Retry System**: Configurable retry with exponential/linear/constant backoff | ||||
| - **Request/Response Interceptors**: Middleware pattern for transforming requests and responses | ||||
| - **Request Deduplication**: Automatically deduplicate simultaneous identical requests | ||||
| - **TypeScript Generics**: Type-safe response parsing with `webrequest.getJson<T>()` | ||||
| - **Better Fault Tolerance**: Enhanced multi-endpoint fallback with retry strategies | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - Fixed: `deleteJson()` now correctly uses `DELETE` method instead of `GET` | ||||
|  | ||||
| ## Migration Path | ||||
|  | ||||
| ### Option 1: Quick Migration (Recommended) | ||||
|  | ||||
| The v3 `WebRequest` class is still available but marked as deprecated. Your existing code will continue to work: | ||||
|  | ||||
| ```typescript | ||||
| // This still works in v4 (with deprecation warnings) | ||||
| import { WebRequest } from '@push.rocks/webrequest'; | ||||
|  | ||||
| const client = new WebRequest({ logging: true }); | ||||
| const data = await client.getJson('https://api.example.com/data', true); | ||||
| ``` | ||||
|  | ||||
| ### Option 2: Migrate to New API | ||||
|  | ||||
| #### Basic Fetch-Compatible Usage | ||||
|  | ||||
| ```typescript | ||||
| // v3 | ||||
| import { WebRequest } from '@push.rocks/webrequest'; | ||||
| const client = new WebRequest(); | ||||
| const response = await client.request('https://api.example.com/data', { | ||||
|   method: 'GET' | ||||
| }); | ||||
| const data = await response.json(); | ||||
|  | ||||
| // v4 - Fetch-compatible | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
| const response = await webrequest('https://api.example.com/data'); | ||||
| const data = await response.json(); | ||||
| ``` | ||||
|  | ||||
| #### JSON Convenience Methods | ||||
|  | ||||
| ```typescript | ||||
| // v3 | ||||
| const client = new WebRequest(); | ||||
| const data = await client.getJson('https://api.example.com/data', true); | ||||
|  | ||||
| // v4 - Function API | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
| const data = await webrequest.getJson('https://api.example.com/data', { | ||||
|   cacheStrategy: 'cache-first' | ||||
| }); | ||||
|  | ||||
| // v4 - Client API (similar to v3) | ||||
| import { WebrequestClient } from '@push.rocks/webrequest'; | ||||
| const client = new WebrequestClient({ logging: true }); | ||||
| const data = await client.getJson('https://api.example.com/data', { | ||||
|   cacheStrategy: 'cache-first' | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## Feature Comparison | ||||
|  | ||||
| ### Caching | ||||
|  | ||||
| ```typescript | ||||
| // v3 - Boolean flag | ||||
| const data = await client.getJson(url, true); // useCache = true | ||||
|  | ||||
| // v4 - Explicit strategies | ||||
| const data = await webrequest.getJson(url, { | ||||
|   cacheStrategy: 'cache-first', | ||||
|   cacheMaxAge: 60000 // 60 seconds | ||||
| }); | ||||
|  | ||||
| // v4 - HTTP header-based caching (automatic) | ||||
| const data = await webrequest.getJson(url, { | ||||
|   cacheStrategy: 'network-first' // Respects Cache-Control headers | ||||
| }); | ||||
|  | ||||
| // v4 - Stale-while-revalidate | ||||
| const data = await webrequest.getJson(url, { | ||||
|   cacheStrategy: 'stale-while-revalidate' // Return cache, update in background | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Multi-Endpoint Fallback | ||||
|  | ||||
| ```typescript | ||||
| // v3 | ||||
| const client = new WebRequest(); | ||||
| const response = await client.requestMultiEndpoint( | ||||
|   ['https://api1.example.com/data', 'https://api2.example.com/data'], | ||||
|   { method: 'GET' } | ||||
| ); | ||||
|  | ||||
| // v4 | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
| const response = await webrequest('https://api1.example.com/data', { | ||||
|   fallbackUrls: ['https://api2.example.com/data'], | ||||
|   retry: { | ||||
|     maxAttempts: 3, | ||||
|     backoff: 'exponential' | ||||
|   } | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Timeout | ||||
|  | ||||
| ```typescript | ||||
| // v3 | ||||
| const response = await client.request(url, { | ||||
|   method: 'GET', | ||||
|   timeoutMs: 30000 | ||||
| }); | ||||
|  | ||||
| // v4 | ||||
| const response = await webrequest(url, { | ||||
|   timeout: 30000 // milliseconds | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## New Features in v4 | ||||
|  | ||||
| ### Retry Strategies | ||||
|  | ||||
| ```typescript | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
|  | ||||
| const response = await webrequest('https://api.example.com/data', { | ||||
|   retry: { | ||||
|     maxAttempts: 3, | ||||
|     backoff: 'exponential', // or 'linear', 'constant' | ||||
|     initialDelay: 1000, | ||||
|     maxDelay: 30000, | ||||
|     retryOn: [408, 429, 500, 502, 503, 504], // Status codes to retry | ||||
|     onRetry: (attempt, error, nextDelay) => { | ||||
|       console.log(`Retry attempt ${attempt}, waiting ${nextDelay}ms`); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Request/Response Interceptors | ||||
|  | ||||
| ```typescript | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
|  | ||||
| // Add global request interceptor | ||||
| webrequest.addRequestInterceptor((request) => { | ||||
|   // Add auth header | ||||
|   const headers = new Headers(request.headers); | ||||
|   headers.set('Authorization', `Bearer ${getToken()}`); | ||||
|   return new Request(request, { headers }); | ||||
| }); | ||||
|  | ||||
| // Add global response interceptor | ||||
| webrequest.addResponseInterceptor((response) => { | ||||
|   console.log(`Response: ${response.status} ${response.url}`); | ||||
|   return response; | ||||
| }); | ||||
|  | ||||
| // Per-request interceptors | ||||
| const response = await webrequest('https://api.example.com/data', { | ||||
|   interceptors: { | ||||
|     request: [(req) => { | ||||
|       console.log('Sending request:', req.url); | ||||
|       return req; | ||||
|     }], | ||||
|     response: [(res) => { | ||||
|       console.log('Received response:', res.status); | ||||
|       return res; | ||||
|     }] | ||||
|   } | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Request Deduplication | ||||
|  | ||||
| ```typescript | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
|  | ||||
| // Multiple simultaneous identical requests will be deduplicated | ||||
| const [res1, res2, res3] = await Promise.all([ | ||||
|   webrequest('https://api.example.com/data', { deduplicate: true }), | ||||
|   webrequest('https://api.example.com/data', { deduplicate: true }), | ||||
|   webrequest('https://api.example.com/data', { deduplicate: true }), | ||||
| ]); | ||||
| // Only one actual network request is made | ||||
| ``` | ||||
|  | ||||
| ### TypeScript Generics | ||||
|  | ||||
| ```typescript | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
|  | ||||
| interface User { | ||||
|   id: number; | ||||
|   name: string; | ||||
|   email: string; | ||||
| } | ||||
|  | ||||
| // Type-safe response parsing | ||||
| const user = await webrequest.getJson<User>('https://api.example.com/user/1'); | ||||
| // user is typed as User | ||||
|  | ||||
| const users = await webrequest.getJson<User[]>('https://api.example.com/users'); | ||||
| // users is typed as User[] | ||||
| ``` | ||||
|  | ||||
| ### Custom Cache Keys | ||||
|  | ||||
| ```typescript | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
|  | ||||
| const response = await webrequest('https://api.example.com/search?q=test', { | ||||
|   cacheStrategy: 'cache-first', | ||||
|   cacheKey: (request) => { | ||||
|     // Custom cache key based on URL without query params | ||||
|     const url = new URL(request.url); | ||||
|     return `search:${url.searchParams.get('q')}`; | ||||
|   } | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Advanced Client Configuration | ||||
|  | ||||
| ```typescript | ||||
| import { WebrequestClient } from '@push.rocks/webrequest'; | ||||
|  | ||||
| // Create a client with default options | ||||
| const apiClient = new WebrequestClient({ | ||||
|   logging: true, | ||||
|   timeout: 30000, | ||||
|   cacheStrategy: 'network-first', | ||||
|   retry: { | ||||
|     maxAttempts: 3, | ||||
|     backoff: 'exponential' | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // Add global interceptors | ||||
| apiClient.addRequestInterceptor((request) => { | ||||
|   const headers = new Headers(request.headers); | ||||
|   headers.set('X-API-Key', process.env.API_KEY); | ||||
|   return new Request(request, { headers }); | ||||
| }); | ||||
|  | ||||
| // All requests through this client use the configured defaults | ||||
| const data = await apiClient.getJson('https://api.example.com/data'); | ||||
| ``` | ||||
|  | ||||
| ## Breaking Changes | ||||
|  | ||||
| ### 1. Cache API Changes | ||||
|  | ||||
| **Before (v3):** | ||||
| ```typescript | ||||
| await client.getJson(url, true); // Boolean for cache | ||||
| ``` | ||||
|  | ||||
| **After (v4):** | ||||
| ```typescript | ||||
| await webrequest.getJson(url, { | ||||
|   cacheStrategy: 'cache-first' // Explicit strategy | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### 2. Multi-Endpoint API | ||||
|  | ||||
| **Before (v3):** | ||||
| ```typescript | ||||
| await client.requestMultiEndpoint([url1, url2], options); | ||||
| ``` | ||||
|  | ||||
| **After (v4):** | ||||
| ```typescript | ||||
| await webrequest(url1, { | ||||
|   fallbackUrls: [url2], | ||||
|   retry: true | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### 3. Constructor Options | ||||
|  | ||||
| **Before (v3):** | ||||
| ```typescript | ||||
| const client = new WebRequest({ logging: true }); | ||||
| ``` | ||||
|  | ||||
| **After (v4):** | ||||
| ```typescript | ||||
| // Function API (no constructor) | ||||
| import { webrequest } from '@push.rocks/webrequest'; | ||||
|  | ||||
| // Or client API | ||||
| import { WebrequestClient } from '@push.rocks/webrequest'; | ||||
| const client = new WebrequestClient({ logging: true }); | ||||
| ``` | ||||
|  | ||||
| ## Deprecation Timeline | ||||
|  | ||||
| - **v4.0**: `WebRequest` class marked as deprecated but fully functional | ||||
| - **v4.x**: Continued support with deprecation warnings | ||||
| - **v5.0**: `WebRequest` class will be removed | ||||
|  | ||||
| ## Recommendation | ||||
|  | ||||
| We strongly recommend migrating to the new API to take advantage of: | ||||
|  | ||||
| - Standards-aligned fetch-compatible interface | ||||
| - Intelligent HTTP caching with header support | ||||
| - Advanced retry and fault tolerance | ||||
| - Request/response interceptors | ||||
| - Better TypeScript support | ||||
| - Request deduplication | ||||
|  | ||||
| The migration is straightforward, and the new API is more powerful and flexible while being simpler to use. | ||||
|  | ||||
| ## Need Help? | ||||
|  | ||||
| - Check the updated [README.md](./readme.md) for comprehensive examples | ||||
| - Report issues at: https://code.foss.global/push.rocks/webrequest/issues | ||||
		Reference in New Issue
	
	Block a user