77 lines
1.9 KiB
TypeScript

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<string, unknown>,
): Promise<string> {
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<unknown> {
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<T>(
key: string,
data: T,
ttlSeconds: number,
): Promise<void> {
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<string> {
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("");
}