- 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.
		
			
				
	
	
		
			173 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			173 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * 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;
 | |
| }
 |