mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
197 lines
4.6 KiB
Markdown
197 lines
4.6 KiB
Markdown
# KV Patterns & Best Practices
|
|
|
|
## Multi-Tier Caching
|
|
|
|
```typescript
|
|
// Memory → KV → Origin (3-tier cache)
|
|
const memoryCache = new Map<string, { data: any; expires: number }>();
|
|
|
|
async function getCached(env: Env, key: string): Promise<any> {
|
|
const now = Date.now();
|
|
|
|
// L1: Memory cache (fastest)
|
|
const cached = memoryCache.get(key);
|
|
if (cached && cached.expires > now) {
|
|
return cached.data;
|
|
}
|
|
|
|
// L2: KV cache (fast)
|
|
const kvValue = await env.CACHE.get(key, "json");
|
|
if (kvValue) {
|
|
memoryCache.set(key, { data: kvValue, expires: now + 60000 }); // 1min in memory
|
|
return kvValue;
|
|
}
|
|
|
|
// L3: Origin (slow)
|
|
const origin = await fetch(`https://api.example.com/${key}`).then(r => r.json());
|
|
|
|
// Backfill caches
|
|
await env.CACHE.put(key, JSON.stringify(origin), { expirationTtl: 300 }); // 5min in KV
|
|
memoryCache.set(key, { data: origin, expires: now + 60000 });
|
|
|
|
return origin;
|
|
}
|
|
```
|
|
|
|
## API Response Caching
|
|
|
|
```typescript
|
|
async function getCachedData(env: Env, key: string, fetcher: () => Promise<any>): Promise<any> {
|
|
const cached = await env.MY_KV.get(key, "json");
|
|
if (cached) return cached;
|
|
|
|
const data = await fetcher();
|
|
await env.MY_KV.put(key, JSON.stringify(data), { expirationTtl: 300 });
|
|
return data;
|
|
}
|
|
|
|
const apiData = await getCachedData(
|
|
env,
|
|
"cache:users",
|
|
() => fetch("https://api.example.com/users").then(r => r.json())
|
|
);
|
|
```
|
|
|
|
## Session Management
|
|
|
|
```typescript
|
|
interface Session { userId: string; expiresAt: number; }
|
|
|
|
async function createSession(env: Env, userId: string): Promise<string> {
|
|
const sessionId = crypto.randomUUID();
|
|
const expiresAt = Date.now() + (24 * 60 * 60 * 1000);
|
|
|
|
await env.SESSIONS.put(
|
|
`session:${sessionId}`,
|
|
JSON.stringify({ userId, expiresAt }),
|
|
{ expirationTtl: 86400, metadata: { createdAt: Date.now() } }
|
|
);
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
async function getSession(env: Env, sessionId: string): Promise<Session | null> {
|
|
const data = await env.SESSIONS.get<Session>(`session:${sessionId}`, "json");
|
|
if (!data || data.expiresAt < Date.now()) return null;
|
|
return data;
|
|
}
|
|
```
|
|
|
|
## Coalesce Cold Keys
|
|
|
|
```typescript
|
|
// ❌ BAD: Many individual keys
|
|
await env.KV.put("user:123:name", "John");
|
|
await env.KV.put("user:123:email", "john@example.com");
|
|
|
|
// ✅ GOOD: Single coalesced object
|
|
await env.USERS.put("user:123:profile", JSON.stringify({
|
|
name: "John",
|
|
email: "john@example.com",
|
|
role: "admin"
|
|
}));
|
|
|
|
// Benefits: Hot key cache, single read, reduced operations
|
|
// Trade-off: Harder to update individual fields
|
|
```
|
|
|
|
## Prefix-Based Namespacing
|
|
|
|
```typescript
|
|
// Logical partitioning within single namespace
|
|
const PREFIXES = {
|
|
users: "user:",
|
|
sessions: "session:",
|
|
cache: "cache:",
|
|
features: "feature:"
|
|
} as const;
|
|
|
|
// Write with prefix
|
|
async function setUser(env: Env, id: string, data: any) {
|
|
await env.KV.put(`${PREFIXES.users}${id}`, JSON.stringify(data));
|
|
}
|
|
|
|
// Read with prefix
|
|
async function getUser(env: Env, id: string) {
|
|
return await env.KV.get(`${PREFIXES.users}${id}`, "json");
|
|
}
|
|
|
|
// List by prefix
|
|
async function listUserIds(env: Env): Promise<string[]> {
|
|
const result = await env.KV.list({ prefix: PREFIXES.users });
|
|
return result.keys.map(k => k.name.replace(PREFIXES.users, ""));
|
|
}
|
|
|
|
// Example hierarchy
|
|
"user:123:profile"
|
|
"user:123:settings"
|
|
"cache:api:users"
|
|
"session:abc-def"
|
|
"feature:flags:beta"
|
|
```
|
|
|
|
## Metadata Versioning
|
|
|
|
```typescript
|
|
interface VersionedData {
|
|
version: number;
|
|
data: any;
|
|
}
|
|
|
|
async function migrateIfNeeded(env: Env, key: string) {
|
|
const result = await env.DATA.getWithMetadata(key, "json");
|
|
|
|
if (!result.value) return null;
|
|
|
|
const currentVersion = result.metadata?.version || 1;
|
|
const targetVersion = 2;
|
|
|
|
if (currentVersion < targetVersion) {
|
|
// Migrate data format
|
|
const migrated = migrate(result.value, currentVersion, targetVersion);
|
|
|
|
// Store with new version
|
|
await env.DATA.put(key, JSON.stringify(migrated), {
|
|
metadata: { version: targetVersion, migratedAt: Date.now() }
|
|
});
|
|
|
|
return migrated;
|
|
}
|
|
|
|
return result.value;
|
|
}
|
|
|
|
function migrate(data: any, from: number, to: number): any {
|
|
if (from === 1 && to === 2) {
|
|
// V1 → V2: Rename field
|
|
return { ...data, userName: data.name };
|
|
}
|
|
return data;
|
|
}
|
|
```
|
|
|
|
## Error Boundary Pattern
|
|
|
|
```typescript
|
|
// Resilient get with fallback
|
|
async function resilientGet<T>(
|
|
env: Env,
|
|
key: string,
|
|
fallback: T
|
|
): Promise<T> {
|
|
try {
|
|
const value = await env.KV.get<T>(key, "json");
|
|
return value ?? fallback;
|
|
} catch (err) {
|
|
console.error(`KV error for ${key}:`, err);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const config = await resilientGet(env, "config:app", {
|
|
theme: "light",
|
|
maxItems: 10
|
|
});
|
|
```
|