返回 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 import type { MerchEnv } from "./merch/types";
7
8 const MEM = new Map<string, string>();
9
10 export interface KVNamespace {
11 get(key: string): Promise<string | null>;
12 put(key: string, value: string, opts?: { expirationTtl?: number }): Promise<void>;
13 list(opts?: { prefix?: string; limit?: number; cursor?: string }): Promise<{ keys: { name: string }[]; cursor?: string; list_complete?: boolean }>;
14 delete(key: string): Promise<void>;
15 }
16
17 /** Native KV streaming reads keep signed facts bounded before allocation. */
18 export interface KVStreamNamespace {
19 get(key: string, type: "stream"): Promise<ReadableStream<Uint8Array> | null>;
20 put: KVNamespace["put"];
21 }
22
23 interface CloudflareEnv extends MerchEnv {
24 CURATED_KV?: KVNamespace & KVStreamNamespace;
25 MERCH_INTEREST_LIMITER?: { limit(options: { key: string }): Promise<{ success: boolean }> };
26 MERCH_INTEREST_SITE_ORIGIN?: string;
27 DEEPSEEK_API_KEY?: string;
28 DEEPSEEK_BASE_URL?: string;
29 DEEPSEEK_MODEL?: string;
30 GITHUB_TOKEN?: string;
31 CRON_SECRET?: string;
32 GITHUB_REPO?: string;
33 /** Cloud facts (facts/v1): Supabase Data API URL + publishable (anon) key. Never a service key. */
34 SUPABASE_URL?: string;
35 SUPABASE_PUBLISHABLE_KEY?: string;
36 }
37
38 function envFromProcess(): CloudflareEnv {
39 return {
40 MERCH_CONFIG_JSON: process.env.MERCH_CONFIG_JSON,
41 MERCH_INTEREST_SITE_ORIGIN: process.env.MERCH_INTEREST_SITE_ORIGIN,
42 MERCH_STRIPE_KEY: process.env.MERCH_STRIPE_KEY,
43 MERCH_STRIPE_WEBHOOK_SECRET: process.env.MERCH_STRIPE_WEBHOOK_SECRET,
44 MERCH_PAYMENT_CONFIGURATION: process.env.MERCH_PAYMENT_CONFIGURATION,
45 YOYCOL_ACCESS_KEY: process.env.YOYCOL_ACCESS_KEY,
46 YOYCOL_SECRET_KEY: process.env.YOYCOL_SECRET_KEY,
47 SUPABASE_URL: process.env.SUPABASE_URL,
48 SUPABASE_PUBLISHABLE_KEY: process.env.SUPABASE_PUBLISHABLE_KEY,
49 DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
50 DEEPSEEK_BASE_URL: process.env.DEEPSEEK_BASE_URL,
51 DEEPSEEK_MODEL: process.env.DEEPSEEK_MODEL,
52 GITHUB_TOKEN: process.env.GITHUB_TOKEN,
53 CRON_SECRET: process.env.CRON_SECRET,
54 GITHUB_REPO: process.env.GITHUB_REPO,
55 };
56 }
57
58 export async function getEnv(): Promise<CloudflareEnv> {
59 if (process.env.NEXT_PHASE === "phase-production-build") {
60 return envFromProcess();
61 }
62
63 try {
64 const mod = await import("@opennextjs/cloudflare");
65 const ctx = await mod.getCloudflareContext({ async: true });
66 return ctx.env as CloudflareEnv;
67 } catch {
68 return envFromProcess();
69 }
70 }
71
72 export async function getDispatch(): Promise<CuratedDispatch | null> {
73 const env = await getEnv();
74 const raw = env.CURATED_KV ? await env.CURATED_KV.get("dispatch:latest") : MEM.get("dispatch:latest") ?? null;
75 if (!raw) return null;
76 try {
77 return JSON.parse(raw) as CuratedDispatch;
78 } catch {
79 return null;
80 }
81 }
82
83 export async function putDispatch(d: CuratedDispatch): Promise<void> {
84 const env = await getEnv();
85 await putDispatchWithKv(env.CURATED_KV, d);
86 }
87
88 export async function putDispatchWithKv(kv: KVNamespace | undefined, d: CuratedDispatch): Promise<void> {
89 const value = JSON.stringify(d);
90 if (kv) {
91 await kv.put("dispatch:latest", value, { expirationTtl: 60 * 60 * 24 * 7 });
92 } else {
93 MEM.set("dispatch:latest", value);
94 }
95 }
96
96 lines TYPESCRIPT