navis.js 5.7.0 → 5.8.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/README.md +21 -6
- package/examples/advanced-cache-demo.js +228 -0
- package/examples/advanced-cache-demo.ts +273 -0
- package/package.json +1 -1
- package/src/cache/advanced-cache.js +502 -0
- package/src/index.js +2 -0
- package/src/utils/service-client.js +7 -5
- package/types/index.d.ts +55 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A lightweight, serverless-first, microservice API framework designed for AWS Lambda and Node.js.
|
|
4
4
|
|
|
5
5
|
**Author:** Syed Imran Ali
|
|
6
|
-
**Version:** 5.
|
|
6
|
+
**Version:** 5.8.0
|
|
7
7
|
**License:** MIT
|
|
8
8
|
|
|
9
9
|
## Philosophy
|
|
@@ -197,7 +197,7 @@ navis metrics
|
|
|
197
197
|
- ✅ **Complex queries** - Support for JOINs, nested WHERE conditions, GROUP BY, HAVING, ORDER BY
|
|
198
198
|
- ✅ **Database-agnostic** - Automatic SQL dialect handling (PostgreSQL, MySQL, SQLite, SQL Server)
|
|
199
199
|
|
|
200
|
-
### v5.7
|
|
200
|
+
### v5.7 ✅
|
|
201
201
|
- ✅ **ORM-like features** - Model definitions with relationships, hooks, and validation
|
|
202
202
|
- ✅ **Database migrations** - Migration system with up/down support and tracking
|
|
203
203
|
- ✅ **Model relationships** - hasMany, belongsTo, hasOne relationship definitions
|
|
@@ -205,6 +205,20 @@ navis metrics
|
|
|
205
205
|
- ✅ **Change tracking** - isDirty, getChanged for detecting model modifications
|
|
206
206
|
- ✅ **TypeScript support** - Full type definitions for models and migrations
|
|
207
207
|
|
|
208
|
+
### v5.7.1 ✅
|
|
209
|
+
- 🐛 **Bug fix** - Fixed ServiceClient JSON parsing error handling - now properly rejects on parse failures instead of silently resolving
|
|
210
|
+
- ✅ **Improved error handling** - ServiceClient now provides detailed error information (status code, headers, raw body) when JSON parsing fails
|
|
211
|
+
|
|
212
|
+
### v5.8.0 ✅ (Current)
|
|
213
|
+
- ✅ **Advanced caching strategies** - Multi-level caching (L1 in-memory + L2 Redis)
|
|
214
|
+
- ✅ **Cache warming** - Pre-populate cache with frequently accessed data
|
|
215
|
+
- ✅ **Cache invalidation** - Tag-based and pattern-based invalidation
|
|
216
|
+
- ✅ **Cache statistics** - Comprehensive hit/miss rates, L1/L2 distribution
|
|
217
|
+
- ✅ **Cache compression** - Automatic compression for large values
|
|
218
|
+
- ✅ **Write strategies** - Write-through, write-back, write-around
|
|
219
|
+
- ✅ **Cache stampede prevention** - Prevents concurrent requests for same key
|
|
220
|
+
- ✅ **Cache versioning** - Support for cache schema migrations
|
|
221
|
+
|
|
208
222
|
## API Reference
|
|
209
223
|
|
|
210
224
|
### NavisApp
|
|
@@ -795,6 +809,10 @@ See the `examples/` directory:
|
|
|
795
809
|
- `database-adapters-demo.ts` - Extended database adapters example (v5.5) - TypeScript
|
|
796
810
|
- `query-builder-demo.js` - Advanced query builder example (v5.6) - JavaScript
|
|
797
811
|
- `query-builder-demo.ts` - Advanced query builder example (v5.6) - TypeScript
|
|
812
|
+
- `orm-migrations-demo.js` - ORM-like features and migrations example (v5.7) - JavaScript
|
|
813
|
+
- `orm-migrations-demo.ts` - ORM-like features and migrations example (v5.7) - TypeScript
|
|
814
|
+
- `advanced-cache-demo.js` - Advanced caching strategies example (v5.8) - JavaScript
|
|
815
|
+
- `advanced-cache-demo.ts` - Advanced caching strategies example (v5.8) - TypeScript
|
|
798
816
|
- `service-client-demo.js` - ServiceClient usage example
|
|
799
817
|
|
|
800
818
|
## Roadmap
|
|
@@ -835,17 +853,14 @@ Extended database adapters: SQLite and SQL Server support, enhanced connection p
|
|
|
835
853
|
|
|
836
854
|
Future versions may include:
|
|
837
855
|
- gRPC integration
|
|
838
|
-
- Advanced caching strategies
|
|
839
856
|
- Enhanced monitoring and alerting
|
|
840
|
-
- Database migrations
|
|
841
|
-
- ORM-like features
|
|
842
857
|
|
|
843
858
|
## Documentation
|
|
844
859
|
|
|
845
860
|
- [V2 Features Guide](./docs/V2_FEATURES.md) - Complete v2 features documentation
|
|
846
861
|
- [V5.6 Features Guide](./docs/V5.6_FEATURES.md) - Advanced query builders documentation
|
|
847
862
|
- [V5.7 Features Guide](./docs/V5.7_FEATURES.md) - ORM-like features and migrations documentation
|
|
848
|
-
- [V5.
|
|
863
|
+
- [V5.8 Features Guide](./docs/V5.8_FEATURES.md) - Advanced caching strategies documentation
|
|
849
864
|
- [V3 Features Guide](./docs/V3_FEATURES.md) - Complete v3 features documentation
|
|
850
865
|
- [V4 Features Guide](./docs/V4_FEATURES.md) - Complete v4 features documentation
|
|
851
866
|
- [V5 Features Guide](./docs/V5_FEATURES.md) - Complete v5 features documentation
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Caching Strategies Demo
|
|
3
|
+
* v5.8: Demonstrates multi-level caching, cache warming, invalidation, and statistics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { AdvancedCache, Cache, RedisCache } = require('navis.js');
|
|
7
|
+
|
|
8
|
+
async function demo() {
|
|
9
|
+
console.log('=== Advanced Caching Strategies Demo ===\n');
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// 1. Multi-Level Caching (L1 + L2)
|
|
13
|
+
// ============================================
|
|
14
|
+
console.log('1. Multi-Level Caching (L1: In-Memory, L2: Redis)');
|
|
15
|
+
|
|
16
|
+
// Create L1 cache (in-memory)
|
|
17
|
+
const l1Cache = new Cache({
|
|
18
|
+
maxSize: 100,
|
|
19
|
+
defaultTTL: 300000, // 5 minutes
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Create L2 cache (Redis - optional, can be null)
|
|
23
|
+
// Note: Redis requires redis package: npm install redis
|
|
24
|
+
let l2Cache = null;
|
|
25
|
+
try {
|
|
26
|
+
l2Cache = new RedisCache({
|
|
27
|
+
defaultTTL: 3600, // 1 hour
|
|
28
|
+
prefix: 'navis:',
|
|
29
|
+
});
|
|
30
|
+
await l2Cache.connect();
|
|
31
|
+
console.log('✅ L2 Cache (Redis) connected');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.log('⚠️ L2 Cache (Redis) not available, using L1 only');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create advanced cache with multi-level support
|
|
37
|
+
const advancedCache = new AdvancedCache({
|
|
38
|
+
l1Cache,
|
|
39
|
+
l2Cache,
|
|
40
|
+
l1MaxSize: 100,
|
|
41
|
+
l1TTL: 300000, // 5 minutes
|
|
42
|
+
writeStrategy: 'write-through', // or 'write-back', 'write-around'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Set a value
|
|
46
|
+
await advancedCache.set('user:1', {
|
|
47
|
+
id: 1,
|
|
48
|
+
name: 'John Doe',
|
|
49
|
+
email: 'john@example.com',
|
|
50
|
+
}, {
|
|
51
|
+
ttl: 600000, // 10 minutes
|
|
52
|
+
tags: ['user', 'user:1'],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log('✅ Set value in cache');
|
|
56
|
+
|
|
57
|
+
// Get value (checks L1 first, then L2)
|
|
58
|
+
const user = await advancedCache.get('user:1');
|
|
59
|
+
console.log('✅ Retrieved from cache:', user);
|
|
60
|
+
|
|
61
|
+
// ============================================
|
|
62
|
+
// 2. Cache Warming
|
|
63
|
+
// ============================================
|
|
64
|
+
console.log('\n2. Cache Warming (Pre-populate cache)');
|
|
65
|
+
|
|
66
|
+
const warmData = [
|
|
67
|
+
{ key: 'product:1', value: { id: 1, name: 'Product 1', price: 99.99 }, options: { tags: ['product'] } },
|
|
68
|
+
{ key: 'product:2', value: { id: 2, name: 'Product 2', price: 149.99 }, options: { tags: ['product'] } },
|
|
69
|
+
{ key: 'product:3', value: { id: 3, name: 'Product 3', price: 199.99 }, options: { tags: ['product'] } },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
await advancedCache.warm(warmData);
|
|
73
|
+
console.log('✅ Cache warmed with', warmData.length, 'items');
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// 3. Cache Invalidation by Tags
|
|
77
|
+
// ============================================
|
|
78
|
+
console.log('\n3. Cache Invalidation by Tags');
|
|
79
|
+
|
|
80
|
+
// Set multiple items with tags
|
|
81
|
+
await advancedCache.set('user:2', { id: 2, name: 'Jane Doe' }, { tags: ['user'] });
|
|
82
|
+
await advancedCache.set('user:3', { id: 3, name: 'Bob Smith' }, { tags: ['user'] });
|
|
83
|
+
|
|
84
|
+
console.log('✅ Set 2 users with "user" tag');
|
|
85
|
+
|
|
86
|
+
// Invalidate all items with 'user' tag
|
|
87
|
+
await advancedCache.invalidateByTag('user');
|
|
88
|
+
console.log('✅ Invalidated all items with "user" tag');
|
|
89
|
+
|
|
90
|
+
// Verify they're gone
|
|
91
|
+
const user2 = await advancedCache.get('user:2');
|
|
92
|
+
console.log('User 2 after invalidation:', user2); // Should be null
|
|
93
|
+
|
|
94
|
+
// ============================================
|
|
95
|
+
// 4. Cache Invalidation by Pattern
|
|
96
|
+
// ============================================
|
|
97
|
+
console.log('\n4. Cache Invalidation by Pattern');
|
|
98
|
+
|
|
99
|
+
// Set multiple product items
|
|
100
|
+
await advancedCache.set('product:10', { id: 10, name: 'Product 10' });
|
|
101
|
+
await advancedCache.set('product:11', { id: 11, name: 'Product 11' });
|
|
102
|
+
await advancedCache.set('order:1', { id: 1, total: 100 });
|
|
103
|
+
|
|
104
|
+
console.log('✅ Set items with different prefixes');
|
|
105
|
+
|
|
106
|
+
// Invalidate all product:* keys
|
|
107
|
+
await advancedCache.invalidateByPattern(/^product:/);
|
|
108
|
+
console.log('✅ Invalidated all keys matching pattern "product:*"');
|
|
109
|
+
|
|
110
|
+
// Verify
|
|
111
|
+
const product10 = await advancedCache.get('product:10');
|
|
112
|
+
const order1 = await advancedCache.get('order:1');
|
|
113
|
+
console.log('Product 10 after invalidation:', product10); // Should be null
|
|
114
|
+
console.log('Order 1 (not matching pattern):', order1); // Should still exist
|
|
115
|
+
|
|
116
|
+
// ============================================
|
|
117
|
+
// 5. Cache Statistics
|
|
118
|
+
// ============================================
|
|
119
|
+
console.log('\n5. Cache Statistics');
|
|
120
|
+
|
|
121
|
+
// Perform some operations
|
|
122
|
+
await advancedCache.set('stats:1', { data: 'test1' });
|
|
123
|
+
await advancedCache.set('stats:2', { data: 'test2' });
|
|
124
|
+
await advancedCache.get('stats:1');
|
|
125
|
+
await advancedCache.get('stats:2');
|
|
126
|
+
await advancedCache.get('stats:3'); // Miss
|
|
127
|
+
|
|
128
|
+
const stats = advancedCache.getStats();
|
|
129
|
+
console.log('Cache Statistics:');
|
|
130
|
+
console.log(' Hits:', stats.hits);
|
|
131
|
+
console.log(' Misses:', stats.misses);
|
|
132
|
+
console.log(' Hit Rate:', stats.hitRate);
|
|
133
|
+
console.log(' L1 Hits:', stats.l1Hits);
|
|
134
|
+
console.log(' L2 Hits:', stats.l2Hits);
|
|
135
|
+
console.log(' L1 Size:', stats.l1Size);
|
|
136
|
+
console.log(' Sets:', stats.sets);
|
|
137
|
+
console.log(' Deletes:', stats.deletes);
|
|
138
|
+
|
|
139
|
+
// ============================================
|
|
140
|
+
// 6. Write Strategies
|
|
141
|
+
// ============================================
|
|
142
|
+
console.log('\n6. Write Strategies');
|
|
143
|
+
|
|
144
|
+
// Write-through: Write to both L1 and L2 immediately
|
|
145
|
+
const writeThroughCache = new AdvancedCache({
|
|
146
|
+
l1Cache: new Cache(),
|
|
147
|
+
l2Cache: l2Cache,
|
|
148
|
+
writeStrategy: 'write-through',
|
|
149
|
+
});
|
|
150
|
+
console.log('✅ Write-through: Writes to both L1 and L2 immediately');
|
|
151
|
+
|
|
152
|
+
// Write-back: Write to L1, queue for L2
|
|
153
|
+
const writeBackCache = new AdvancedCache({
|
|
154
|
+
l1Cache: new Cache(),
|
|
155
|
+
l2Cache: l2Cache,
|
|
156
|
+
writeStrategy: 'write-back',
|
|
157
|
+
});
|
|
158
|
+
await writeBackCache.set('writeback:1', { data: 'test' });
|
|
159
|
+
console.log('✅ Write-back: Written to L1, queued for L2');
|
|
160
|
+
await writeBackCache.flush(); // Flush queue
|
|
161
|
+
console.log('✅ Write-back queue flushed');
|
|
162
|
+
|
|
163
|
+
// Write-around: Write to L2 only, L1 populated on read
|
|
164
|
+
const writeAroundCache = new AdvancedCache({
|
|
165
|
+
l1Cache: new Cache(),
|
|
166
|
+
l2Cache: l2Cache,
|
|
167
|
+
writeStrategy: 'write-around',
|
|
168
|
+
});
|
|
169
|
+
await writeAroundCache.set('writearound:1', { data: 'test' });
|
|
170
|
+
console.log('✅ Write-around: Written to L2 only, L1 populated on read');
|
|
171
|
+
|
|
172
|
+
// ============================================
|
|
173
|
+
// 7. Cache Compression
|
|
174
|
+
// ============================================
|
|
175
|
+
console.log('\n7. Cache Compression (for large values)');
|
|
176
|
+
|
|
177
|
+
const largeData = {
|
|
178
|
+
items: Array(1000).fill(0).map((_, i) => ({
|
|
179
|
+
id: i,
|
|
180
|
+
name: `Item ${i}`,
|
|
181
|
+
description: 'This is a long description that repeats many times. '.repeat(10),
|
|
182
|
+
})),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
await advancedCache.set('large:data', largeData, {
|
|
186
|
+
compress: true, // Enable compression
|
|
187
|
+
});
|
|
188
|
+
console.log('✅ Large data cached with compression');
|
|
189
|
+
|
|
190
|
+
const retrieved = await advancedCache.get('large:data');
|
|
191
|
+
console.log('✅ Retrieved and decompressed:', retrieved.items.length, 'items');
|
|
192
|
+
|
|
193
|
+
// ============================================
|
|
194
|
+
// 8. Cache Versioning
|
|
195
|
+
// ============================================
|
|
196
|
+
console.log('\n8. Cache Versioning');
|
|
197
|
+
|
|
198
|
+
const v1Cache = new AdvancedCache({
|
|
199
|
+
l1Cache: new Cache(),
|
|
200
|
+
version: '1.0',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const v2Cache = new AdvancedCache({
|
|
204
|
+
l1Cache: new Cache(),
|
|
205
|
+
version: '2.0',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await v1Cache.set('versioned:key', { version: '1.0' });
|
|
209
|
+
await v2Cache.set('versioned:key', { version: '2.0' });
|
|
210
|
+
|
|
211
|
+
const v1Value = await v1Cache.get('versioned:key');
|
|
212
|
+
const v2Value = await v2Cache.get('versioned:key');
|
|
213
|
+
|
|
214
|
+
console.log('✅ Version 1.0 value:', v1Value);
|
|
215
|
+
console.log('✅ Version 2.0 value:', v2Value);
|
|
216
|
+
console.log('✅ Different versions maintain separate caches');
|
|
217
|
+
|
|
218
|
+
// Cleanup
|
|
219
|
+
if (l2Cache) {
|
|
220
|
+
await l2Cache.disconnect();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log('\n=== Demo Complete ===');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Run demo
|
|
227
|
+
demo().catch(console.error);
|
|
228
|
+
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Caching Strategies Demo (TypeScript)
|
|
3
|
+
* v5.8: Demonstrates multi-level caching, cache warming, invalidation, and statistics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
AdvancedCache,
|
|
8
|
+
Cache,
|
|
9
|
+
RedisCache,
|
|
10
|
+
AdvancedCacheOptions,
|
|
11
|
+
AdvancedCacheSetOptions,
|
|
12
|
+
AdvancedCacheWarmItem,
|
|
13
|
+
AdvancedCacheStats,
|
|
14
|
+
} from 'navis.js';
|
|
15
|
+
|
|
16
|
+
interface User {
|
|
17
|
+
id: number;
|
|
18
|
+
name: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Product {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
price: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function demo(): Promise<void> {
|
|
29
|
+
console.log('=== Advanced Caching Strategies Demo (TypeScript) ===\n');
|
|
30
|
+
|
|
31
|
+
// ============================================
|
|
32
|
+
// 1. Multi-Level Caching (L1 + L2)
|
|
33
|
+
// ============================================
|
|
34
|
+
console.log('1. Multi-Level Caching (L1: In-Memory, L2: Redis)');
|
|
35
|
+
|
|
36
|
+
// Create L1 cache (in-memory)
|
|
37
|
+
const l1Cache = new Cache({
|
|
38
|
+
maxSize: 100,
|
|
39
|
+
defaultTTL: 300000, // 5 minutes
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Create L2 cache (Redis - optional, can be null)
|
|
43
|
+
// Note: Redis requires redis package: npm install redis
|
|
44
|
+
let l2Cache: RedisCache | null = null;
|
|
45
|
+
try {
|
|
46
|
+
l2Cache = new RedisCache({
|
|
47
|
+
defaultTTL: 3600, // 1 hour
|
|
48
|
+
prefix: 'navis:',
|
|
49
|
+
});
|
|
50
|
+
await l2Cache.connect();
|
|
51
|
+
console.log('✅ L2 Cache (Redis) connected');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.log('⚠️ L2 Cache (Redis) not available, using L1 only');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create advanced cache with multi-level support
|
|
57
|
+
const options: AdvancedCacheOptions = {
|
|
58
|
+
l1Cache,
|
|
59
|
+
l2Cache: l2Cache || undefined,
|
|
60
|
+
l1MaxSize: 100,
|
|
61
|
+
l1TTL: 300000, // 5 minutes
|
|
62
|
+
writeStrategy: 'write-through', // or 'write-back', 'write-around'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const advancedCache = new AdvancedCache(options);
|
|
66
|
+
|
|
67
|
+
// Set a value with type safety
|
|
68
|
+
const user: User = {
|
|
69
|
+
id: 1,
|
|
70
|
+
name: 'John Doe',
|
|
71
|
+
email: 'john@example.com',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const setOptions: AdvancedCacheSetOptions = {
|
|
75
|
+
ttl: 600000, // 10 minutes
|
|
76
|
+
tags: ['user', 'user:1'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
await advancedCache.set('user:1', user, setOptions);
|
|
80
|
+
console.log('✅ Set value in cache');
|
|
81
|
+
|
|
82
|
+
// Get value (checks L1 first, then L2)
|
|
83
|
+
const retrievedUser = await advancedCache.get('user:1') as User | null;
|
|
84
|
+
console.log('✅ Retrieved from cache:', retrievedUser);
|
|
85
|
+
|
|
86
|
+
// ============================================
|
|
87
|
+
// 2. Cache Warming
|
|
88
|
+
// ============================================
|
|
89
|
+
console.log('\n2. Cache Warming (Pre-populate cache)');
|
|
90
|
+
|
|
91
|
+
const warmData: AdvancedCacheWarmItem[] = [
|
|
92
|
+
{
|
|
93
|
+
key: 'product:1',
|
|
94
|
+
value: { id: 1, name: 'Product 1', price: 99.99 } as Product,
|
|
95
|
+
options: { tags: ['product'] },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: 'product:2',
|
|
99
|
+
value: { id: 2, name: 'Product 2', price: 149.99 } as Product,
|
|
100
|
+
options: { tags: ['product'] },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: 'product:3',
|
|
104
|
+
value: { id: 3, name: 'Product 3', price: 199.99 } as Product,
|
|
105
|
+
options: { tags: ['product'] },
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
await advancedCache.warm(warmData);
|
|
110
|
+
console.log('✅ Cache warmed with', warmData.length, 'items');
|
|
111
|
+
|
|
112
|
+
// ============================================
|
|
113
|
+
// 3. Cache Invalidation by Tags
|
|
114
|
+
// ============================================
|
|
115
|
+
console.log('\n3. Cache Invalidation by Tags');
|
|
116
|
+
|
|
117
|
+
// Set multiple items with tags
|
|
118
|
+
await advancedCache.set('user:2', { id: 2, name: 'Jane Doe' } as User, { tags: ['user'] });
|
|
119
|
+
await advancedCache.set('user:3', { id: 3, name: 'Bob Smith' } as User, { tags: ['user'] });
|
|
120
|
+
|
|
121
|
+
console.log('✅ Set 2 users with "user" tag');
|
|
122
|
+
|
|
123
|
+
// Invalidate all items with 'user' tag
|
|
124
|
+
await advancedCache.invalidateByTag('user');
|
|
125
|
+
console.log('✅ Invalidated all items with "user" tag');
|
|
126
|
+
|
|
127
|
+
// Verify they're gone
|
|
128
|
+
const user2 = await advancedCache.get('user:2') as User | null;
|
|
129
|
+
console.log('User 2 after invalidation:', user2); // Should be null
|
|
130
|
+
|
|
131
|
+
// ============================================
|
|
132
|
+
// 4. Cache Invalidation by Pattern
|
|
133
|
+
// ============================================
|
|
134
|
+
console.log('\n4. Cache Invalidation by Pattern');
|
|
135
|
+
|
|
136
|
+
// Set multiple product items
|
|
137
|
+
await advancedCache.set('product:10', { id: 10, name: 'Product 10' } as Product);
|
|
138
|
+
await advancedCache.set('product:11', { id: 11, name: 'Product 11' } as Product);
|
|
139
|
+
await advancedCache.set('order:1', { id: 1, total: 100 });
|
|
140
|
+
|
|
141
|
+
console.log('✅ Set items with different prefixes');
|
|
142
|
+
|
|
143
|
+
// Invalidate all product:* keys
|
|
144
|
+
await advancedCache.invalidateByPattern(/^product:/);
|
|
145
|
+
console.log('✅ Invalidated all keys matching pattern "product:*"');
|
|
146
|
+
|
|
147
|
+
// Verify
|
|
148
|
+
const product10 = await advancedCache.get('product:10') as Product | null;
|
|
149
|
+
const order1 = await advancedCache.get('order:1');
|
|
150
|
+
console.log('Product 10 after invalidation:', product10); // Should be null
|
|
151
|
+
console.log('Order 1 (not matching pattern):', order1); // Should still exist
|
|
152
|
+
|
|
153
|
+
// ============================================
|
|
154
|
+
// 5. Cache Statistics
|
|
155
|
+
// ============================================
|
|
156
|
+
console.log('\n5. Cache Statistics');
|
|
157
|
+
|
|
158
|
+
// Perform some operations
|
|
159
|
+
await advancedCache.set('stats:1', { data: 'test1' });
|
|
160
|
+
await advancedCache.set('stats:2', { data: 'test2' });
|
|
161
|
+
await advancedCache.get('stats:1');
|
|
162
|
+
await advancedCache.get('stats:2');
|
|
163
|
+
await advancedCache.get('stats:3'); // Miss
|
|
164
|
+
|
|
165
|
+
const stats: AdvancedCacheStats = advancedCache.getStats();
|
|
166
|
+
console.log('Cache Statistics:');
|
|
167
|
+
console.log(' Hits:', stats.hits);
|
|
168
|
+
console.log(' Misses:', stats.misses);
|
|
169
|
+
console.log(' Hit Rate:', stats.hitRate);
|
|
170
|
+
console.log(' L1 Hits:', stats.l1Hits);
|
|
171
|
+
console.log(' L2 Hits:', stats.l2Hits);
|
|
172
|
+
console.log(' L1 Size:', stats.l1Size);
|
|
173
|
+
console.log(' Sets:', stats.sets);
|
|
174
|
+
console.log(' Deletes:', stats.deletes);
|
|
175
|
+
|
|
176
|
+
// ============================================
|
|
177
|
+
// 6. Write Strategies
|
|
178
|
+
// ============================================
|
|
179
|
+
console.log('\n6. Write Strategies');
|
|
180
|
+
|
|
181
|
+
// Write-through: Write to both L1 and L2 immediately
|
|
182
|
+
const writeThroughCache = new AdvancedCache({
|
|
183
|
+
l1Cache: new Cache(),
|
|
184
|
+
l2Cache: l2Cache || undefined,
|
|
185
|
+
writeStrategy: 'write-through',
|
|
186
|
+
});
|
|
187
|
+
console.log('✅ Write-through: Writes to both L1 and L2 immediately');
|
|
188
|
+
|
|
189
|
+
// Write-back: Write to L1, queue for L2
|
|
190
|
+
const writeBackCache = new AdvancedCache({
|
|
191
|
+
l1Cache: new Cache(),
|
|
192
|
+
l2Cache: l2Cache || undefined,
|
|
193
|
+
writeStrategy: 'write-back',
|
|
194
|
+
});
|
|
195
|
+
await writeBackCache.set('writeback:1', { data: 'test' });
|
|
196
|
+
console.log('✅ Write-back: Written to L1, queued for L2');
|
|
197
|
+
await writeBackCache.flush(); // Flush queue
|
|
198
|
+
console.log('✅ Write-back queue flushed');
|
|
199
|
+
|
|
200
|
+
// Write-around: Write to L2 only, L1 populated on read
|
|
201
|
+
const writeAroundCache = new AdvancedCache({
|
|
202
|
+
l1Cache: new Cache(),
|
|
203
|
+
l2Cache: l2Cache || undefined,
|
|
204
|
+
writeStrategy: 'write-around',
|
|
205
|
+
});
|
|
206
|
+
await writeAroundCache.set('writearound:1', { data: 'test' });
|
|
207
|
+
console.log('✅ Write-around: Written to L2 only, L1 populated on read');
|
|
208
|
+
|
|
209
|
+
// ============================================
|
|
210
|
+
// 7. Cache Compression
|
|
211
|
+
// ============================================
|
|
212
|
+
console.log('\n7. Cache Compression (for large values)');
|
|
213
|
+
|
|
214
|
+
interface LargeItem {
|
|
215
|
+
id: number;
|
|
216
|
+
name: string;
|
|
217
|
+
description: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const largeData: { items: LargeItem[] } = {
|
|
221
|
+
items: Array(1000).fill(0).map((_, i) => ({
|
|
222
|
+
id: i,
|
|
223
|
+
name: `Item ${i}`,
|
|
224
|
+
description: 'This is a long description that repeats many times. '.repeat(10),
|
|
225
|
+
})),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await advancedCache.set('large:data', largeData, {
|
|
229
|
+
compress: true, // Enable compression
|
|
230
|
+
});
|
|
231
|
+
console.log('✅ Large data cached with compression');
|
|
232
|
+
|
|
233
|
+
const retrieved = await advancedCache.get('large:data') as { items: LargeItem[] } | null;
|
|
234
|
+
if (retrieved) {
|
|
235
|
+
console.log('✅ Retrieved and decompressed:', retrieved.items.length, 'items');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================
|
|
239
|
+
// 8. Cache Versioning
|
|
240
|
+
// ============================================
|
|
241
|
+
console.log('\n8. Cache Versioning');
|
|
242
|
+
|
|
243
|
+
const v1Cache = new AdvancedCache({
|
|
244
|
+
l1Cache: new Cache(),
|
|
245
|
+
version: '1.0',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const v2Cache = new AdvancedCache({
|
|
249
|
+
l1Cache: new Cache(),
|
|
250
|
+
version: '2.0',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await v1Cache.set('versioned:key', { version: '1.0' });
|
|
254
|
+
await v2Cache.set('versioned:key', { version: '2.0' });
|
|
255
|
+
|
|
256
|
+
const v1Value = await v1Cache.get('versioned:key');
|
|
257
|
+
const v2Value = await v2Cache.get('versioned:key');
|
|
258
|
+
|
|
259
|
+
console.log('✅ Version 1.0 value:', v1Value);
|
|
260
|
+
console.log('✅ Version 2.0 value:', v2Value);
|
|
261
|
+
console.log('✅ Different versions maintain separate caches');
|
|
262
|
+
|
|
263
|
+
// Cleanup
|
|
264
|
+
if (l2Cache) {
|
|
265
|
+
await l2Cache.disconnect();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log('\n=== Demo Complete ===');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Run demo
|
|
272
|
+
demo().catch(console.error);
|
|
273
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Caching Strategies
|
|
3
|
+
* v5.8: Multi-level caching, cache warming, invalidation, statistics, and more
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Cache = require('./cache');
|
|
7
|
+
const zlib = require('zlib');
|
|
8
|
+
const { promisify } = require('util');
|
|
9
|
+
|
|
10
|
+
const gzip = promisify(zlib.gzip);
|
|
11
|
+
const gunzip = promisify(zlib.gunzip);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Advanced Cache with multiple strategies
|
|
15
|
+
*/
|
|
16
|
+
class AdvancedCache {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
// Multi-level cache: L1 (in-memory), L2 (Redis/remote)
|
|
19
|
+
this.l1Cache = options.l1Cache || new Cache({
|
|
20
|
+
maxSize: options.l1MaxSize || 1000,
|
|
21
|
+
defaultTTL: options.l1TTL || 300000, // 5 minutes
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.l2Cache = options.l2Cache || null; // Optional Redis or other remote cache
|
|
25
|
+
|
|
26
|
+
// Cache statistics
|
|
27
|
+
this.stats = {
|
|
28
|
+
hits: 0,
|
|
29
|
+
misses: 0,
|
|
30
|
+
sets: 0,
|
|
31
|
+
deletes: 0,
|
|
32
|
+
errors: 0,
|
|
33
|
+
l1Hits: 0,
|
|
34
|
+
l2Hits: 0,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Cache invalidation tags
|
|
38
|
+
this.tagStore = new Map(); // tag -> Set of keys
|
|
39
|
+
this.keyTags = new Map(); // key -> Set of tags
|
|
40
|
+
|
|
41
|
+
// Cache warming
|
|
42
|
+
this.warmingQueue = [];
|
|
43
|
+
this.isWarming = false;
|
|
44
|
+
|
|
45
|
+
// Cache compression
|
|
46
|
+
this.compressThreshold = options.compressThreshold || 1024; // Compress values > 1KB
|
|
47
|
+
|
|
48
|
+
// Cache stampede prevention
|
|
49
|
+
this.pendingGets = new Map(); // key -> Promise
|
|
50
|
+
|
|
51
|
+
// Write strategy: 'write-through', 'write-back', 'write-around'
|
|
52
|
+
this.writeStrategy = options.writeStrategy || 'write-through';
|
|
53
|
+
this.writeBackQueue = [];
|
|
54
|
+
|
|
55
|
+
// Cache versioning
|
|
56
|
+
this.version = options.version || '1.0';
|
|
57
|
+
this.versionPrefix = `v${this.version}:`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get value from cache (multi-level)
|
|
62
|
+
* @param {string} key - Cache key
|
|
63
|
+
* @returns {Promise<*>} - Cached value or null
|
|
64
|
+
*/
|
|
65
|
+
async get(key) {
|
|
66
|
+
const fullKey = this.versionPrefix + key;
|
|
67
|
+
|
|
68
|
+
// Prevent cache stampede
|
|
69
|
+
if (this.pendingGets.has(fullKey)) {
|
|
70
|
+
return this.pendingGets.get(fullKey);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const getPromise = this._getInternal(fullKey);
|
|
74
|
+
this.pendingGets.set(fullKey, getPromise);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await getPromise;
|
|
78
|
+
return result;
|
|
79
|
+
} finally {
|
|
80
|
+
this.pendingGets.delete(fullKey);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Internal get with multi-level lookup
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
async _getInternal(fullKey) {
|
|
89
|
+
// Try L1 cache first
|
|
90
|
+
const l1Value = this.l1Cache.get(fullKey);
|
|
91
|
+
if (l1Value !== null) {
|
|
92
|
+
this.stats.hits++;
|
|
93
|
+
this.stats.l1Hits++;
|
|
94
|
+
return await this._decompress(l1Value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try L2 cache if available
|
|
98
|
+
if (this.l2Cache) {
|
|
99
|
+
try {
|
|
100
|
+
const l2Value = await this.l2Cache.get(fullKey);
|
|
101
|
+
if (l2Value !== null) {
|
|
102
|
+
this.stats.hits++;
|
|
103
|
+
this.stats.l2Hits++;
|
|
104
|
+
|
|
105
|
+
// Promote to L1
|
|
106
|
+
const ttl = this._getRemainingTTL(fullKey);
|
|
107
|
+
if (ttl > 0) {
|
|
108
|
+
this.l1Cache.set(fullKey, l2Value, ttl);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return await this._decompress(l2Value);
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.stats.errors++;
|
|
115
|
+
// Continue to miss
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.stats.misses++;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set value in cache
|
|
125
|
+
* @param {string} key - Cache key
|
|
126
|
+
* @param {*} value - Value to cache
|
|
127
|
+
* @param {Object} options - Options (ttl, tags, compress)
|
|
128
|
+
*/
|
|
129
|
+
async set(key, value, options = {}) {
|
|
130
|
+
const fullKey = this.versionPrefix + key;
|
|
131
|
+
const ttl = options.ttl || null;
|
|
132
|
+
const tags = options.tags || [];
|
|
133
|
+
const shouldCompress = options.compress !== false;
|
|
134
|
+
|
|
135
|
+
// Compress if needed (always wrap in object for consistency)
|
|
136
|
+
const processedValue = shouldCompress ? await this._compress(value) : { compressed: false, data: value };
|
|
137
|
+
|
|
138
|
+
// Apply write strategy
|
|
139
|
+
switch (this.writeStrategy) {
|
|
140
|
+
case 'write-through':
|
|
141
|
+
await this._writeThrough(fullKey, processedValue, ttl);
|
|
142
|
+
break;
|
|
143
|
+
case 'write-back':
|
|
144
|
+
await this._writeBack(fullKey, processedValue, ttl);
|
|
145
|
+
break;
|
|
146
|
+
case 'write-around':
|
|
147
|
+
await this._writeAround(fullKey, processedValue, ttl);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle tags
|
|
152
|
+
if (tags.length > 0) {
|
|
153
|
+
this._addTags(fullKey, tags);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.stats.sets++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Write-through: Write to both L1 and L2 immediately
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
async _writeThrough(key, value, ttl) {
|
|
164
|
+
// Write to L1
|
|
165
|
+
this.l1Cache.set(key, value, ttl);
|
|
166
|
+
|
|
167
|
+
// Write to L2 if available
|
|
168
|
+
if (this.l2Cache) {
|
|
169
|
+
try {
|
|
170
|
+
if (this.l2Cache.set) {
|
|
171
|
+
await this.l2Cache.set(key, value, ttl ? Math.floor(ttl / 1000) : null);
|
|
172
|
+
} else {
|
|
173
|
+
this.l2Cache.set(key, value, ttl);
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.stats.errors++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Write-back: Write to L1, queue for L2
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
async _writeBack(key, value, ttl) {
|
|
186
|
+
// Write to L1 immediately
|
|
187
|
+
this.l1Cache.set(key, value, ttl);
|
|
188
|
+
|
|
189
|
+
// Queue for L2 write
|
|
190
|
+
this.writeBackQueue.push({ key, value, ttl, timestamp: Date.now() });
|
|
191
|
+
|
|
192
|
+
// Flush queue if it gets too large
|
|
193
|
+
if (this.writeBackQueue.length > 100) {
|
|
194
|
+
await this._flushWriteBackQueue();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Write-around: Write to L2 only, L1 populated on read
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
async _writeAround(key, value, ttl) {
|
|
203
|
+
// Write to L2 only
|
|
204
|
+
if (this.l2Cache) {
|
|
205
|
+
try {
|
|
206
|
+
if (this.l2Cache.set) {
|
|
207
|
+
await this.l2Cache.set(key, value, ttl ? Math.floor(ttl / 1000) : null);
|
|
208
|
+
} else {
|
|
209
|
+
this.l2Cache.set(key, value, ttl);
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
this.stats.errors++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Flush write-back queue
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
async _flushWriteBackQueue() {
|
|
222
|
+
if (!this.l2Cache || this.writeBackQueue.length === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const items = this.writeBackQueue.splice(0);
|
|
227
|
+
for (const item of items) {
|
|
228
|
+
try {
|
|
229
|
+
if (this.l2Cache.set) {
|
|
230
|
+
await this.l2Cache.set(item.key, item.value, item.ttl ? Math.floor(item.ttl / 1000) : null);
|
|
231
|
+
} else {
|
|
232
|
+
this.l2Cache.set(item.key, item.value, item.ttl);
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
this.stats.errors++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Delete from cache
|
|
242
|
+
* @param {string} key - Cache key
|
|
243
|
+
*/
|
|
244
|
+
async delete(key) {
|
|
245
|
+
const fullKey = this.versionPrefix + key;
|
|
246
|
+
|
|
247
|
+
// Delete from L1
|
|
248
|
+
this.l1Cache.delete(fullKey);
|
|
249
|
+
|
|
250
|
+
// Delete from L2
|
|
251
|
+
if (this.l2Cache) {
|
|
252
|
+
try {
|
|
253
|
+
if (this.l2Cache.delete) {
|
|
254
|
+
await this.l2Cache.delete(fullKey);
|
|
255
|
+
} else {
|
|
256
|
+
this.l2Cache.delete(fullKey);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this.stats.errors++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Remove tags
|
|
264
|
+
this._removeTags(fullKey);
|
|
265
|
+
|
|
266
|
+
this.stats.deletes++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Invalidate by tag
|
|
271
|
+
* @param {string|Array<string>} tags - Tag(s) to invalidate
|
|
272
|
+
*/
|
|
273
|
+
async invalidateByTag(tags) {
|
|
274
|
+
const tagArray = Array.isArray(tags) ? tags : [tags];
|
|
275
|
+
const keysToDelete = new Set();
|
|
276
|
+
|
|
277
|
+
for (const tag of tagArray) {
|
|
278
|
+
const keys = this.tagStore.get(tag);
|
|
279
|
+
if (keys) {
|
|
280
|
+
for (const key of keys) {
|
|
281
|
+
keysToDelete.add(key);
|
|
282
|
+
}
|
|
283
|
+
this.tagStore.delete(tag);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Delete all keys
|
|
288
|
+
for (const key of keysToDelete) {
|
|
289
|
+
await this.delete(key.replace(this.versionPrefix, ''));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Invalidate by pattern
|
|
295
|
+
* @param {string|RegExp} pattern - Pattern to match keys (without version prefix)
|
|
296
|
+
*/
|
|
297
|
+
async invalidateByPattern(pattern) {
|
|
298
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
299
|
+
const keysToDelete = [];
|
|
300
|
+
|
|
301
|
+
// Check L1 cache (keys include version prefix)
|
|
302
|
+
for (const fullKey of this.l1Cache.keys()) {
|
|
303
|
+
// Remove version prefix for pattern matching
|
|
304
|
+
const keyWithoutPrefix = fullKey.replace(this.versionPrefix, '');
|
|
305
|
+
if (regex.test(keyWithoutPrefix)) {
|
|
306
|
+
keysToDelete.push(keyWithoutPrefix);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Delete matched keys
|
|
311
|
+
for (const key of keysToDelete) {
|
|
312
|
+
await this.delete(key);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Add tags to a key
|
|
318
|
+
* @private
|
|
319
|
+
*/
|
|
320
|
+
_addTags(key, tags) {
|
|
321
|
+
if (!this.keyTags.has(key)) {
|
|
322
|
+
this.keyTags.set(key, new Set());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const tag of tags) {
|
|
326
|
+
this.keyTags.get(key).add(tag);
|
|
327
|
+
|
|
328
|
+
if (!this.tagStore.has(tag)) {
|
|
329
|
+
this.tagStore.set(tag, new Set());
|
|
330
|
+
}
|
|
331
|
+
this.tagStore.get(tag).add(key);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Remove tags from a key
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
_removeTags(key) {
|
|
340
|
+
const tags = this.keyTags.get(key);
|
|
341
|
+
if (tags) {
|
|
342
|
+
for (const tag of tags) {
|
|
343
|
+
const keys = this.tagStore.get(tag);
|
|
344
|
+
if (keys) {
|
|
345
|
+
keys.delete(key);
|
|
346
|
+
if (keys.size === 0) {
|
|
347
|
+
this.tagStore.delete(tag);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
this.keyTags.delete(key);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Warm cache (pre-populate)
|
|
357
|
+
* @param {Array<Object>} items - Array of {key, value, options}
|
|
358
|
+
*/
|
|
359
|
+
async warm(items) {
|
|
360
|
+
if (this.isWarming) {
|
|
361
|
+
this.warmingQueue.push(...items);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this.isWarming = true;
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
for (const item of items) {
|
|
369
|
+
await this.set(item.key, item.value, item.options || {});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Process queued items
|
|
373
|
+
while (this.warmingQueue.length > 0) {
|
|
374
|
+
const item = this.warmingQueue.shift();
|
|
375
|
+
await this.set(item.key, item.value, item.options || {});
|
|
376
|
+
}
|
|
377
|
+
} finally {
|
|
378
|
+
this.isWarming = false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get cache statistics
|
|
384
|
+
* @returns {Object} - Cache statistics
|
|
385
|
+
*/
|
|
386
|
+
getStats() {
|
|
387
|
+
const total = this.stats.hits + this.stats.misses;
|
|
388
|
+
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
...this.stats,
|
|
392
|
+
total,
|
|
393
|
+
hitRate: hitRate.toFixed(2) + '%',
|
|
394
|
+
l1Size: this.l1Cache.size(),
|
|
395
|
+
l2Size: this.l2Cache ? 'N/A' : 0, // Would need async call for Redis
|
|
396
|
+
writeBackQueueSize: this.writeBackQueue.length,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Reset statistics
|
|
402
|
+
*/
|
|
403
|
+
resetStats() {
|
|
404
|
+
this.stats = {
|
|
405
|
+
hits: 0,
|
|
406
|
+
misses: 0,
|
|
407
|
+
sets: 0,
|
|
408
|
+
deletes: 0,
|
|
409
|
+
errors: 0,
|
|
410
|
+
l1Hits: 0,
|
|
411
|
+
l2Hits: 0,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Clear all cache
|
|
417
|
+
*/
|
|
418
|
+
async clear() {
|
|
419
|
+
this.l1Cache.clear();
|
|
420
|
+
|
|
421
|
+
if (this.l2Cache) {
|
|
422
|
+
try {
|
|
423
|
+
if (this.l2Cache.clear) {
|
|
424
|
+
await this.l2Cache.clear();
|
|
425
|
+
} else {
|
|
426
|
+
this.l2Cache.clear();
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
this.stats.errors++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.tagStore.clear();
|
|
434
|
+
this.keyTags.clear();
|
|
435
|
+
this.writeBackQueue = [];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Compress value if needed
|
|
440
|
+
* @private
|
|
441
|
+
*/
|
|
442
|
+
async _compress(value) {
|
|
443
|
+
const serialized = JSON.stringify(value);
|
|
444
|
+
|
|
445
|
+
if (serialized.length < this.compressThreshold) {
|
|
446
|
+
return { compressed: false, data: value };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const compressed = await gzip(serialized);
|
|
451
|
+
return { compressed: true, data: compressed.toString('base64') };
|
|
452
|
+
} catch (error) {
|
|
453
|
+
// If compression fails, return uncompressed
|
|
454
|
+
return { compressed: false, data: value };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Decompress value if needed
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
async _decompress(value) {
|
|
463
|
+
// If not an object or doesn't have compressed property, return as-is
|
|
464
|
+
if (!value || typeof value !== 'object' || value.compressed === undefined) {
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// If compressed is false, return the data directly
|
|
469
|
+
if (!value.compressed) {
|
|
470
|
+
return value.data;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Decompress if compressed
|
|
474
|
+
try {
|
|
475
|
+
const buffer = Buffer.from(value.data, 'base64');
|
|
476
|
+
const decompressed = await gunzip(buffer);
|
|
477
|
+
return JSON.parse(decompressed.toString());
|
|
478
|
+
} catch (error) {
|
|
479
|
+
// If decompression fails, return as-is
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get remaining TTL for a key (approximate)
|
|
486
|
+
* @private
|
|
487
|
+
*/
|
|
488
|
+
_getRemainingTTL(key) {
|
|
489
|
+
// This is a simplified version - would need to track TTL in real implementation
|
|
490
|
+
return this.l1Cache.defaultTTL;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Flush write-back queue (public method)
|
|
495
|
+
*/
|
|
496
|
+
async flush() {
|
|
497
|
+
await this._flushWriteBackQueue();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
module.exports = AdvancedCache;
|
|
502
|
+
|
package/src/index.js
CHANGED
|
@@ -50,6 +50,7 @@ const {
|
|
|
50
50
|
// v5: Enterprise Features
|
|
51
51
|
const Cache = require('./cache/cache');
|
|
52
52
|
const RedisCache = require('./cache/redis-cache');
|
|
53
|
+
const AdvancedCache = require('./cache/advanced-cache');
|
|
53
54
|
const cache = require('./middleware/cache-middleware');
|
|
54
55
|
const cors = require('./middleware/cors');
|
|
55
56
|
const security = require('./middleware/security');
|
|
@@ -137,6 +138,7 @@ module.exports = {
|
|
|
137
138
|
// v5: Enterprise Features
|
|
138
139
|
Cache,
|
|
139
140
|
RedisCache,
|
|
141
|
+
AdvancedCache,
|
|
140
142
|
cache,
|
|
141
143
|
cors,
|
|
142
144
|
security,
|
|
@@ -79,11 +79,13 @@ class ServiceClient {
|
|
|
79
79
|
resolve(response);
|
|
80
80
|
}
|
|
81
81
|
} catch (err) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
// JSON parsing failed - reject with error instead of silently resolving
|
|
83
|
+
const parseError = new Error(`Failed to parse JSON response: ${err.message}`);
|
|
84
|
+
parseError.statusCode = res.statusCode;
|
|
85
|
+
parseError.headers = res.headers;
|
|
86
|
+
parseError.rawBody = body;
|
|
87
|
+
parseError.parseError = err;
|
|
88
|
+
reject(parseError);
|
|
87
89
|
}
|
|
88
90
|
});
|
|
89
91
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -358,8 +358,58 @@ export interface RedisCache {
|
|
|
358
358
|
size(): Promise<number>;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
export interface AdvancedCacheOptions {
|
|
362
|
+
l1Cache?: Cache;
|
|
363
|
+
l2Cache?: RedisCache | any;
|
|
364
|
+
l1MaxSize?: number;
|
|
365
|
+
l1TTL?: number;
|
|
366
|
+
compressThreshold?: number;
|
|
367
|
+
writeStrategy?: 'write-through' | 'write-back' | 'write-around';
|
|
368
|
+
version?: string;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export interface AdvancedCacheSetOptions {
|
|
372
|
+
ttl?: number;
|
|
373
|
+
tags?: string[];
|
|
374
|
+
compress?: boolean;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export interface AdvancedCacheStats {
|
|
378
|
+
hits: number;
|
|
379
|
+
misses: number;
|
|
380
|
+
sets: number;
|
|
381
|
+
deletes: number;
|
|
382
|
+
errors: number;
|
|
383
|
+
l1Hits: number;
|
|
384
|
+
l2Hits: number;
|
|
385
|
+
total: number;
|
|
386
|
+
hitRate: string;
|
|
387
|
+
l1Size: number;
|
|
388
|
+
l2Size: number | string;
|
|
389
|
+
writeBackQueueSize: number;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface AdvancedCacheWarmItem {
|
|
393
|
+
key: string;
|
|
394
|
+
value: any;
|
|
395
|
+
options?: AdvancedCacheSetOptions;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface AdvancedCache {
|
|
399
|
+
get(key: string): Promise<any>;
|
|
400
|
+
set(key: string, value: any, options?: AdvancedCacheSetOptions): Promise<void>;
|
|
401
|
+
delete(key: string): Promise<void>;
|
|
402
|
+
invalidateByTag(tags: string | string[]): Promise<void>;
|
|
403
|
+
invalidateByPattern(pattern: string | RegExp): Promise<void>;
|
|
404
|
+
warm(items: AdvancedCacheWarmItem[]): Promise<void>;
|
|
405
|
+
getStats(): AdvancedCacheStats;
|
|
406
|
+
resetStats(): void;
|
|
407
|
+
clear(): Promise<void>;
|
|
408
|
+
flush(): Promise<void>;
|
|
409
|
+
}
|
|
410
|
+
|
|
361
411
|
export interface CacheMiddlewareOptions {
|
|
362
|
-
cacheStore: Cache | RedisCache;
|
|
412
|
+
cacheStore: Cache | RedisCache | AdvancedCache;
|
|
363
413
|
ttl?: number;
|
|
364
414
|
keyGenerator?: (req: NavisRequest) => string;
|
|
365
415
|
skipCache?: (req: NavisRequest, res: NavisResponse) => boolean;
|
|
@@ -698,6 +748,10 @@ export const RedisCache: {
|
|
|
698
748
|
new (options?: RedisCacheOptions): RedisCache;
|
|
699
749
|
};
|
|
700
750
|
|
|
751
|
+
export const AdvancedCache: {
|
|
752
|
+
new (options?: AdvancedCacheOptions): AdvancedCache;
|
|
753
|
+
};
|
|
754
|
+
|
|
701
755
|
export const HealthChecker: {
|
|
702
756
|
new (options?: HealthCheckOptions): HealthChecker;
|
|
703
757
|
};
|