All files / lib edge-cache.ts

100% Statements 29/29
100% Branches 18/18
100% Functions 7/7
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94                                    5x 5x                   7x 5x 3x                   5x 2x 2x 2x                         2x 1x 1x 1x 1x       1x                 12x 1x 1x                 5x 1x   1x 1x 3x      
// Cloudflare Cache API helpers for edge-level response caching.
// The Cache API (`caches.default`) stores Response objects at the edge CDN.
// Unlike KV or D1, cached responses are served before the Worker executes.
//
// IMPORTANT: The Cache API is only available in production Cloudflare Workers.
// In local dev (`wrangler dev --local`) and tests, `caches` is undefined.
// All helpers gracefully degrade to no-ops in those environments.
 
/**
 * Detect local development: wrangler dev now provides `caches` but we
 * don't want edge caching to hide fresh data during development.
 * In production, the Worker URL uses the real domain (e.g. api.interioring.com).
 * In local dev, it uses localhost or 127.0.0.1.
 */
function isLocalDev(request?: Request): boolean {
	/* v8 ignore start -- V8 artifact: callers always pass request */
	if (!request) return false;
	/* v8 ignore stop */
	const url = new URL(request.url);
	return (
		url.hostname === "localhost" || url.hostname === "127.0.0.1"
	);
}
 
/**
 * Check if the Cache API is available and should be used.
 * Disabled in local dev to ensure fresh data during development.
 */
function isCacheAvailable(request?: Request): boolean {
	if (typeof caches === "undefined") return false;
	if (request && isLocalDev(request)) return false;
	return true;
}
 
/**
 * Attempt to retrieve a cached response from the Cloudflare edge cache.
 * Returns null if the Cache API is unavailable, in local dev, or no match found.
 */
export async function getEdgeCachedResponse(
	request: Request,
): Promise<Response | null> {
	if (!isCacheAvailable(request)) return null;
	const cache = caches.default;
	const match = await cache.match(request);
	return match ?? null;
}
 
/**
 * Store a response in the Cloudflare edge cache with the given TTL (seconds).
 * The response is cloned with an explicit Cache-Control header so the edge
 * knows how long to keep it. No-op in local dev.
 */
export async function putEdgeCacheResponse(
	request: Request,
	response: Response,
	ttl: number,
): Promise<void> {
	if (!isCacheAvailable(request)) return;
	const cache = caches.default;
	const headers = new Headers(response.headers);
	headers.set("Cache-Control", `public, max-age=${ttl}`);
	const cached = new Response(response.body, {
		status: response.status,
		headers,
	});
	await cache.put(request, cached);
}
 
/**
 * Purge a single URL from the Cloudflare edge cache.
 * Useful when pro data changes and stale marketplace responses
 * should be evicted. No-op in local dev.
 */
export async function purgeEdgeCache(url: string): Promise<void> {
	if (typeof caches === "undefined") return;
	const cache = caches.default;
	await cache.delete(new Request(url));
}
 
/**
 * Purge all marketplace-related edge cache entries for a given base URL.
 * Clears the main list endpoints that are most likely stale after a pro
 * or project change.
 */
export async function purgeMarketplaceCache(baseUrl: string): Promise<void> {
	if (typeof caches === "undefined") return;
	const cache = caches.default;
	// Purge known high-traffic list endpoints
	const paths = ["/api/marketplace/pros", "/api/marketplace/projects", "/api/marketplace/blogs"];
	await Promise.all(
		paths.map((path) => cache.delete(new Request(`${baseUrl}${path}`))),
	);
}