import { env } from "cloudflare:workers"; import { sortBy } from "remeda"; /** * Cache TTL constants in seconds. */ export const CACHE_TTL = { /** Related keyword research results */ researchResult: 86400, } as const; const CACHE_PREFIX = "dataforseo-cache/"; /** * Build a deterministic cache key from an endpoint slug and input params. * Uses a SHA-256 digest for stability across runtimes. */ export async function buildCacheKey( prefix: string, params: Record, ): Promise { const raw = JSON.stringify( Object.fromEntries(sortBy(Object.entries(params), ([key]) => key)), ); return `${prefix}:${await sha256Hex(raw)}`; } /** * Get a cached JSON value from R2. Returns null on miss or expiry. * Callers should validate the shape with Zod before trusting it — schema * drift between writes and reads is otherwise silent. */ export async function getCached(key: string): Promise { const obj = await env.R2.get(`${CACHE_PREFIX}${key}`); if (!obj) return null; const expiresAt = obj.customMetadata?.expiresAt; if (expiresAt && Date.parse(expiresAt) < Date.now()) return null; try { return JSON.parse(await obj.text()); } catch { return null; } } /** * Store a JSON value in R2 with a soft TTL via custom metadata. */ export async function setCached( key: string, data: T, ttlSeconds: number, ): Promise { await env.R2.put(`${CACHE_PREFIX}${key}`, JSON.stringify(data), { httpMetadata: { contentType: "application/json" }, customMetadata: { expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(), }, }); } /** * Compute a deterministic SHA-256 digest for cache keys. */ async function sha256Hex(input: string): Promise { const digest = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(input), ); return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0"), ).join(""); }