340 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			340 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|  | # 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 |