1/**
2 * Deploy-time Cloudflare KV sink for prerender seed.
3 *
4 * Runtime Workers use {@link kvAdapter} (`env.VINEXT_KV_CACHE` binding).
5 * Deploy cannot call that binding — it talks the distilled KV HTTP API
6 * with `accountId` + `namespaceId` from the Worker env. Both write the
7 * same {@link DataCacheStore} shape (`putText`) so Redis / S3 / KV seed
8 * through one function.
9 */
10import * as kv from "@distilled.cloud/cloudflare/kv";
11import * as Effect from "effect/Effect";
12import { vinextCacheNamespaceFromEnv } from "../PrerenderCache.ts";
13import type { SeedSink } from "./seed.ts";
14
15/** Cloudflare KV rejects expirationTtl below 60 seconds. */
16const MIN_KV_TTL_SECONDS = 60;
17
18export type KvHttpNamespace = {
19 readonly accountId: string;
20 readonly namespaceId: string;
21};
22
23export const kvHttpNamespaceFromEnv = (
24 env: Record<string, unknown>,
25): KvHttpNamespace | undefined => {
26 const namespace = vinextCacheNamespaceFromEnv(env);
27 if (namespace === undefined || namespace.accountId === undefined) {
28 return undefined;
29 }
30 return {
31 accountId: namespace.accountId,
32 namespaceId: namespace.namespaceId,
33 };
34};
35
36const ttlSeconds = (ttlMs: number | undefined): number | undefined => {
37 if (ttlMs === undefined || ttlMs <= 0) return undefined;
38 return Math.max(MIN_KV_TTL_SECONDS, Math.ceil(ttlMs / 1000));
39};
40
41/**
42 * Effect sink over distilled `putNamespaceValue`. Requires the Worker
43 * provider's Cloudflare KV layer (same as the previous inline seed).
44 */
45export const kvHttpSink = (
46 namespace: KvHttpNamespace,
47): SeedSink<kv.PutNamespaceValueError, kv.CloudflareOpContext> => ({
48 putText: (key, value, ttlMs) => {
49 const expirationTtl = ttlSeconds(ttlMs);
50 return kv.putNamespaceValue({
51 accountId: namespace.accountId,
52 namespaceId: namespace.namespaceId,
53 keyName: key,
54 value,
55 ...(expirationTtl !== undefined ? { expirationTtl } : {}),
56 });
57 },
58});