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:
		
							
								
								
									
										172
									
								
								ts/cache/cache.headers.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								ts/cache/cache.headers.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| /** | ||||
|  * HTTP Cache Header parsing and utilities | ||||
|  * Implements RFC 7234 (HTTP Caching) | ||||
|  */ | ||||
|  | ||||
| import type { ICacheMetadata } from '../webrequest.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Parse Cache-Control header into metadata | ||||
|  */ | ||||
| export function parseCacheControl( | ||||
|   cacheControlHeader: string | null, | ||||
| ): Partial<ICacheMetadata> { | ||||
|   const metadata: Partial<ICacheMetadata> = { | ||||
|     maxAge: 0, | ||||
|     immutable: false, | ||||
|     noCache: false, | ||||
|     noStore: false, | ||||
|     mustRevalidate: false, | ||||
|   }; | ||||
|  | ||||
|   if (!cacheControlHeader) { | ||||
|     return metadata; | ||||
|   } | ||||
|  | ||||
|   const directives = cacheControlHeader | ||||
|     .toLowerCase() | ||||
|     .split(',') | ||||
|     .map((d) => d.trim()); | ||||
|  | ||||
|   for (const directive of directives) { | ||||
|     if (directive === 'no-cache') { | ||||
|       metadata.noCache = true; | ||||
|     } else if (directive === 'no-store') { | ||||
|       metadata.noStore = true; | ||||
|     } else if (directive === 'immutable') { | ||||
|       metadata.immutable = true; | ||||
|     } else if (directive === 'must-revalidate') { | ||||
|       metadata.mustRevalidate = true; | ||||
|     } else if (directive.startsWith('max-age=')) { | ||||
|       const maxAge = parseInt(directive.split('=')[1], 10); | ||||
|       if (!isNaN(maxAge)) { | ||||
|         metadata.maxAge = maxAge * 1000; // Convert to milliseconds | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return metadata; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Parse Expires header into timestamp | ||||
|  */ | ||||
| export function parseExpires(expiresHeader: string | null): number | undefined { | ||||
|   if (!expiresHeader) { | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const date = new Date(expiresHeader); | ||||
|     return date.getTime(); | ||||
|   } catch { | ||||
|     return undefined; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Extract cache metadata from response headers | ||||
|  */ | ||||
| export function extractCacheMetadata(headers: Headers): ICacheMetadata { | ||||
|   const cacheControl = headers.get('cache-control'); | ||||
|   const expires = headers.get('expires'); | ||||
|   const etag = headers.get('etag'); | ||||
|   const lastModified = headers.get('last-modified'); | ||||
|  | ||||
|   const metadata = parseCacheControl(cacheControl); | ||||
|  | ||||
|   // If no max-age from Cache-Control, try Expires header | ||||
|   if (metadata.maxAge === 0 && expires) { | ||||
|     const expiresTime = parseExpires(expires); | ||||
|     if (expiresTime) { | ||||
|       metadata.maxAge = Math.max(0, expiresTime - Date.now()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     maxAge: metadata.maxAge || 0, | ||||
|     etag: etag || undefined, | ||||
|     lastModified: lastModified || undefined, | ||||
|     immutable: metadata.immutable || false, | ||||
|     noCache: metadata.noCache || false, | ||||
|     noStore: metadata.noStore || false, | ||||
|     mustRevalidate: metadata.mustRevalidate || false, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if a cached response is still fresh | ||||
|  */ | ||||
| export function isFresh( | ||||
|   cacheEntry: { timestamp: number; maxAge?: number }, | ||||
|   metadata: ICacheMetadata, | ||||
| ): boolean { | ||||
|   // no-store means never cache | ||||
|   if (metadata.noStore) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   // If immutable, it's always fresh | ||||
|   if (metadata.immutable) { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   const age = Date.now() - cacheEntry.timestamp; | ||||
|   const maxAge = cacheEntry.maxAge || metadata.maxAge || 0; | ||||
|  | ||||
|   // If no max-age specified, consider stale | ||||
|   if (maxAge === 0) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return age < maxAge; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if revalidation is required | ||||
|  */ | ||||
| export function requiresRevalidation(metadata: ICacheMetadata): boolean { | ||||
|   return metadata.noCache || metadata.mustRevalidate; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create conditional request headers for revalidation | ||||
|  */ | ||||
| export function createConditionalHeaders(cacheEntry: { | ||||
|   etag?: string; | ||||
|   lastModified?: string; | ||||
| }): HeadersInit { | ||||
|   const headers: Record<string, string> = {}; | ||||
|  | ||||
|   if (cacheEntry.etag) { | ||||
|     headers['if-none-match'] = cacheEntry.etag; | ||||
|   } | ||||
|  | ||||
|   if (cacheEntry.lastModified) { | ||||
|     headers['if-modified-since'] = cacheEntry.lastModified; | ||||
|   } | ||||
|  | ||||
|   return headers; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Convert Headers object to plain object for storage | ||||
|  */ | ||||
| export function headersToObject(headers: Headers): Record<string, string> { | ||||
|   const obj: Record<string, string> = {}; | ||||
|   headers.forEach((value, key) => { | ||||
|     obj[key] = value; | ||||
|   }); | ||||
|   return obj; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Convert plain object back to Headers | ||||
|  */ | ||||
| export function objectToHeaders(obj: Record<string, string>): Headers { | ||||
|   const headers = new Headers(); | ||||
|   Object.entries(obj).forEach(([key, value]) => { | ||||
|     headers.set(key, value); | ||||
|   }); | ||||
|   return headers; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user