-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcache.ts
More file actions
88 lines (78 loc) · 2.76 KB
/
Copy pathcache.ts
File metadata and controls
88 lines (78 loc) · 2.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Version-stamped read-through cache over Workers KV.
*
* Every cached read is keyed `c:<ns>:<version>:<key>`. To invalidate a
* namespace we simply bump its integer version — every old key becomes
* unreachable and expires on its TTL. No key enumeration, no purge API,
* and reads after a write are guaranteed fresh (new version → cache miss).
*/
const DEFAULT_TTL = 600; // seconds — bounds orphaned-key lifetime only
const versionKey = (ns: string) => `cv:${ns}`;
const dataKey = (ns: string, v: string, key: string) => `c:${ns}:${v}:${key}`;
async function currentVersion(cache: KVNamespace, ns: string): Promise<string> {
return (await cache.get(versionKey(ns))) ?? "1";
}
/** Read through the cache; on miss, run `loader`, store and return it. */
export async function cached<T>(
env: Env | null,
ns: string,
key: string,
loader: () => Promise<T>,
ttl: number = DEFAULT_TTL,
shouldCache?: (data: T) => boolean,
): Promise<T> {
const cache = env?.CACHE;
if (!cache) return loader(); // no KV bound (e.g. bare unit test) → straight through
const v = await currentVersion(cache, ns);
const k = dataKey(ns, v, key);
const hit = await cache.get(k);
if (hit !== null) {
try {
return JSON.parse(hit) as T;
} catch {
/* corrupt entry — fall through and refresh */
}
}
const data = await loader();
// Don't cache empty/undefined payloads as authoritative. Callers can refine
// with `shouldCache` — e.g. search skips caching empty result sets so a query
// run during Vectorize's eventual-consistency window can't pin a stale "no
// results" for the whole TTL.
const storable =
data !== undefined && data !== null && (shouldCache ? shouldCache(data) : true);
if (storable) {
await cache.put(k, JSON.stringify(data), { expirationTtl: ttl });
}
return data;
}
/** Invalidate one namespace by bumping its version. */
export async function invalidate(env: Env | null, ns: string): Promise<void> {
const cache = env?.CACHE;
if (!cache) return;
const next = parseInt((await cache.get(versionKey(ns))) ?? "1", 10) + 1;
await cache.put(versionKey(ns), String(next));
}
/** Invalidate several namespaces (used after admin writes). */
export async function invalidateMany(
env: Env | null,
namespaces: string[],
): Promise<void> {
await Promise.all(namespaces.map((ns) => invalidate(env, ns)));
}
/** Namespaces used across the app. "home" is the aggregate landing payload. */
export const NS = {
settings: "settings",
events: "events",
posts: "posts",
links: "links",
resources: "resources",
recordings: "recordings",
gallery: "gallery",
team: "team",
social: "social",
home: "home",
communities: "communities",
companies: "companies",
speakers: "speakers",
search: "search",
} as const;