Files
gh-jezweb-claude-skills-ski…/templates/kv-caching-pattern.ts
2025-11-30 08:24:21 +08:00

519 lines
12 KiB
TypeScript

/**
* Cloudflare Workers KV - Caching Pattern
*
* This template demonstrates optimal caching patterns with KV:
* - Cache-aside pattern (read-through cache)
* - Write-through cache
* - CacheTtl optimization for edge caching
* - Stale-while-revalidate pattern
* - Cache invalidation
*/
import { Hono } from 'hono';
type Bindings = {
CACHE: KVNamespace;
DB: D1Database; // Example: database for cache misses
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Cache-Aside Pattern (Read-Through Cache)
// ============================================================================
/**
* Generic cache-aside helper
*
* 1. Try to read from cache
* 2. On miss, fetch from source
* 3. Store in cache
* 4. Return data
*/
async function getCached<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
options: {
ttl?: number; // KV expiration (default: 3600)
cacheTtl?: number; // Edge cache TTL (default: 300)
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600; // 1 hour default
const cacheTtl = options.cacheTtl ?? 300; // 5 minutes default
// Try cache first (with edge caching)
const cached = await kv.get<T>(cacheKey, {
type: 'json',
cacheTtl: Math.max(cacheTtl, 60), // Minimum 60 seconds
});
if (cached !== null) {
return cached;
}
// Cache miss - fetch from source
const data = await fetchFn();
// Store in cache (fire-and-forget)
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: Math.max(ttl, 60), // Minimum 60 seconds
});
return data;
}
// Example: Cache API response
app.get('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const cacheKey = `user:${userId}`;
try {
const user = await getCached(
c.env.CACHE,
cacheKey,
async () => {
// Simulate database fetch
const result = await c.env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
)
.bind(userId)
.first();
if (!result) {
throw new Error('User not found');
}
return result;
},
{
ttl: 3600, // Cache in KV for 1 hour
cacheTtl: 300, // Cache at edge for 5 minutes
}
);
return c.json({
success: true,
user,
cached: true,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Write-Through Cache Pattern
// ============================================================================
/**
* Write-through cache: Update cache when data changes
*/
app.put('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const userData = await c.req.json();
const cacheKey = `user:${userId}`;
try {
// Update database
await c.env.DB.prepare(
'UPDATE users SET name = ?, email = ? WHERE id = ?'
)
.bind(userData.name, userData.email, userId)
.run();
// Update cache immediately
await c.env.CACHE.put(cacheKey, JSON.stringify(userData), {
expirationTtl: 3600,
});
return c.json({
success: true,
message: 'User updated and cache refreshed',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Invalidation
// ============================================================================
/**
* Invalidate cache when data changes
*/
app.delete('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const cacheKey = `user:${userId}`;
try {
// Delete from database
await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(userId).run();
// Invalidate cache
await c.env.CACHE.delete(cacheKey);
return c.json({
success: true,
message: 'User deleted and cache invalidated',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Invalidate multiple cache keys
app.post('/api/cache/invalidate', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
try {
// Delete all cache keys in parallel
await Promise.all(keys.map((key) => c.env.CACHE.delete(key)));
return c.json({
success: true,
message: `${keys.length} cache keys invalidated`,
count: keys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Invalidate by prefix (requires list + delete)
app.post('/api/cache/invalidate/prefix', async (c) => {
const { prefix } = await c.req.json<{ prefix: string }>();
try {
let cursor: string | undefined;
let deletedCount = 0;
// List all keys with prefix and delete them
do {
const result = await c.env.CACHE.list({ prefix, cursor });
// Delete batch in parallel
await Promise.all(result.keys.map((key) => c.env.CACHE.delete(key.name)));
deletedCount += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Cache invalidated for prefix "${prefix}"`,
deletedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Stale-While-Revalidate Pattern
// ============================================================================
/**
* Return cached data immediately, refresh in background
*/
async function staleWhileRevalidate<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
ctx: ExecutionContext,
options: {
ttl?: number;
staleThreshold?: number; // Refresh if older than this
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600;
const staleThreshold = options.staleThreshold ?? 300; // 5 minutes
// Get cached value with metadata
const { value, metadata } = await kv.getWithMetadata<
T,
{ timestamp: number }
>(cacheKey, { type: 'json' });
// If cached and not too stale, return immediately
if (value !== null && metadata) {
const age = Date.now() - metadata.timestamp;
// If stale, refresh in background
if (age > staleThreshold * 1000) {
ctx.waitUntil(
(async () => {
try {
const fresh = await fetchFn();
await kv.put(cacheKey, JSON.stringify(fresh), {
expirationTtl: ttl,
metadata: { timestamp: Date.now() },
});
} catch (error) {
console.error('Background refresh failed:', error);
}
})()
);
}
return value;
}
// Cache miss - fetch and store
const data = await fetchFn();
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: ttl,
metadata: { timestamp: Date.now() },
});
return data;
}
// Example usage
app.get('/api/stats', async (c) => {
try {
const stats = await staleWhileRevalidate(
c.env.CACHE,
'global:stats',
async () => {
// Expensive computation
const result = await c.env.DB.prepare(
'SELECT COUNT(*) as total FROM users'
).first();
return result;
},
c.executionCtx,
{
ttl: 3600, // Cache for 1 hour
staleThreshold: 300, // Refresh if older than 5 minutes
}
);
return c.json({
success: true,
stats,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Multi-Layer Cache (KV + Memory)
// ============================================================================
/**
* Two-tier cache: In-memory cache + KV cache
* Useful for frequently accessed data within same Worker instance
*/
const memoryCache = new Map<string, { value: any; expires: number }>();
async function getMultiLayerCache<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
options: {
ttl?: number;
memoryTtl?: number; // In-memory cache duration
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600;
const memoryTtl = (options.memoryTtl ?? 60) * 1000; // Convert to ms
// Check memory cache first (fastest)
const memoryCached = memoryCache.get(cacheKey);
if (memoryCached && memoryCached.expires > Date.now()) {
return memoryCached.value;
}
// Check KV cache (fast, global)
const kvCached = await kv.get<T>(cacheKey, {
type: 'json',
cacheTtl: 300,
});
if (kvCached !== null) {
// Store in memory cache
memoryCache.set(cacheKey, {
value: kvCached,
expires: Date.now() + memoryTtl,
});
return kvCached;
}
// Cache miss - fetch from source
const data = await fetchFn();
// Store in both caches
memoryCache.set(cacheKey, {
value: data,
expires: Date.now() + memoryTtl,
});
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: ttl,
});
return data;
}
// Example usage
app.get('/api/config', async (c) => {
try {
const config = await getMultiLayerCache(
c.env.CACHE,
'app:config',
async () => {
// Fetch from database or API
return {
theme: 'dark',
features: ['feature1', 'feature2'],
version: '1.0.0',
};
},
{
ttl: 3600, // KV cache: 1 hour
memoryTtl: 60, // Memory cache: 1 minute
}
);
return c.json({
success: true,
config,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Warming
// ============================================================================
/**
* Pre-populate cache with frequently accessed data
*/
app.post('/api/cache/warm', async (c) => {
try {
// Example: Warm cache with top 100 users
const topUsers = await c.env.DB.prepare(
'SELECT * FROM users ORDER BY activity DESC LIMIT 100'
).all();
// Store each user in cache
const promises = topUsers.results.map((user: any) =>
c.env.CACHE.put(`user:${user.id}`, JSON.stringify(user), {
expirationTtl: 3600,
})
);
await Promise.all(promises);
return c.json({
success: true,
message: `Warmed cache with ${topUsers.results.length} users`,
count: topUsers.results.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Statistics
// ============================================================================
/**
* Track cache hit/miss rates
*/
let cacheStats = {
hits: 0,
misses: 0,
errors: 0,
};
app.get('/api/cache/stats', (c) => {
const total = cacheStats.hits + cacheStats.misses;
const hitRate = total > 0 ? (cacheStats.hits / total) * 100 : 0;
return c.json({
success: true,
stats: {
hits: cacheStats.hits,
misses: cacheStats.misses,
errors: cacheStats.errors,
total,
hitRate: `${hitRate.toFixed(2)}%`,
},
});
});
// Reset stats
app.post('/api/cache/stats/reset', (c) => {
cacheStats = { hits: 0, misses: 0, errors: 0 };
return c.json({
success: true,
message: 'Cache stats reset',
});
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;