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