@web-widget/shared-cache
Version:
Standards-compliant HTTP cache implementation for server-side JavaScript with RFC 7234 compliance and cross-runtime support
541 lines (531 loc) • 22.6 kB
TypeScript
/**
* Generic logger interface for any logging implementation.
* Provides standardized logging methods compatible with console, winston, pino, etc.
*/
interface Logger {
/**
* Log informational messages about normal operations.
*/
info(message?: unknown, ...optionalParams: unknown[]): void;
/**
* Log warning messages about potentially problematic situations.
*/
warn(message?: unknown, ...optionalParams: unknown[]): void;
/**
* Log detailed debugging information.
*/
debug(message?: unknown, ...optionalParams: unknown[]): void;
/**
* Log error messages about failed operations.
*/
error(message?: unknown, ...optionalParams: unknown[]): void;
}
/**
* Log levels in order of priority (lowest to highest)
*/
declare enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
}
/**
* Structured logger utility class that provides consistent logging format and optional level filtering
* @template TContext - The log context type structure, defaults to a flexible object type
*/
declare class StructuredLogger<TContext = Record<string, unknown>> {
private logger?;
private minLevel;
private prefix?;
constructor(logger?: Logger, minLevel?: LogLevel, prefix?: string);
/**
* Log debug information about operations
*/
debug(operation: string, context?: TContext, details?: string): void;
/**
* Log informational messages about successful operations
*/
info(operation: string, context?: TContext, details?: string): void;
/**
* Log warning messages about potentially problematic situations
*/
warn(operation: string, context?: TContext, details?: string): void;
/**
* Log error messages about failed operations
*/
error(operation: string, context?: TContext, details?: string): void;
/**
* Handle promise rejections with proper error logging
*/
handleAsyncError: (operation: string, context?: TContext) => (error: unknown) => void;
/**
* Check if a log level should be output based on minimum level setting
*/
private shouldLog;
/**
* Create a new logger instance with a different minimum level
*/
withLevel(minLevel: LogLevel): StructuredLogger<TContext>;
/**
* Create a new logger instance with a different prefix
*/
withPrefix(prefix: string): StructuredLogger<TContext>;
}
/**
* Helper function to create a structured logger instance
* @template TContext - The log context type structure
*/
declare function createLogger<TContext = Record<string, unknown>>(logger?: Logger, minLevel?: LogLevel, prefix?: string): StructuredLogger<TContext>;
/**
* HTTP header names and cache-status values for SharedCache.
*
* Cache-key domain constants (`DEFAULT_CACHE_KEY_RULES`, `CANNOT_INCLUDE_HEADERS`)
* live in `key.ts` and are re-exported from the package entry.
*/
/** HTTP header name for cache status information. */
declare const CACHE_STATUS_HEADER_NAME = "x-cache-status";
/** HTTP header name for debugging cache key information. */
declare const CACHE_KEY_HEADER_NAME = "x-cache-key";
/** Canonical cache status literals. */
declare const SHARED_CACHE_STATUS: {
/** Response served from cache without validation */
readonly HIT: "HIT";
/** Response not found in cache, fetched from origin */
readonly MISS: "MISS";
/** Cached response was expired, fresh response fetched */
readonly EXPIRED: "EXPIRED";
/** Stale response served when origin is unreachable (stale-if-error) */
readonly STALE: "STALE";
/** Expired response served while revalidating in the background */
readonly UPDATING: "UPDATING";
/** Cache was bypassed due to cache-control directives */
readonly BYPASS: "BYPASS";
/** Cached response was revalidated and determined still fresh */
readonly REVALIDATED: "REVALIDATED";
/** Response is dynamic and cannot be cached */
readonly DYNAMIC: "DYNAMIC";
};
/** Cache status values as defined in HTTP caching standards. */
type CacheStatus = (typeof SHARED_CACHE_STATUS)[keyof typeof SHARED_CACHE_STATUS];
declare const HIT: "HIT";
declare const MISS: "MISS";
declare const EXPIRED: "EXPIRED";
declare const STALE: "STALE";
declare const UPDATING: "UPDATING";
declare const BYPASS: "BYPASS";
declare const REVALIDATED: "REVALIDATED";
declare const DYNAMIC: "DYNAMIC";
/**
* Package-level type barrel. Domain types (cache key, logger) are defined in
* their modules and re-exported here; `index.ts` exposes types only via this file.
*
* Constants: HTTP/status → `constants.ts`; key defaults → `key.ts` (`DEFAULT_CACHE_KEY_RULES`).
*/
/**
* Filter options for controlling which keys to include/exclude in cache key generation.
*/
interface KeyFilterOptions {
/** Array of keys to explicitly include in the cache key */
include?: string[];
/** Array of keys to explicitly exclude from the cache key */
exclude?: string[];
/** Array of keys to check for presence only (value set to empty string) */
checkPresence?: string[];
}
/**
* Configuration rules for generating cache keys.
* Each property can be `true`, `false`, or {@link KeyFilterOptions}.
*/
interface CacheKeyRules {
cookie?: KeyFilterOptions | boolean;
device?: KeyFilterOptions | boolean;
header?: KeyFilterOptions | boolean;
search?: KeyFilterOptions | boolean;
}
/** Cache key generator with an optional synchronous fast path for URL-only rules. */
interface CacheKeyGenerator {
(request: Request, cacheKeyRules?: CacheKeyRules): Promise<string>;
sync: (request: Request, cacheKeyRules?: CacheKeyRules) => string | undefined;
}
/**
* Log context structure for SharedCache operations
*/
interface CacheLogContext {
/** The URL being processed */
url?: string;
/** Cache key involved in the operation */
cacheKey?: string;
/** HTTP status code */
status?: number;
/** Operation duration in milliseconds */
duration?: number;
/** Error object if applicable */
error?: unknown;
/** Cache hit/miss/stale status */
cacheStatus?: string;
/** TTL value in seconds */
ttl?: number;
/** Request method */
method?: string;
/** Additional context data */
[key: string]: unknown;
}
type WebCache = globalThis.Cache;
type WebCacheQueryOptions = globalThis.CacheQueryOptions;
type WebCacheStorage = globalThis.CacheStorage;
type WebRequest = globalThis.Request;
type WebRequestInit = globalThis.RequestInit;
/**
* Configuration options for SharedCache instances.
* These options control caching behavior and key generation.
*/
interface SharedCacheOptions {
/**
* Rules for generating cache keys from requests.
* Controls which parts of the request are used in the cache key.
*/
cacheKeyRules?: CacheKeyRules;
/**
* Custom logger for debugging and monitoring cache operations.
*/
logger?: Logger;
}
/**
* Key-Value storage interface for cache persistence.
* This abstraction allows different storage backends (memory, Redis, etc.).
*/
interface KVStorage {
/**
* Retrieve a value from storage.
* @param cacheKey - The key to retrieve
* @returns The stored value or undefined if not found
*/
get: (cacheKey: string) => Promise<unknown | undefined>;
/**
* Store a value in storage with optional TTL.
* @param cacheKey - The key to store
* @param value - The value to store
* @param ttl - Time to live in seconds (optional)
*/
set: (cacheKey: string, value: unknown, ttl?: number) => Promise<void>;
/**
* Delete a value from storage.
* @param cacheKey - The key to delete
* @returns True if the key was deleted, false if it didn't exist
*/
delete: (cacheKey: string) => Promise<boolean>;
}
/**
* Extended cache query options for shared cache operations.
* Extends standard SharedCacheQueryOptions with shared cache specific options.
*/
type SharedCacheQueryOptions = WebCacheQueryOptions & {};
/**
* Type alias for fetch function compatible with shared cache.
*/
type SharedCacheFetch = (input: SharedCacheRequestInfo | URL, init?: SharedCacheRequestInit) => Promise<Response>;
type SharedCacheRequestInfo = Request | string;
type SharedCacheRequestInit = WebRequestInit & {
sharedCache?: SharedCacheRequestInitProperties;
};
type SharedCacheRequest = WebRequest & {
sharedCache?: SharedCacheRequestInitProperties;
};
/**
* Shared cache specific request properties.
* These properties control cache behavior on a per-request basis.
*/
interface SharedCacheRequestInitProperties {
/**
* Whether to expose the computed cache key via response header.
* When true, the response includes the `x-cache-key` header for debugging.
* Non-ASCII and control characters in the key are percent-encoded for valid HTTP headers.
*/
debugCacheKey?: boolean;
/**
* Override the cache-control header for caching decisions.
* This allows forcing specific cache behavior regardless of origin headers.
*/
cacheControlOverride?: string;
/**
* Custom cache key rules for this specific request.
* Overrides default cache key generation rules.
*/
cacheKeyRules?: CacheKeyRules;
/**
* Whether to ignore request cache-control headers.
* When true, request cache-control directives are ignored.
*/
ignoreRequestCacheControl?: boolean;
/**
* Whether to ignore Vary header processing.
* When true, Vary header is not considered for cache key generation.
*/
ignoreVary?: boolean;
/**
* Override the vary header for this request.
* Allows custom vary behavior regardless of response headers.
*/
varyOverride?: string;
/**
* Event instance to handle background operations (like stale-while-revalidate).
* The event.waitUntil() method will be called with promises that should be awaited in the background.
*/
event?: ExtendableEvent;
/**
* Function to handle background operations (like stale-while-revalidate).
* Called with promises that should be awaited in the background.
* @deprecated Use event instead. This option will be removed in a future version.
*/
waitUntil?: (promise: Promise<unknown>) => void;
}
/**
* Phase indicating why a cache origin handler is invoked.
*/
type CacheOriginPhase = 'miss' | 'revalidate';
/**
* Context passed to middleware-friendly cache origin handlers.
*/
interface CacheOriginContext {
/** Why the origin is being invoked. */
phase: CacheOriginPhase;
/** Present when invoked from a conditional revalidation request. */
revalidationRequest?: Request;
/** Abort signal from the outer resolve call or request. */
signal?: AbortSignal;
}
/**
* In-process origin handler for middleware integrations.
*
* @remarks
* - **miss**: throws propagate to the caller (framework error handling).
* - **revalidate**: throws are converted to 5xx responses for stale-if-error.
*/
type CacheOriginHandler = (request: Request, context: CacheOriginContext) => Response | Promise<Response>;
/**
* Options for {@link resolveWithCache} and {@link createCacheHandler}.
*/
type CacheResolveOptions = SharedCacheRequestInitProperties & {
signal?: AbortSignal;
};
interface CacheHandler {
resolve(request: Request, origin: CacheOriginHandler, options?: CacheResolveOptions): Promise<Response>;
}
/**
* SharedCache implements the Cache interface with additional features for shared caching.
* It provides HTTP-compliant caching with support for revalidation, stale-while-revalidate,
* and custom cache key generation.
*
* This implementation follows HTTP caching semantics as defined in RFC 7234 and related specifications.
*/
declare class SharedCache implements WebCache {
#private;
/**
* Creates a new SharedCache instance.
*
* @param storage - The key-value storage backend for persistence
* @param options - Configuration options for cache behavior
* @throws {TypeError} When storage is not provided
*/
constructor(storage: KVStorage, options?: SharedCacheOptions);
/**
* Computes the cache key for a request using the current cache key rules.
* Useful for debugging and diagnostics in callers that need to surface the key.
*
* @param request - Request to compute key for
* @returns Promise resolving to the computed cache key
*/
getCacheKey(request: SharedCacheRequestInfo): Promise<string>;
/**
* The add() method is not implemented in this cache implementation.
* This method is part of the Cache interface but not commonly used in practice.
*
* @param _request - The request to add (unused)
* @throws {Error} Always throws as this method is not implemented
*/
add(_request: SharedCacheRequestInfo): Promise<void>;
/**
* The addAll() method is not implemented in this cache implementation.
* This method is part of the Cache interface but not commonly used in practice.
*
* @param _requests - The requests to add (unused)
* @throws {Error} Always throws as this method is not implemented
*/
addAll(_requests: SharedCacheRequestInfo[]): Promise<void>;
/**
* The delete() method of the Cache interface finds the Cache entry whose key
* matches the request, and if found, deletes the Cache entry and returns a Promise
* that resolves to true. If no Cache entry is found, it resolves to false.
*
* This implementation follows the algorithm specified in the Cache API specification:
* https://w3c.github.io/ServiceWorker/#cache-delete
*
* @param request - The Request for which you are looking to delete. This can be a Request object or a URL.
* @param options - An object whose properties control how matching is done in the delete operation.
* @returns A Promise that resolves to true if the cache entry is deleted, or false otherwise.
*/
delete(request: SharedCacheRequestInfo, options?: SharedCacheQueryOptions): Promise<boolean>;
/**
* The keys() method is not implemented in this cache implementation.
* This method would return all Request objects that serve as keys for cached responses.
*
* @param _request - Optional request to match against (unused)
* @param _options - Optional query options (unused)
* @throws {Error} Always throws as this method is not implemented
*/
keys(_request?: SharedCacheRequestInfo, _options?: SharedCacheQueryOptions): Promise<readonly SharedCacheRequest[]>;
/**
* The match() method of the Cache interface returns a Promise that resolves
* to the Response associated with the first matching request in the Cache
* object. If no match is found, the Promise resolves to undefined.
*
* This implementation includes advanced features:
* - HTTP cache validation (ETag, Last-Modified)
* - Stale-while-revalidate support
* - Custom cache key generation
* - Proper Vary header handling
*
* @param request - The Request for which you are attempting to find responses in the Cache.
* This can be a Request object or a URL.
* @param options - An object that sets options for the match operation.
* @returns A Promise that resolves to the first Response that matches the request
* or to undefined if no match is found.
*/
match(request: SharedCacheRequestInfo, options?: SharedCacheQueryOptions): Promise<Response | undefined>;
/**
* The matchAll() method is not implemented in this cache implementation.
* This method would return all matching responses for a given request.
*
* @param _request - Optional request to match against (unused)
* @param _options - Optional query options (unused)
* @throws {Error} Always throws as this method is not implemented
*/
matchAll(_request?: SharedCacheRequestInfo, _options?: SharedCacheQueryOptions): Promise<readonly Response[]>;
/**
* The put() method of the Cache interface allows key/value pairs to be added
* to the current Cache object.
*
* This implementation includes several HTTP-compliant validations:
* - Only HTTP/HTTPS schemes are supported for GET requests
* - 206 (Partial Content) responses are rejected
* - Vary: * responses are rejected
* - Body usage validation to prevent corruption
*
* @param request - The Request object or URL that you want to add to the cache.
* @param response - The Response you want to match up to the request.
* @throws {TypeError} For various validation failures as per Cache API specification
*/
put(request: SharedCacheRequestInfo, response: Response): Promise<void>;
}
/**
* Named cache registry backed by a shared KV storage.
* Only `open()` is implemented; other CacheStorage methods throw.
*/
declare class SharedCacheStorage implements WebCacheStorage {
#private;
constructor(storage: KVStorage, options?: SharedCacheOptions);
delete(_cacheName: string): Promise<boolean>;
has(_cacheName: string): Promise<boolean>;
keys(): Promise<string[]>;
match(_request: RequestInfo, _options?: MultiCacheQueryOptions): Promise<Response | undefined>;
open(cacheName: string): Promise<SharedCache>;
}
/**
* Creates a fetch function with shared caching capabilities.
*
* This is the internal implementation that powers the `createFetch` export.
* Users should import and use `createFetch` instead of this function directly.
*
* This function implements HTTP caching semantics on top of the standard fetch API,
* providing automatic cache management with support for:
* - HTTP cache semantics (RFC 7234)
* - Conditional requests and revalidation
* - Stale-while-revalidate patterns
* - Custom cache control and vary header overrides
*
* The returned fetch function is compatible with the standard fetch API while
* adding transparent caching capabilities.
*
* @param cache - Optional SharedCache instance (defaults to global cache if available)
* @param options - Configuration options
* @param options.fetch - Custom fetch implementation (defaults to global fetch)
* @param options.defaults - Default shared cache options to apply to all requests
* @returns A fetch function with caching capabilities
*
* @example
* ```typescript
* import { createFetch, CacheStorage } from '@web-widget/shared-cache';
* import { LRUCache } from 'lru-cache';
*
* // Set up cache storage
* const caches = new CacheStorage(createLRUStorage());
* const cache = await caches.open('api-cache');
*
* // Create cached fetch with default configuration
* const fetch = createFetch(cache, {
* defaults: {
* cacheControlOverride: 's-maxage=300',
* ignoreRequestCacheControl: true,
* }
* });
*
* // Use the cached fetch
* const response = await fetch('/api/data');
* console.log(response.headers.get('x-cache-status')); // "MISS" or "HIT"
* ```
*/
declare function createSharedCacheFetch(cache?: SharedCache, options?: {
/** Custom fetch implementation to use as the underlying fetcher */
fetch?: typeof globalThis.fetch;
/** Default shared cache options to apply to all requests */
defaults?: Partial<SharedCacheRequestInitProperties>;
}): SharedCacheFetch;
/**
* Default shared cache fetch instance using global cache.
*
* This is a convenience export that creates a shared cache fetch function
* using the default configuration. It will automatically use the global
* cache storage if available.
*
* @deprecated
*/
declare const sharedCacheFetch: SharedCacheFetch;
/**
* Resolves a request through shared cache using an in-process origin handler.
*
* @remarks
* Origin error contract:
* - **miss**: throws propagate to the caller (framework `onError`).
* - **revalidate**: throws are converted to 5xx responses for `stale-if-error`.
*
* @throws When the origin throws during a cache miss.
*/
declare function resolveWithCache(cache: SharedCache, request: SharedCacheRequest, origin: CacheOriginHandler, options?: CacheResolveOptions): Promise<Response>;
/**
* Creates a reusable cache resolver for middleware-style origin handlers.
*
* Prefer this over `createFetch` when the origin is an in-process handler such as
* middleware `next()` rather than an outbound HTTP `fetch`.
*/
declare function createCacheHandler(cache: SharedCache, defaults?: CacheResolveOptions): CacheHandler;
/**
* List of HTTP headers that should not be included in cache keys.
*
* These headers are excluded for the following reasons:
* - High cardinality: Risk of cache fragmentation (Accept-*, User-Agent, Referer)
* - Cache/proxy features: Would interfere with caching logic (Cache-Control, If-*)
* - Covered by other features: Handled by dedicated cache key components (Cookie, Host)
* - Implementation details: Not relevant for cache key generation (Content-Length, Connection)
*
* Based on best practices from CDN implementations and HTTP caching specifications.
*/
declare const CANNOT_INCLUDE_HEADERS: readonly ["accept", "accept-charset", "accept-encoding", "accept-datetime", "accept-language", "referer", "user-agent", "connection", "content-length", "cache-control", "if-match", "if-modified-since", "if-none-match", "if-unmodified-since", "range", "upgrade", "cookie", "host", "vary", "x-cache-status", "x-cache-key"];
/**
* Default cache key generation rules.
*/
declare const DEFAULT_CACHE_KEY_RULES: CacheKeyRules;
/**
* Creates a cache key generator function with customizable rules.
*/
declare function createCacheKeyGenerator(cacheKeyNormalize?: boolean | CacheKeyNormalizeOptions): CacheKeyGenerator;
export { BYPASS, CACHE_KEY_HEADER_NAME, CACHE_STATUS_HEADER_NAME, CANNOT_INCLUDE_HEADERS, SharedCache as Cache, type CacheHandler, type CacheKeyGenerator, type CacheKeyRules, type CacheLogContext, type CacheOriginContext, type CacheOriginHandler, type CacheOriginPhase, type CacheResolveOptions, type CacheStatus, SharedCacheStorage as CacheStorage, DEFAULT_CACHE_KEY_RULES, DYNAMIC, EXPIRED, HIT, type KVStorage, type KeyFilterOptions, LogLevel, type Logger, MISS, REVALIDATED, SHARED_CACHE_STATUS, STALE, type SharedCacheFetch, type SharedCacheOptions, type SharedCacheQueryOptions, type SharedCacheRequestInitProperties, StructuredLogger, UPDATING, createCacheHandler, createCacheKeyGenerator, createSharedCacheFetch as createFetch, createLogger, sharedCacheFetch as fetch, resolveWithCache };