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 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.7.0
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 (Current)
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.7 Features Guide](./docs/V5.7_FEATURES.md) - ORM-like features and migrations documentation
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navis.js",
3
- "version": "5.7.0",
3
+ "version": "5.8.0",
4
4
  "description": "A lightweight, serverless-first, microservice API framework designed for AWS Lambda and Node.js",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -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
- resolve({
83
- statusCode: res.statusCode,
84
- headers: res.headers,
85
- data: body,
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
  };