返回 CodeWhale
kv.ts
根目录 / web / lib / kv.ts
1 /**
2 * Cloudflare KV access via the OpenNext binding helper.
3 * Falls back to in-memory cache for `next dev` outside of `wrangler dev`.
4 */
5 import type { CuratedDispatch } from "./types";
6
7 const MEM = new Map<string, string>();
8
9 export interface KVNamespace {
10 get(key: string): Promise<string | null>;
11 put(key: string, value: string, opts?: { expirationTtl?: number }): Promise<void>;
12 list(opts?: { prefix?: string; limit?: number }): Promise<{ keys: { name: string }[] }>;
13 delete(key: string): Promise<void>;
14 }
15
16 /** Native KV streaming reads keep signed facts bounded before allocation. */
17 export interface KVStreamNamespace {
18 get(key: string, type: "stream"): Promise<ReadableStream<Uint8Array> | null>;
19 put: KVNamespace["put"];
20 }
21
22 interface CloudflareEnv {
23 CURATED_KV?: KVNamespace & KVStreamNamespace;
24 DEEPSEEK_API_KEY?: string;
25 DEEPSEEK_BASE_URL?: string;
26 DEEPSEEK_MODEL?: string;
27 GITHUB_TOKEN?: string;
28 CRON_SECRET?: string;
29 GITHUB_REPO?: string;
30 /** Cloud facts (facts/v1): Supabase Data API URL + publishable (anon) key. Never a service key. */
31 SUPABASE_URL?: string;
32 SUPABASE_PUBLISHABLE_KEY?: string;
33 }
34
35 function envFromProcess(): CloudflareEnv {
36 return {
37 SUPABASE_URL: process.env.SUPABASE_URL,
38 SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY,
39 DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
40 DEEPSEEK_BASE_URL: process.env.DEEPSEEK_BASE_URL,
41 DEEPSEEK_MODEL: process.env.DEEPSEEK_MODEL,
42 GITHUB_TOKEN: process.env.GITHUB_TOKEN,
43 CRON_SECRET: process.env.CRON_SECRET,
44 GITHUB_REPO: process.env.GITHUB_REPO,
45 };
46 }
47
48 export async function getEnv(): Promise<CloudflareEnv> {
49 if (process.env.NEXT_PHASE === "phase-production-build") {
50 return envFromProcess();
51 }
52
53 try {
54 const mod = await import("@opennextjs/cloudflare");
55 const ctx = await mod.getCloudflareContext({ async: true });
56 return ctx.env as CloudflareEnv;
57 } catch {
58 return envFromProcess();
59 }
60 }
61
62 export async function getDispatch(): Promise<CuratedDispatch | null> {
63 const env = await getEnv();
64 const raw = env.CURATED_KV ? await env.CURATED_KV.get("dispatch:latest") : MEM.get("dispatch:latest") ?? null;
65 if (!raw) return null;
66 try {
67 return JSON.parse(raw) as CuratedDispatch;
68 } catch {
69 return null;
70 }
71 }
72
73 export async function putDispatch(d: CuratedDispatch): Promise<void> {
74 const env = await getEnv();
75 await putDispatchWithKv(env.CURATED_KV, d);
76 }
77
78 export async function putDispatchWithKv(kv: KVNamespace | undefined, d: CuratedDispatch): Promise<void> {
79 const value = JSON.stringify(d);
80 if (kv) {
81 await kv.put("dispatch:latest", value, { expirationTtl: 60 * 60 * 24 * 7 });
82 } else {
83 MEM.set("dispatch:latest", value);
84 }
85 }
86
86 lines TYPESCRIPT