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:
		
							
								
								
									
										154
									
								
								ts/cache/cache.store.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								ts/cache/cache.store.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| /** | ||||
|  * Cache storage layer using IndexedDB via @push.rocks/webstore | ||||
|  */ | ||||
|  | ||||
| import * as plugins from '../webrequest.plugins.js'; | ||||
| import type { ICacheEntry } from '../webrequest.types.js'; | ||||
|  | ||||
| export class CacheStore { | ||||
|   private webstore: plugins.webstore.WebStore; | ||||
|   private initPromise: Promise<void>; | ||||
|  | ||||
|   constructor(dbName: string = 'webrequest-v4', storeName: string = 'cache') { | ||||
|     this.webstore = new plugins.webstore.WebStore({ | ||||
|       dbName, | ||||
|       storeName, | ||||
|     }); | ||||
|  | ||||
|     // Initialize the store | ||||
|     this.initPromise = this.init(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Initialize the store | ||||
|    */ | ||||
|   private async init(): Promise<void> { | ||||
|     // WebStore handles initialization internally | ||||
|     // This method exists for future extension if needed | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Generate a cache key from a request | ||||
|    */ | ||||
|   public generateCacheKey(request: Request): string { | ||||
|     // Use URL + method as the base key | ||||
|     const url = request.url; | ||||
|     const method = request.method; | ||||
|  | ||||
|     // For GET requests, just use the URL | ||||
|     if (method === 'GET') { | ||||
|       return url; | ||||
|     } | ||||
|  | ||||
|     // For other methods, include the method | ||||
|     return `${method}:${url}`; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Store a response in the cache | ||||
|    */ | ||||
|   public async set(cacheKey: string, entry: ICacheEntry): Promise<void> { | ||||
|     await this.initPromise; | ||||
|     await this.webstore.set(cacheKey, entry); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Retrieve a cached response | ||||
|    */ | ||||
|   public async get(cacheKey: string): Promise<ICacheEntry | null> { | ||||
|     await this.initPromise; | ||||
|  | ||||
|     try { | ||||
|       const entry = (await this.webstore.get(cacheKey)) as ICacheEntry; | ||||
|       return entry || null; | ||||
|     } catch (error) { | ||||
|       // If entry doesn't exist or is corrupted, return null | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if a cache entry exists | ||||
|    */ | ||||
|   public async has(cacheKey: string): Promise<boolean> { | ||||
|     await this.initPromise; | ||||
|     return await this.webstore.check(cacheKey); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete a cache entry | ||||
|    */ | ||||
|   public async delete(cacheKey: string): Promise<void> { | ||||
|     await this.initPromise; | ||||
|     await this.webstore.delete(cacheKey); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Clear all cache entries | ||||
|    */ | ||||
|   public async clear(): Promise<void> { | ||||
|     await this.initPromise; | ||||
|     await this.webstore.clear(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Create a Response object from a cache entry | ||||
|    */ | ||||
|   public responseFromCacheEntry(entry: ICacheEntry): Response { | ||||
|     const headers = new Headers(entry.headers); | ||||
|  | ||||
|     return new Response(entry.response, { | ||||
|       status: entry.status, | ||||
|       statusText: entry.statusText, | ||||
|       headers, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Create a cache entry from a Response object | ||||
|    */ | ||||
|   public async cacheEntryFromResponse( | ||||
|     url: string, | ||||
|     response: Response, | ||||
|     metadata?: { maxAge?: number; etag?: string; lastModified?: string }, | ||||
|   ): Promise<ICacheEntry> { | ||||
|     // Clone the response so we can read it multiple times | ||||
|     const clonedResponse = response.clone(); | ||||
|     const buffer = await clonedResponse.arrayBuffer(); | ||||
|  | ||||
|     // Extract headers | ||||
|     const headers: Record<string, string> = {}; | ||||
|     clonedResponse.headers.forEach((value, key) => { | ||||
|       headers[key] = value; | ||||
|     }); | ||||
|  | ||||
|     return { | ||||
|       response: buffer, | ||||
|       headers, | ||||
|       timestamp: Date.now(), | ||||
|       etag: metadata?.etag || clonedResponse.headers.get('etag') || undefined, | ||||
|       lastModified: | ||||
|         metadata?.lastModified || | ||||
|         clonedResponse.headers.get('last-modified') || | ||||
|         undefined, | ||||
|       maxAge: metadata?.maxAge, | ||||
|       url, | ||||
|       status: clonedResponse.status, | ||||
|       statusText: clonedResponse.statusText, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Prune expired entries (garbage collection) | ||||
|    * Returns the number of entries deleted | ||||
|    */ | ||||
|   public async pruneExpired(): Promise<number> { | ||||
|     await this.initPromise; | ||||
|  | ||||
|     // Note: WebStore doesn't provide a way to list all keys | ||||
|     // This would need to be implemented if we want automatic cleanup | ||||
|     // For now, we rely on individual entry checks | ||||
|  | ||||
|     return 0; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user