155 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			155 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * 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; | ||
|  |   } | ||
|  | } |