1/**
2 * vinext data-cache adapter backed by a Cloudflare KV namespace.
3 * Default export is the factory `virtual:vinext-cache-adapters` calls as
4 * `({ env, options }) => CacheHandler`.
5 *
6 * Reads `env[binding]` (default `VINEXT_KV_CACHE`). Same codec as Redis
7 * and S3 via {@link makeDataCacheHandler}. Prerender pairs are uploaded
8 * at `alchemy deploy` — this runtime does not walk the filesystem.
9 */
10import { makeDataCacheHandler, type DataCacheStore } from "./handler.ts";
11
12export interface KvAdapterOptions extends Record<string, unknown> {
13 /** Worker binding name. @default "VINEXT_KV_CACHE" */
14 readonly binding?: string;
15 readonly appPrefix?: string;
16 readonly ttlSeconds?: number;
17 readonly tagCacheTtlMs?: number;
18}
19
20const DEFAULT_BINDING = "VINEXT_KV_CACHE";
21/** Cloudflare KV rejects expirationTtl below 60 seconds. */
22const MIN_KV_TTL_SECONDS = 60;
23
24interface KvNamespaceLike {
25 get(key: string): Promise<string | null>;
26 put(
27 key: string,
28 value: string,
29 options?: { expirationTtl?: number },
30 ): Promise<void>;
31 delete(key: string): Promise<void>;
32}
33
34const isKvNamespace = (value: unknown): value is KvNamespaceLike =>
35 typeof value === "object" &&
36 value !== null &&
37 typeof (value as KvNamespaceLike).get === "function" &&
38 typeof (value as KvNamespaceLike).put === "function" &&
39 typeof (value as KvNamespaceLike).delete === "function";
40
41const kvStore = (ns: KvNamespaceLike): DataCacheStore => ({
42 async getText(key) {
43 return (await ns.get(key)) ?? undefined;
44 },
45 async putText(key, value, ttlMs) {
46 const expirationTtl =
47 ttlMs !== undefined && ttlMs > 0
48 ? Math.max(MIN_KV_TTL_SECONDS, Math.ceil(ttlMs / 1000))
49 : undefined;
50 await ns.put(
51 key,
52 value,
53 expirationTtl !== undefined ? { expirationTtl } : undefined,
54 );
55 },
56 async delete(key) {
57 await ns.delete(key);
58 },
59});
60
61const createKvDataCacheAdapter = ({
62 env,
63 options,
64}: {
65 env?: Record<string, unknown>;
66 options?: KvAdapterOptions;
67}) => {
68 const binding = options?.binding ?? DEFAULT_BINDING;
69 const ns = env?.[binding];
70 if (!isKvNamespace(ns)) {
71 throw new Error(
72 `[vinext] The KV data cache adapter requires Worker binding \`${binding}\`.`,
73 );
74 }
75 return makeDataCacheHandler(kvStore(ns), options);
76};
77
78export default createKvDataCacheAdapter;