Files
webrequest/ts/cache/cache.headers.ts
Juergen Kunz 54afcc46e2 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.
2025-10-20 09:59:24 +00:00

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;
}