omgkit 2.0.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caching-strategies
|
|
3
|
+
description: Multi-layer caching with Redis, CDN, and browser caching for optimal application performance
|
|
4
|
+
category: backend
|
|
5
|
+
triggers:
|
|
6
|
+
- caching strategies
|
|
7
|
+
- redis cache
|
|
8
|
+
- cdn caching
|
|
9
|
+
- cache invalidation
|
|
10
|
+
- http caching
|
|
11
|
+
- performance caching
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Caching Strategies
|
|
15
|
+
|
|
16
|
+
Implement **multi-layer caching** for optimal performance. This skill covers Redis patterns, CDN configuration, HTTP cache headers, and cache invalidation strategies.
|
|
17
|
+
|
|
18
|
+
## Purpose
|
|
19
|
+
|
|
20
|
+
Dramatically improve application performance through strategic caching:
|
|
21
|
+
|
|
22
|
+
- Reduce database load with application caching
|
|
23
|
+
- Minimize latency with edge caching
|
|
24
|
+
- Optimize bandwidth with browser caching
|
|
25
|
+
- Handle cache invalidation correctly
|
|
26
|
+
- Implement cache-aside and write-through patterns
|
|
27
|
+
- Monitor cache effectiveness
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
### 1. Redis Caching Patterns
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import Redis from 'ioredis';
|
|
35
|
+
|
|
36
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
37
|
+
|
|
38
|
+
// Basic cache operations
|
|
39
|
+
class CacheService {
|
|
40
|
+
private prefix: string;
|
|
41
|
+
private defaultTTL: number;
|
|
42
|
+
|
|
43
|
+
constructor(prefix: string = 'app', defaultTTL: number = 3600) {
|
|
44
|
+
this.prefix = prefix;
|
|
45
|
+
this.defaultTTL = defaultTTL;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private key(key: string): string {
|
|
49
|
+
return `${this.prefix}:${key}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async get<T>(key: string): Promise<T | null> {
|
|
53
|
+
const data = await redis.get(this.key(key));
|
|
54
|
+
return data ? JSON.parse(data) : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
58
|
+
const serialized = JSON.stringify(value);
|
|
59
|
+
await redis.setex(this.key(key), ttl || this.defaultTTL, serialized);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async del(key: string): Promise<void> {
|
|
63
|
+
await redis.del(this.key(key));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async exists(key: string): Promise<boolean> {
|
|
67
|
+
return (await redis.exists(this.key(key))) === 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get or set pattern
|
|
71
|
+
async getOrSet<T>(
|
|
72
|
+
key: string,
|
|
73
|
+
factory: () => Promise<T>,
|
|
74
|
+
ttl?: number
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
const cached = await this.get<T>(key);
|
|
77
|
+
if (cached !== null) return cached;
|
|
78
|
+
|
|
79
|
+
const value = await factory();
|
|
80
|
+
await this.set(key, value, ttl);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Bulk operations
|
|
85
|
+
async mget<T>(keys: string[]): Promise<(T | null)[]> {
|
|
86
|
+
const prefixedKeys = keys.map(k => this.key(k));
|
|
87
|
+
const values = await redis.mget(prefixedKeys);
|
|
88
|
+
return values.map(v => (v ? JSON.parse(v) : null));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async mset<T>(entries: Array<{ key: string; value: T; ttl?: number }>): Promise<void> {
|
|
92
|
+
const pipeline = redis.pipeline();
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
pipeline.setex(
|
|
96
|
+
this.key(entry.key),
|
|
97
|
+
entry.ttl || this.defaultTTL,
|
|
98
|
+
JSON.stringify(entry.value)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await pipeline.exec();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Pattern-based invalidation
|
|
106
|
+
async invalidatePattern(pattern: string): Promise<number> {
|
|
107
|
+
const keys = await redis.keys(this.key(pattern));
|
|
108
|
+
if (keys.length === 0) return 0;
|
|
109
|
+
return redis.del(...keys);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Usage
|
|
114
|
+
const cache = new CacheService('users', 3600);
|
|
115
|
+
|
|
116
|
+
async function getUser(id: string): Promise<User> {
|
|
117
|
+
return cache.getOrSet(`user:${id}`, () => db.user.findUnique({ where: { id } }));
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. Cache-Aside Pattern
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Cache-aside with database fallback
|
|
125
|
+
class UserRepository {
|
|
126
|
+
private cache: CacheService;
|
|
127
|
+
|
|
128
|
+
constructor() {
|
|
129
|
+
this.cache = new CacheService('users', 3600);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async findById(id: string): Promise<User | null> {
|
|
133
|
+
// Try cache first
|
|
134
|
+
const cached = await this.cache.get<User>(`${id}`);
|
|
135
|
+
if (cached) {
|
|
136
|
+
metrics.increment('cache.hit', { type: 'user' });
|
|
137
|
+
return cached;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
metrics.increment('cache.miss', { type: 'user' });
|
|
141
|
+
|
|
142
|
+
// Fetch from database
|
|
143
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
144
|
+
|
|
145
|
+
// Cache the result (even null to prevent cache stampede)
|
|
146
|
+
if (user) {
|
|
147
|
+
await this.cache.set(`${id}`, user);
|
|
148
|
+
} else {
|
|
149
|
+
// Cache null with shorter TTL
|
|
150
|
+
await this.cache.set(`${id}`, null, 60);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return user;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async update(id: string, data: Partial<User>): Promise<User> {
|
|
157
|
+
const user = await db.user.update({ where: { id }, data });
|
|
158
|
+
|
|
159
|
+
// Invalidate cache
|
|
160
|
+
await this.cache.del(`${id}`);
|
|
161
|
+
|
|
162
|
+
return user;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async delete(id: string): Promise<void> {
|
|
166
|
+
await db.user.delete({ where: { id } });
|
|
167
|
+
await this.cache.del(`${id}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Cache stampede prevention with locking
|
|
172
|
+
async function getWithLock<T>(
|
|
173
|
+
key: string,
|
|
174
|
+
factory: () => Promise<T>,
|
|
175
|
+
ttl: number = 3600
|
|
176
|
+
): Promise<T> {
|
|
177
|
+
const cache = new CacheService();
|
|
178
|
+
const lockKey = `lock:${key}`;
|
|
179
|
+
|
|
180
|
+
// Try to get cached value
|
|
181
|
+
const cached = await cache.get<T>(key);
|
|
182
|
+
if (cached !== null) return cached;
|
|
183
|
+
|
|
184
|
+
// Try to acquire lock
|
|
185
|
+
const acquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');
|
|
186
|
+
|
|
187
|
+
if (!acquired) {
|
|
188
|
+
// Another process is fetching, wait and retry
|
|
189
|
+
await new Promise(r => setTimeout(r, 100));
|
|
190
|
+
return getWithLock(key, factory, ttl);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Double-check after acquiring lock
|
|
195
|
+
const cached = await cache.get<T>(key);
|
|
196
|
+
if (cached !== null) return cached;
|
|
197
|
+
|
|
198
|
+
// Fetch and cache
|
|
199
|
+
const value = await factory();
|
|
200
|
+
await cache.set(key, value, ttl);
|
|
201
|
+
return value;
|
|
202
|
+
} finally {
|
|
203
|
+
await redis.del(lockKey);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 3. Write-Through & Write-Behind
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Write-through cache
|
|
212
|
+
class WriteThroughCache<T> {
|
|
213
|
+
constructor(
|
|
214
|
+
private cache: CacheService,
|
|
215
|
+
private repository: Repository<T>
|
|
216
|
+
) {}
|
|
217
|
+
|
|
218
|
+
async create(entity: T): Promise<T> {
|
|
219
|
+
// Write to database first
|
|
220
|
+
const saved = await this.repository.create(entity);
|
|
221
|
+
|
|
222
|
+
// Then update cache
|
|
223
|
+
await this.cache.set(this.getKey(saved), saved);
|
|
224
|
+
|
|
225
|
+
return saved;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async update(id: string, data: Partial<T>): Promise<T> {
|
|
229
|
+
// Write to database
|
|
230
|
+
const updated = await this.repository.update(id, data);
|
|
231
|
+
|
|
232
|
+
// Update cache
|
|
233
|
+
await this.cache.set(this.getKey(updated), updated);
|
|
234
|
+
|
|
235
|
+
return updated;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private getKey(entity: T): string {
|
|
239
|
+
return `${(entity as any).id}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Write-behind (async write) cache
|
|
244
|
+
class WriteBehindCache<T> {
|
|
245
|
+
private writeQueue: Array<{ key: string; data: T }> = [];
|
|
246
|
+
private flushInterval: NodeJS.Timer;
|
|
247
|
+
|
|
248
|
+
constructor(
|
|
249
|
+
private cache: CacheService,
|
|
250
|
+
private repository: Repository<T>,
|
|
251
|
+
flushIntervalMs: number = 5000
|
|
252
|
+
) {
|
|
253
|
+
this.flushInterval = setInterval(() => this.flush(), flushIntervalMs);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async set(key: string, data: T): Promise<void> {
|
|
257
|
+
// Write to cache immediately
|
|
258
|
+
await this.cache.set(key, data);
|
|
259
|
+
|
|
260
|
+
// Queue for async database write
|
|
261
|
+
this.writeQueue.push({ key, data });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async flush(): Promise<void> {
|
|
265
|
+
if (this.writeQueue.length === 0) return;
|
|
266
|
+
|
|
267
|
+
const batch = this.writeQueue.splice(0, 100);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await this.repository.bulkUpsert(batch.map(b => b.data));
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// Re-queue failed items
|
|
273
|
+
this.writeQueue.unshift(...batch);
|
|
274
|
+
console.error('Write-behind flush failed:', error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async stop(): Promise<void> {
|
|
279
|
+
clearInterval(this.flushInterval);
|
|
280
|
+
await this.flush();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 4. HTTP Caching Headers
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Cache control middleware
|
|
289
|
+
interface CacheOptions {
|
|
290
|
+
maxAge?: number;
|
|
291
|
+
sMaxAge?: number;
|
|
292
|
+
staleWhileRevalidate?: number;
|
|
293
|
+
staleIfError?: number;
|
|
294
|
+
private?: boolean;
|
|
295
|
+
noStore?: boolean;
|
|
296
|
+
mustRevalidate?: boolean;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function cacheControl(options: CacheOptions) {
|
|
300
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
301
|
+
const directives: string[] = [];
|
|
302
|
+
|
|
303
|
+
if (options.noStore) {
|
|
304
|
+
directives.push('no-store');
|
|
305
|
+
} else {
|
|
306
|
+
if (options.private) {
|
|
307
|
+
directives.push('private');
|
|
308
|
+
} else {
|
|
309
|
+
directives.push('public');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (options.maxAge !== undefined) {
|
|
313
|
+
directives.push(`max-age=${options.maxAge}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (options.sMaxAge !== undefined) {
|
|
317
|
+
directives.push(`s-maxage=${options.sMaxAge}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (options.staleWhileRevalidate !== undefined) {
|
|
321
|
+
directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (options.staleIfError !== undefined) {
|
|
325
|
+
directives.push(`stale-if-error=${options.staleIfError}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options.mustRevalidate) {
|
|
329
|
+
directives.push('must-revalidate');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
res.set('Cache-Control', directives.join(', '));
|
|
334
|
+
next();
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Route examples
|
|
339
|
+
// Static assets - cache for 1 year
|
|
340
|
+
app.use(
|
|
341
|
+
'/static',
|
|
342
|
+
cacheControl({ maxAge: 31536000 }),
|
|
343
|
+
express.static('public')
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// API responses - no caching for dynamic data
|
|
347
|
+
app.get(
|
|
348
|
+
'/api/user/profile',
|
|
349
|
+
cacheControl({ noStore: true }),
|
|
350
|
+
profileHandler
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Public data - cache with revalidation
|
|
354
|
+
app.get(
|
|
355
|
+
'/api/products',
|
|
356
|
+
cacheControl({
|
|
357
|
+
maxAge: 60,
|
|
358
|
+
sMaxAge: 300,
|
|
359
|
+
staleWhileRevalidate: 86400,
|
|
360
|
+
}),
|
|
361
|
+
productsHandler
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// ETag support
|
|
365
|
+
import etag from 'etag';
|
|
366
|
+
|
|
367
|
+
app.get('/api/products/:id', async (req, res) => {
|
|
368
|
+
const product = await getProduct(req.params.id);
|
|
369
|
+
|
|
370
|
+
if (!product) {
|
|
371
|
+
return res.status(404).json({ error: 'Not found' });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Generate ETag
|
|
375
|
+
const body = JSON.stringify(product);
|
|
376
|
+
const tag = etag(body);
|
|
377
|
+
|
|
378
|
+
res.set('ETag', tag);
|
|
379
|
+
res.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
380
|
+
|
|
381
|
+
// Check If-None-Match
|
|
382
|
+
if (req.headers['if-none-match'] === tag) {
|
|
383
|
+
return res.status(304).end();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
res.json(product);
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 5. CDN Configuration
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Vercel Edge Config
|
|
394
|
+
// vercel.json
|
|
395
|
+
{
|
|
396
|
+
"headers": [
|
|
397
|
+
{
|
|
398
|
+
"source": "/api/public/(.*)",
|
|
399
|
+
"headers": [
|
|
400
|
+
{
|
|
401
|
+
"key": "Cache-Control",
|
|
402
|
+
"value": "public, s-maxage=60, stale-while-revalidate=86400"
|
|
403
|
+
}
|
|
404
|
+
]
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
"source": "/_next/static/(.*)",
|
|
408
|
+
"headers": [
|
|
409
|
+
{
|
|
410
|
+
"key": "Cache-Control",
|
|
411
|
+
"value": "public, max-age=31536000, immutable"
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Cloudflare Cache Rules
|
|
419
|
+
// Using Page Rules or Cache Rules API
|
|
420
|
+
const cacheRules = {
|
|
421
|
+
rules: [
|
|
422
|
+
{
|
|
423
|
+
expression: '(http.request.uri.path matches "^/api/public/")',
|
|
424
|
+
action: 'set_cache_settings',
|
|
425
|
+
action_parameters: {
|
|
426
|
+
edge_ttl: {
|
|
427
|
+
mode: 'override_origin',
|
|
428
|
+
default: 300,
|
|
429
|
+
},
|
|
430
|
+
browser_ttl: {
|
|
431
|
+
mode: 'override_origin',
|
|
432
|
+
default: 60,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
expression: '(http.request.uri.path matches "^/static/")',
|
|
438
|
+
action: 'set_cache_settings',
|
|
439
|
+
action_parameters: {
|
|
440
|
+
cache: true,
|
|
441
|
+
edge_ttl: { mode: 'override_origin', default: 86400 * 30 },
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// Purge cache via API
|
|
448
|
+
async function purgeCloudflareCache(urls: string[]): Promise<void> {
|
|
449
|
+
await fetch(
|
|
450
|
+
`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
|
|
451
|
+
{
|
|
452
|
+
method: 'POST',
|
|
453
|
+
headers: {
|
|
454
|
+
'Authorization': `Bearer ${CF_API_TOKEN}`,
|
|
455
|
+
'Content-Type': 'application/json',
|
|
456
|
+
},
|
|
457
|
+
body: JSON.stringify({ files: urls }),
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Purge by tag
|
|
463
|
+
async function purgeCacheByTag(tags: string[]): Promise<void> {
|
|
464
|
+
await fetch(
|
|
465
|
+
`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
|
|
466
|
+
{
|
|
467
|
+
method: 'POST',
|
|
468
|
+
headers: {
|
|
469
|
+
'Authorization': `Bearer ${CF_API_TOKEN}`,
|
|
470
|
+
'Content-Type': 'application/json',
|
|
471
|
+
},
|
|
472
|
+
body: JSON.stringify({ tags }),
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 6. Cache Invalidation
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// Event-based cache invalidation
|
|
482
|
+
import { EventEmitter } from 'events';
|
|
483
|
+
|
|
484
|
+
class CacheInvalidator extends EventEmitter {
|
|
485
|
+
private cache: CacheService;
|
|
486
|
+
|
|
487
|
+
constructor() {
|
|
488
|
+
super();
|
|
489
|
+
this.cache = new CacheService();
|
|
490
|
+
this.setupListeners();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private setupListeners(): void {
|
|
494
|
+
// User events
|
|
495
|
+
this.on('user:updated', async (userId: string) => {
|
|
496
|
+
await this.cache.del(`user:${userId}`);
|
|
497
|
+
await this.cache.invalidatePattern(`user:${userId}:*`);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
this.on('user:deleted', async (userId: string) => {
|
|
501
|
+
await this.cache.invalidatePattern(`user:${userId}*`);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Product events
|
|
505
|
+
this.on('product:updated', async (productId: string) => {
|
|
506
|
+
await this.cache.del(`product:${productId}`);
|
|
507
|
+
// Also invalidate category cache
|
|
508
|
+
const product = await db.product.findUnique({ where: { id: productId } });
|
|
509
|
+
if (product) {
|
|
510
|
+
await this.cache.del(`category:${product.categoryId}:products`);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Bulk invalidation
|
|
515
|
+
this.on('cache:purge:all', async () => {
|
|
516
|
+
await redis.flushdb();
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const invalidator = new CacheInvalidator();
|
|
522
|
+
|
|
523
|
+
// Use in services
|
|
524
|
+
class ProductService {
|
|
525
|
+
async update(id: string, data: UpdateProductInput): Promise<Product> {
|
|
526
|
+
const product = await db.product.update({ where: { id }, data });
|
|
527
|
+
invalidator.emit('product:updated', id);
|
|
528
|
+
return product;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Time-based invalidation with scheduled jobs
|
|
533
|
+
import { CronJob } from 'cron';
|
|
534
|
+
|
|
535
|
+
// Invalidate daily stats cache at midnight
|
|
536
|
+
new CronJob('0 0 * * *', async () => {
|
|
537
|
+
await cache.invalidatePattern('stats:daily:*');
|
|
538
|
+
}).start();
|
|
539
|
+
|
|
540
|
+
// Refresh popular products cache every hour
|
|
541
|
+
new CronJob('0 * * * *', async () => {
|
|
542
|
+
const products = await getPopularProducts();
|
|
543
|
+
await cache.set('products:popular', products, 3600);
|
|
544
|
+
}).start();
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### 7. Multi-Layer Caching
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
// L1: In-memory (fastest, smallest)
|
|
551
|
+
// L2: Redis (fast, shared)
|
|
552
|
+
// L3: Database (slowest, source of truth)
|
|
553
|
+
|
|
554
|
+
import LRUCache from 'lru-cache';
|
|
555
|
+
|
|
556
|
+
class MultiLayerCache<T> {
|
|
557
|
+
private l1: LRUCache<string, T>;
|
|
558
|
+
private l2: CacheService;
|
|
559
|
+
|
|
560
|
+
constructor(options: {
|
|
561
|
+
l1MaxSize: number;
|
|
562
|
+
l1TTL: number;
|
|
563
|
+
l2Prefix: string;
|
|
564
|
+
l2TTL: number;
|
|
565
|
+
}) {
|
|
566
|
+
this.l1 = new LRUCache({
|
|
567
|
+
max: options.l1MaxSize,
|
|
568
|
+
ttl: options.l1TTL * 1000,
|
|
569
|
+
});
|
|
570
|
+
this.l2 = new CacheService(options.l2Prefix, options.l2TTL);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async get(key: string): Promise<T | null> {
|
|
574
|
+
// Check L1 (in-memory)
|
|
575
|
+
const l1Value = this.l1.get(key);
|
|
576
|
+
if (l1Value !== undefined) {
|
|
577
|
+
metrics.increment('cache.l1.hit');
|
|
578
|
+
return l1Value;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Check L2 (Redis)
|
|
582
|
+
const l2Value = await this.l2.get<T>(key);
|
|
583
|
+
if (l2Value !== null) {
|
|
584
|
+
metrics.increment('cache.l2.hit');
|
|
585
|
+
// Promote to L1
|
|
586
|
+
this.l1.set(key, l2Value);
|
|
587
|
+
return l2Value;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
metrics.increment('cache.miss');
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async set(key: string, value: T, l1TTL?: number, l2TTL?: number): Promise<void> {
|
|
595
|
+
// Set in both layers
|
|
596
|
+
this.l1.set(key, value, { ttl: l1TTL ? l1TTL * 1000 : undefined });
|
|
597
|
+
await this.l2.set(key, value, l2TTL);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async getOrSet(
|
|
601
|
+
key: string,
|
|
602
|
+
factory: () => Promise<T>,
|
|
603
|
+
l1TTL?: number,
|
|
604
|
+
l2TTL?: number
|
|
605
|
+
): Promise<T> {
|
|
606
|
+
const cached = await this.get(key);
|
|
607
|
+
if (cached !== null) return cached;
|
|
608
|
+
|
|
609
|
+
const value = await factory();
|
|
610
|
+
await this.set(key, value, l1TTL, l2TTL);
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async invalidate(key: string): Promise<void> {
|
|
615
|
+
this.l1.delete(key);
|
|
616
|
+
await this.l2.del(key);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Usage
|
|
621
|
+
const userCache = new MultiLayerCache<User>({
|
|
622
|
+
l1MaxSize: 1000,
|
|
623
|
+
l1TTL: 60, // 1 minute in memory
|
|
624
|
+
l2Prefix: 'users',
|
|
625
|
+
l2TTL: 3600, // 1 hour in Redis
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
async function getUser(id: string): Promise<User | null> {
|
|
629
|
+
return userCache.getOrSet(
|
|
630
|
+
`user:${id}`,
|
|
631
|
+
() => db.user.findUnique({ where: { id } })
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Use Cases
|
|
637
|
+
|
|
638
|
+
### 1. API Response Caching
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// Middleware for caching API responses
|
|
642
|
+
function apiCache(options: {
|
|
643
|
+
ttl: number;
|
|
644
|
+
keyGenerator?: (req: Request) => string;
|
|
645
|
+
condition?: (req: Request) => boolean;
|
|
646
|
+
}) {
|
|
647
|
+
const cache = new CacheService('api');
|
|
648
|
+
|
|
649
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
650
|
+
// Skip caching for non-GET or if condition fails
|
|
651
|
+
if (req.method !== 'GET' || (options.condition && !options.condition(req))) {
|
|
652
|
+
return next();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const key = options.keyGenerator?.(req) || req.originalUrl;
|
|
656
|
+
const cached = await cache.get<{ body: any; headers: Record<string, string> }>(key);
|
|
657
|
+
|
|
658
|
+
if (cached) {
|
|
659
|
+
Object.entries(cached.headers).forEach(([k, v]) => res.set(k, v));
|
|
660
|
+
res.set('X-Cache', 'HIT');
|
|
661
|
+
return res.json(cached.body);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Capture response
|
|
665
|
+
const originalJson = res.json.bind(res);
|
|
666
|
+
res.json = (body: any) => {
|
|
667
|
+
cache.set(key, { body, headers: res.getHeaders() as any }, options.ttl);
|
|
668
|
+
res.set('X-Cache', 'MISS');
|
|
669
|
+
return originalJson(body);
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
next();
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Apply to routes
|
|
677
|
+
app.get('/api/products', apiCache({ ttl: 300 }), getProducts);
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### 2. Session Caching
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
// Redis session store
|
|
684
|
+
import session from 'express-session';
|
|
685
|
+
import RedisStore from 'connect-redis';
|
|
686
|
+
|
|
687
|
+
app.use(session({
|
|
688
|
+
store: new RedisStore({ client: redis }),
|
|
689
|
+
secret: process.env.SESSION_SECRET!,
|
|
690
|
+
resave: false,
|
|
691
|
+
saveUninitialized: false,
|
|
692
|
+
cookie: {
|
|
693
|
+
secure: process.env.NODE_ENV === 'production',
|
|
694
|
+
httpOnly: true,
|
|
695
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
696
|
+
},
|
|
697
|
+
}));
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
## Best Practices
|
|
701
|
+
|
|
702
|
+
### Do's
|
|
703
|
+
|
|
704
|
+
- **Cache close to the user** - Browser > CDN > App > Database
|
|
705
|
+
- **Use appropriate TTLs** - Balance freshness vs. performance
|
|
706
|
+
- **Implement cache warming** - Pre-populate for critical paths
|
|
707
|
+
- **Monitor hit rates** - Target > 90% for hot data
|
|
708
|
+
- **Plan invalidation** - Know when and how to invalidate
|
|
709
|
+
- **Use consistent hashing** - For distributed caches
|
|
710
|
+
|
|
711
|
+
### Don'ts
|
|
712
|
+
|
|
713
|
+
- Don't cache sensitive data in shared caches
|
|
714
|
+
- Don't forget cache key namespacing
|
|
715
|
+
- Don't ignore cache stampede scenarios
|
|
716
|
+
- Don't cache errors with long TTLs
|
|
717
|
+
- Don't skip monitoring
|
|
718
|
+
- Don't assume cache is always available
|
|
719
|
+
|
|
720
|
+
### Cache Strategy Checklist
|
|
721
|
+
|
|
722
|
+
```markdown
|
|
723
|
+
## Cache Implementation Checklist
|
|
724
|
+
|
|
725
|
+
### Design
|
|
726
|
+
- [ ] Identified cacheable data
|
|
727
|
+
- [ ] Defined appropriate TTLs
|
|
728
|
+
- [ ] Planned invalidation strategy
|
|
729
|
+
- [ ] Considered cache layers
|
|
730
|
+
|
|
731
|
+
### Implementation
|
|
732
|
+
- [ ] Added cache-aside logic
|
|
733
|
+
- [ ] Implemented stampede prevention
|
|
734
|
+
- [ ] Set up monitoring
|
|
735
|
+
- [ ] Added cache headers
|
|
736
|
+
|
|
737
|
+
### Operations
|
|
738
|
+
- [ ] Monitoring hit/miss ratios
|
|
739
|
+
- [ ] Alerting on cache failures
|
|
740
|
+
- [ ] Regular cache analysis
|
|
741
|
+
- [ ] Invalidation testing
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Related Skills
|
|
745
|
+
|
|
746
|
+
- **redis** - Redis operations
|
|
747
|
+
- **performance-profiling** - Measuring cache impact
|
|
748
|
+
- **api-architecture** - API caching patterns
|
|
749
|
+
|
|
750
|
+
## Reference Resources
|
|
751
|
+
|
|
752
|
+
- [Redis Documentation](https://redis.io/documentation)
|
|
753
|
+
- [HTTP Caching MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching)
|
|
754
|
+
- [Cloudflare Caching](https://developers.cloudflare.com/cache/)
|
|
755
|
+
- [Cache-Control Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
|