@wgtechlabs/nuvex 0.1.1

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +427 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/cjs/core/client.js +981 -0
  5. package/dist/cjs/core/client.js.map +1 -0
  6. package/dist/cjs/core/database.js +297 -0
  7. package/dist/cjs/core/database.js.map +1 -0
  8. package/dist/cjs/core/engine.js +1202 -0
  9. package/dist/cjs/core/engine.js.map +1 -0
  10. package/dist/cjs/core/index.js +35 -0
  11. package/dist/cjs/core/index.js.map +1 -0
  12. package/dist/cjs/index.js +109 -0
  13. package/dist/cjs/index.js.map +1 -0
  14. package/dist/cjs/interfaces/index.js +12 -0
  15. package/dist/cjs/interfaces/index.js.map +1 -0
  16. package/dist/cjs/layers/index.js +22 -0
  17. package/dist/cjs/layers/index.js.map +1 -0
  18. package/dist/cjs/layers/memory.js +388 -0
  19. package/dist/cjs/layers/memory.js.map +1 -0
  20. package/dist/cjs/layers/postgres.js +492 -0
  21. package/dist/cjs/layers/postgres.js.map +1 -0
  22. package/dist/cjs/layers/redis.js +388 -0
  23. package/dist/cjs/layers/redis.js.map +1 -0
  24. package/dist/cjs/types/index.js +52 -0
  25. package/dist/cjs/types/index.js.map +1 -0
  26. package/dist/esm/core/client.js +944 -0
  27. package/dist/esm/core/client.js.map +1 -0
  28. package/dist/esm/core/database.js +289 -0
  29. package/dist/esm/core/database.js.map +1 -0
  30. package/dist/esm/core/engine.js +1198 -0
  31. package/dist/esm/core/engine.js.map +1 -0
  32. package/dist/esm/core/index.js +16 -0
  33. package/dist/esm/core/index.js.map +1 -0
  34. package/dist/esm/index.js +87 -0
  35. package/dist/esm/index.js.map +1 -0
  36. package/dist/esm/interfaces/index.js +11 -0
  37. package/dist/esm/interfaces/index.js.map +1 -0
  38. package/dist/esm/layers/index.js +16 -0
  39. package/dist/esm/layers/index.js.map +1 -0
  40. package/dist/esm/layers/memory.js +384 -0
  41. package/dist/esm/layers/memory.js.map +1 -0
  42. package/dist/esm/layers/postgres.js +485 -0
  43. package/dist/esm/layers/postgres.js.map +1 -0
  44. package/dist/esm/layers/redis.js +384 -0
  45. package/dist/esm/layers/redis.js.map +1 -0
  46. package/dist/esm/types/index.js +49 -0
  47. package/dist/esm/types/index.js.map +1 -0
  48. package/dist/types/core/client.d.ts +561 -0
  49. package/dist/types/core/client.d.ts.map +1 -0
  50. package/dist/types/core/database.d.ts +130 -0
  51. package/dist/types/core/database.d.ts.map +1 -0
  52. package/dist/types/core/engine.d.ts +450 -0
  53. package/dist/types/core/engine.d.ts.map +1 -0
  54. package/dist/types/core/index.d.ts +13 -0
  55. package/dist/types/core/index.d.ts.map +1 -0
  56. package/dist/types/index.d.ts +85 -0
  57. package/dist/types/index.d.ts.map +1 -0
  58. package/dist/types/interfaces/index.d.ts +209 -0
  59. package/dist/types/interfaces/index.d.ts.map +1 -0
  60. package/dist/types/layers/index.d.ts +16 -0
  61. package/dist/types/layers/index.d.ts.map +1 -0
  62. package/dist/types/layers/memory.d.ts +261 -0
  63. package/dist/types/layers/memory.d.ts.map +1 -0
  64. package/dist/types/layers/postgres.d.ts +313 -0
  65. package/dist/types/layers/postgres.d.ts.map +1 -0
  66. package/dist/types/layers/redis.d.ts +248 -0
  67. package/dist/types/layers/redis.d.ts.map +1 -0
  68. package/dist/types/types/index.d.ts +410 -0
  69. package/dist/types/types/index.d.ts.map +1 -0
  70. package/package.json +90 -0
@@ -0,0 +1,1202 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StorageEngine = void 0;
4
+ const index_js_1 = require("../types/index.js");
5
+ const index_js_2 = require("../layers/index.js");
6
+ /**
7
+ * # StorageEngine - Multi-layer Storage Architecture
8
+ *
9
+ * The core storage engine that implements Nuvex's intelligent three-tier storage system.
10
+ * Provides automatic data management, intelligent caching, and comprehensive fallback mechanisms
11
+ * across Memory, Redis, and PostgreSQL layers.
12
+ *
13
+ * ## Architecture Design
14
+ *
15
+ * The StorageEngine follows a hierarchical approach where data flows through layers based on
16
+ * access patterns and configured policies:
17
+ *
18
+ * ```
19
+ * ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
20
+ * │ Memory │───▶│ Redis │───▶│ PostgreSQL │
21
+ * │ (Layer 1) │ │ (Layer 2) │ │ (Layer 3) │
22
+ * │ < 1ms │ │ 1-5ms │ │ 5-50ms │
23
+ * └─────────────┘ └─────────────┘ └─────────────┘
24
+ * ```
25
+ *
26
+ * ## Key Features
27
+ *
28
+ * ### Intelligent Data Management
29
+ * - **Automatic Promotion**: Frequently accessed data moves to faster layers
30
+ * - **Smart Demotion**: Unused data gracefully moves to persistent storage
31
+ * - **TTL Management**: Configurable time-to-live for each layer
32
+ * - **Memory Optimization**: LRU eviction and automatic cleanup
33
+ *
34
+ * ### Performance Optimization
35
+ * - **Sub-millisecond Access**: Memory cache for hot data
36
+ * - **Batch Operations**: Efficient bulk data operations
37
+ * - **Connection Pooling**: Optimized database connections
38
+ * - **Metrics Collection**: Real-time performance monitoring
39
+ *
40
+ * ### Reliability Features
41
+ * - **Graceful Degradation**: Automatic fallback when layers are unavailable
42
+ * - **Error Recovery**: Comprehensive error handling and logging
43
+ * - **Data Consistency**: Synchronization across all storage layers
44
+ * - **Health Monitoring**: Layer availability and performance tracking
45
+ *
46
+ * ## Usage Examples
47
+ *
48
+ * ### Basic Operations
49
+ * ```typescript
50
+ * const engine = new StorageEngine({
51
+ * memory: { ttl: 3600000, maxSize: 10000 },
52
+ * redis: { url: 'redis://localhost:6379' },
53
+ * postgres: { host: 'localhost', database: 'app' }
54
+ * });
55
+ *
56
+ * await engine.connect();
57
+ *
58
+ * // Set data across all layers
59
+ * await engine.set('user:123', userData);
60
+ *
61
+ * // Get data (checks Memory → Redis → PostgreSQL)
62
+ * const user = await engine.get('user:123');
63
+ * ```
64
+ *
65
+ * ### Layer-specific Operations
66
+ * ```typescript
67
+ * // Store only in Redis
68
+ * await engine.set('session:abc', sessionData, {
69
+ * layer: StorageLayer.REDIS
70
+ * });
71
+ *
72
+ * // Skip cache and go directly to PostgreSQL
73
+ * const criticalData = await engine.get('config:critical', {
74
+ * skipCache: true
75
+ * });
76
+ * ```
77
+ *
78
+ * ### Batch Operations
79
+ * ```typescript
80
+ * const operations = [
81
+ * { operation: 'set', key: 'key1', value: 'value1' },
82
+ * { operation: 'set', key: 'key2', value: 'value2' }
83
+ * ];
84
+ *
85
+ * const results = await engine.setBatch(operations);
86
+ * ```
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Initialize with full configuration
91
+ * const engine = new StorageEngine({
92
+ * memory: {
93
+ * ttl: 24 * 60 * 60 * 1000, // 24 hours
94
+ * maxSize: 10000
95
+ * },
96
+ * redis: {
97
+ * url: 'redis://localhost:6379',
98
+ * ttl: 3 * 24 * 60 * 60 // 3 days
99
+ * },
100
+ * postgres: {
101
+ * host: 'localhost',
102
+ * port: 5432,
103
+ * database: 'myapp',
104
+ * user: 'user',
105
+ * password: 'password'
106
+ * },
107
+ * logging: {
108
+ * enabled: true,
109
+ * logger: console
110
+ * }
111
+ * });
112
+ *
113
+ * await engine.connect();
114
+ *
115
+ * // Your storage operations here...
116
+ *
117
+ * await engine.disconnect();
118
+ * ```
119
+ *
120
+ * @see {@link NuvexClient} for high-level client operations
121
+ * @see {@link NuvexConfig} for configuration options
122
+ * @see {@link StorageOptions} for operation-specific options
123
+ *
124
+ * @public
125
+ * @category Core
126
+ */
127
+ class StorageEngine {
128
+ /**
129
+ * Creates a new StorageEngine instance with the specified configuration.
130
+ *
131
+ * The constructor initializes all three storage layers and sets up automatic
132
+ * memory cleanup intervals. No connections are established until `connect()` is called.
133
+ *
134
+ * @param config - Complete configuration object for all storage layers
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const engine = new StorageEngine({
139
+ * memory: { ttl: 3600000, maxSize: 10000 },
140
+ * redis: { url: 'redis://localhost:6379', ttl: 86400 },
141
+ * postgres: { host: 'localhost', database: 'myapp' },
142
+ * logging: { enabled: true }
143
+ * });
144
+ * ```
145
+ *
146
+ * @throws {Error} When configuration is invalid
147
+ * @since 1.0.0
148
+ */
149
+ constructor(config) {
150
+ this.config = config;
151
+ this.connected = false;
152
+ this.cleanupInterval = null;
153
+ // Logging setup
154
+ this.logger = config.logging?.enabled ? (config.logging.logger || null) : null;
155
+ // Layer 1: Memory storage with LRU eviction
156
+ const maxMemorySize = config.memory?.maxSize || 10000; // 10k entries default
157
+ this.l1Memory = new index_js_2.MemoryStorage(maxMemorySize, this.logger);
158
+ // Layer 2: Redis storage (optional)
159
+ this.l2Redis = config.redis?.url
160
+ ? new index_js_2.RedisStorage(config.redis.url, this.logger)
161
+ : null;
162
+ // Layer 3: PostgreSQL storage
163
+ this.l3Postgres = config.postgres
164
+ ? new index_js_2.PostgresStorage(config.postgres, this.logger)
165
+ : null;
166
+ // Metrics initialization
167
+ this.metrics = {
168
+ memoryHits: 0,
169
+ memoryMisses: 0,
170
+ redisHits: 0,
171
+ redisMisses: 0,
172
+ postgresHits: 0,
173
+ postgresMisses: 0,
174
+ totalOperations: 0,
175
+ averageResponseTime: 0
176
+ };
177
+ // Start memory cleanup interval
178
+ this.startMemoryCleanup();
179
+ }
180
+ log(level, message, meta) {
181
+ if (this.logger) {
182
+ this.logger[level](message, meta);
183
+ }
184
+ }
185
+ /**
186
+ * Establishes connections to all configured storage layers.
187
+ *
188
+ * This method initializes connections to Redis and PostgreSQL (if configured)
189
+ * and sets up the internal state for multi-layer operations. The memory layer
190
+ * is always available and doesn't require connection setup.
191
+ *
192
+ * @throws {Error} When connection to any configured layer fails
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const engine = new StorageEngine(config);
197
+ * await engine.connect();
198
+ * console.log('All storage layers connected');
199
+ * ```
200
+ *
201
+ * @since 1.0.0
202
+ * @public
203
+ */
204
+ async connect() {
205
+ try {
206
+ // Connect to Redis (L2) - optional, gracefully degrade if unavailable
207
+ if (this.l2Redis) {
208
+ try {
209
+ await this.l2Redis.connect();
210
+ this.log('info', 'Redis L2 connected for Nuvex storage');
211
+ }
212
+ catch (error) {
213
+ this.log('warn', 'Redis L2 not available, using Memory + PostgreSQL only', {
214
+ error: error instanceof Error ? error.message : String(error)
215
+ });
216
+ this.l2Redis = null;
217
+ }
218
+ }
219
+ else {
220
+ this.log('info', 'Redis L2 URL not provided, using Memory + PostgreSQL only');
221
+ }
222
+ // Connect to PostgreSQL (L3) - critical for data persistence
223
+ if (this.l3Postgres) {
224
+ await this.l3Postgres.connect();
225
+ this.log('info', 'PostgreSQL L3 connected for Nuvex storage');
226
+ }
227
+ this.connected = true;
228
+ this.log('info', 'Nuvex StorageEngine initialized with 3-layer architecture');
229
+ }
230
+ catch (error) {
231
+ const err = error;
232
+ this.log('error', 'Nuvex StorageEngine connection failed', {
233
+ error: err.message,
234
+ stack: err.stack
235
+ });
236
+ throw error;
237
+ }
238
+ }
239
+ async disconnect() {
240
+ // Disconnect from Redis (L2)
241
+ if (this.l2Redis) {
242
+ await this.l2Redis.disconnect();
243
+ }
244
+ // Disconnect from PostgreSQL (L3)
245
+ if (this.l3Postgres) {
246
+ await this.l3Postgres.disconnect();
247
+ }
248
+ // Stop memory cleanup interval
249
+ if (this.cleanupInterval) {
250
+ clearInterval(this.cleanupInterval);
251
+ this.cleanupInterval = null;
252
+ }
253
+ this.connected = false;
254
+ this.log('info', 'Nuvex StorageEngine disconnected');
255
+ }
256
+ isConnected() {
257
+ return this.connected;
258
+ }
259
+ /**
260
+ * Retrieves a value from storage using the intelligent layer hierarchy.
261
+ *
262
+ * The get operation follows the multi-layer approach:
263
+ * 1. **Memory Cache**: Checks in-memory storage first (fastest)
264
+ * 2. **Redis Cache**: Falls back to Redis if not in memory
265
+ * 3. **PostgreSQL**: Final fallback to persistent storage
266
+ *
267
+ * When data is found in a lower layer, it's automatically promoted to higher
268
+ * layers for faster future access (intelligent caching).
269
+ *
270
+ * @template T - The expected type of the stored value
271
+ * @param key - The storage key to retrieve
272
+ * @param options - Optional configuration for the get operation
273
+ * @returns Promise resolving to the stored value or null if not found
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * // Basic get operation
278
+ * const userData = await engine.get<UserData>('user:123');
279
+ *
280
+ * // Get from specific layer only
281
+ * const sessionData = await engine.get('session:abc', {
282
+ * layer: StorageLayer.REDIS
283
+ * });
284
+ *
285
+ * // Skip cache layers and get from PostgreSQL directly
286
+ * const criticalData = await engine.get('config:critical', {
287
+ * skipCache: true
288
+ * });
289
+ * ```
290
+ *
291
+ * @since 1.0.0
292
+ * @public
293
+ */
294
+ async get(key, options = {}) {
295
+ const startTime = Date.now();
296
+ this.metrics.totalOperations++;
297
+ try {
298
+ // Default TTL configuration
299
+ const defaultTTL = this.config.redis?.ttl || 3 * 24 * 60 * 60; // 3 days in seconds
300
+ // Skip cache if requested - go directly to L3
301
+ if (options?.skipCache && this.l3Postgres) {
302
+ const value = await this.l3Postgres.get(key);
303
+ this.updateResponseTime(Date.now() - startTime);
304
+ return value;
305
+ }
306
+ // Specific layer requested
307
+ if (options?.layer) {
308
+ let value = null;
309
+ switch (options.layer) {
310
+ case index_js_1.StorageLayer.MEMORY:
311
+ value = await this.l1Memory.get(key);
312
+ break;
313
+ case index_js_1.StorageLayer.REDIS:
314
+ value = this.l2Redis ? await this.l2Redis.get(key) : null;
315
+ break;
316
+ case index_js_1.StorageLayer.POSTGRES:
317
+ value = this.l3Postgres ? await this.l3Postgres.get(key) : null;
318
+ break;
319
+ }
320
+ this.updateResponseTime(Date.now() - startTime);
321
+ return value;
322
+ }
323
+ // Layer 1: Check memory cache first (fastest)
324
+ let data = await this.l1Memory.get(key);
325
+ if (data !== null) {
326
+ this.metrics.memoryHits++;
327
+ this.updateResponseTime(Date.now() - startTime);
328
+ return data;
329
+ }
330
+ this.metrics.memoryMisses++;
331
+ // Layer 2: Check Redis cache (fast distributed cache)
332
+ if (this.l2Redis) {
333
+ data = await this.l2Redis.get(key);
334
+ if (data !== null) {
335
+ this.metrics.redisHits++;
336
+ // Warm L1 cache for next access
337
+ await this.l1Memory.set(key, data, defaultTTL);
338
+ this.updateResponseTime(Date.now() - startTime);
339
+ return data;
340
+ }
341
+ this.metrics.redisMisses++;
342
+ }
343
+ // Layer 3: Check PostgreSQL (persistent storage, source of truth)
344
+ if (this.l3Postgres) {
345
+ data = await this.l3Postgres.get(key);
346
+ if (data !== null) {
347
+ this.metrics.postgresHits++;
348
+ // Warm both L1 and L2 caches for future access
349
+ await Promise.allSettled([
350
+ this.l1Memory.set(key, data, defaultTTL),
351
+ this.l2Redis ? this.l2Redis.set(key, data, defaultTTL) : Promise.resolve()
352
+ ]);
353
+ this.updateResponseTime(Date.now() - startTime);
354
+ return data;
355
+ }
356
+ this.metrics.postgresMisses++;
357
+ }
358
+ this.updateResponseTime(Date.now() - startTime);
359
+ return null;
360
+ }
361
+ catch (error) {
362
+ const err = error;
363
+ this.log('error', `Error getting ${key}`, {
364
+ error: err.message,
365
+ stack: err.stack,
366
+ operation: 'get',
367
+ key
368
+ });
369
+ this.updateResponseTime(Date.now() - startTime);
370
+ return null;
371
+ }
372
+ }
373
+ /**
374
+ * Set value in storage layers using L3-first write strategy
375
+ *
376
+ * **L3-First Write Strategy:**
377
+ * 1. Write to PostgreSQL (L3) first as source of truth
378
+ * 2. If L3 write succeeds, warm caches (L1, L2) using Promise.allSettled
379
+ * 3. Cache failures don't break the operation (graceful degradation)
380
+ *
381
+ * **Error Handling:**
382
+ * - Returns `false` if L3 (PostgreSQL) write fails - operation is aborted
383
+ * - Returns `false` if engine is not connected
384
+ * - Returns `true` if L3 write succeeds (cache failures are tolerated)
385
+ * - For memory/Redis-only deployments (no L3), cache write success determines result
386
+ *
387
+ * **Usage Recommendations:**
388
+ * ```typescript
389
+ * // Always check the return value for critical data
390
+ * const success = await engine.set('user:123', userData);
391
+ * if (!success) {
392
+ * // Handle failure: retry, log, alert, or use fallback
393
+ * console.error('Failed to persist user data');
394
+ * throw new Error('Storage operation failed');
395
+ * }
396
+ *
397
+ * // For non-critical data, you may proceed regardless
398
+ * await engine.set('cache:temp', tempData); // Fire and forget
399
+ * ```
400
+ *
401
+ * @param key - The key to store
402
+ * @param value - The value to store
403
+ * @param options - Optional storage options (ttl, layer targeting)
404
+ * @returns Promise resolving to true if operation succeeded, false otherwise
405
+ */
406
+ async set(key, value, options = {}) {
407
+ if (!this.connected) {
408
+ return false;
409
+ }
410
+ const startTime = Date.now();
411
+ this.metrics.totalOperations++;
412
+ try {
413
+ const ttl = options?.ttl;
414
+ // Specific layer requested
415
+ if (options?.layer) {
416
+ switch (options.layer) {
417
+ case index_js_1.StorageLayer.MEMORY:
418
+ await this.l1Memory.set(key, value, ttl);
419
+ this.updateResponseTime(Date.now() - startTime);
420
+ return true;
421
+ case index_js_1.StorageLayer.REDIS:
422
+ if (this.l2Redis) {
423
+ await this.l2Redis.set(key, value, ttl);
424
+ }
425
+ this.updateResponseTime(Date.now() - startTime);
426
+ return true;
427
+ case index_js_1.StorageLayer.POSTGRES:
428
+ if (this.l3Postgres) {
429
+ await this.l3Postgres.set(key, value, ttl);
430
+ }
431
+ this.updateResponseTime(Date.now() - startTime);
432
+ return true;
433
+ }
434
+ }
435
+ // L3-First Write Strategy: Write to PostgreSQL first (source of truth)
436
+ if (this.l3Postgres) {
437
+ try {
438
+ await this.l3Postgres.set(key, value, ttl);
439
+ }
440
+ catch (error) {
441
+ const err = error;
442
+ this.log('error', `Critical: L3 PostgreSQL write failed for ${key}`, {
443
+ error: err.message,
444
+ stack: err.stack,
445
+ operation: 'set',
446
+ key,
447
+ layer: 'L3'
448
+ });
449
+ this.updateResponseTime(Date.now() - startTime);
450
+ return false; // L3 failure is critical - abort operation
451
+ }
452
+ }
453
+ // Best-effort cache warming - tolerate cache failures using Promise.allSettled
454
+ await Promise.allSettled([
455
+ this.l1Memory.set(key, value, ttl),
456
+ this.l2Redis ? this.l2Redis.set(key, value, ttl) : Promise.resolve()
457
+ ]);
458
+ this.updateResponseTime(Date.now() - startTime);
459
+ return true;
460
+ }
461
+ catch (error) {
462
+ const err = error;
463
+ this.log('error', `Error setting ${key}`, {
464
+ error: err.message,
465
+ stack: err.stack,
466
+ operation: 'set',
467
+ key
468
+ });
469
+ this.updateResponseTime(Date.now() - startTime);
470
+ return false;
471
+ }
472
+ }
473
+ /**
474
+ * Delete from all storage layers using resilient approach
475
+ *
476
+ * Uses Promise.allSettled to attempt deletion from all layers without
477
+ * failing if individual layers are unavailable. This provides graceful
478
+ * degradation - even if cache layers fail, the operation continues.
479
+ *
480
+ * @param key - The key to delete
481
+ * @param options - Optional storage options (layer targeting)
482
+ * @returns Promise resolving to true if operation completed
483
+ */
484
+ async delete(key, options = {}) {
485
+ const startTime = Date.now();
486
+ this.metrics.totalOperations++;
487
+ try {
488
+ // Specific layer requested
489
+ if (options?.layer) {
490
+ switch (options.layer) {
491
+ case index_js_1.StorageLayer.MEMORY:
492
+ await this.l1Memory.delete(key);
493
+ this.updateResponseTime(Date.now() - startTime);
494
+ return true;
495
+ case index_js_1.StorageLayer.REDIS:
496
+ if (this.l2Redis) {
497
+ await this.l2Redis.delete(key);
498
+ }
499
+ this.updateResponseTime(Date.now() - startTime);
500
+ return true;
501
+ case index_js_1.StorageLayer.POSTGRES:
502
+ if (this.l3Postgres) {
503
+ await this.l3Postgres.delete(key);
504
+ }
505
+ this.updateResponseTime(Date.now() - startTime);
506
+ return true;
507
+ }
508
+ }
509
+ // Delete from all layers using Promise.allSettled for resilience
510
+ // Even if some layers fail, we continue with others
511
+ await Promise.allSettled([
512
+ this.l1Memory.delete(key),
513
+ this.l2Redis ? this.l2Redis.delete(key) : Promise.resolve(),
514
+ this.l3Postgres ? this.l3Postgres.delete(key) : Promise.resolve()
515
+ ]);
516
+ this.updateResponseTime(Date.now() - startTime);
517
+ return true;
518
+ }
519
+ catch (error) {
520
+ const err = error;
521
+ this.log('error', `Error deleting ${key}`, {
522
+ error: err.message,
523
+ stack: err.stack,
524
+ operation: 'delete',
525
+ key
526
+ });
527
+ this.updateResponseTime(Date.now() - startTime);
528
+ return false;
529
+ }
530
+ }
531
+ /**
532
+ * Check if key exists in any storage layer
533
+ */
534
+ async exists(key, options = {}) {
535
+ try {
536
+ // Specific layer requested
537
+ if (options?.layer) {
538
+ switch (options.layer) {
539
+ case index_js_1.StorageLayer.MEMORY:
540
+ return await this.l1Memory.exists(key);
541
+ case index_js_1.StorageLayer.REDIS:
542
+ return this.l2Redis ? await this.l2Redis.exists(key) : false;
543
+ case index_js_1.StorageLayer.POSTGRES:
544
+ return this.l3Postgres ? await this.l3Postgres.exists(key) : false;
545
+ }
546
+ }
547
+ // Check L1 (Memory) first
548
+ if (await this.l1Memory.exists(key)) {
549
+ return true;
550
+ }
551
+ // Check L2 (Redis)
552
+ if (this.l2Redis && await this.l2Redis.exists(key)) {
553
+ return true;
554
+ }
555
+ // Check L3 (PostgreSQL)
556
+ if (this.l3Postgres && await this.l3Postgres.exists(key)) {
557
+ return true;
558
+ }
559
+ return false;
560
+ }
561
+ catch (error) {
562
+ const err = error;
563
+ this.log('error', `Error checking existence of ${key}`, {
564
+ error: err.message,
565
+ stack: err.stack,
566
+ operation: 'exists',
567
+ key
568
+ });
569
+ return false;
570
+ }
571
+ }
572
+ /**
573
+ * Set expiration for a key
574
+ *
575
+ * Note: This is a simplified implementation that re-sets the value with new TTL.
576
+ * For a more efficient implementation, layers would need an expire() method.
577
+ */
578
+ async expire(key, ttl) {
579
+ try {
580
+ // Get current value
581
+ const value = await this.get(key);
582
+ if (value === null) {
583
+ return false;
584
+ }
585
+ // Re-set with new TTL
586
+ return await this.set(key, value, { ttl });
587
+ }
588
+ catch (error) {
589
+ const err = error;
590
+ this.log('error', `Error setting expiration for ${key}`, {
591
+ error: err.message,
592
+ operation: 'expire',
593
+ key,
594
+ ttl
595
+ });
596
+ return false;
597
+ }
598
+ }
599
+ /**
600
+ * Atomically increment a numeric value
601
+ *
602
+ * This method uses layer-specific atomic operations when available,
603
+ * providing thread-safe increments across all storage layers.
604
+ *
605
+ * **Important:** The key must contain a numeric value (or not exist).
606
+ * If the key contains a non-numeric value, the operation will fail:
607
+ * - Redis: Throws error "value is not an integer"
608
+ * - PostgreSQL: Throws error during numeric cast
609
+ * - Memory: Treats non-numeric values as 0
610
+ *
611
+ * The increment cascades through layers:
612
+ * 1. If L3 (PostgreSQL) is available, use its atomic UPSERT
613
+ * 2. Else if L2 (Redis) is available, use INCRBY
614
+ * 3. Else use L1 (Memory) increment
615
+ *
616
+ * After incrementing in the authoritative layer, the new value is
617
+ * propagated to higher layers for cache consistency.
618
+ *
619
+ * @param key - The key to increment (must contain numeric value or not exist)
620
+ * @param delta - The amount to increment by (default: 1)
621
+ * @param ttl - Optional TTL in milliseconds
622
+ * @returns Promise resolving to the new value after increment
623
+ * @throws {Error} If the key contains a non-numeric value (Redis/PostgreSQL)
624
+ * @throws {Error} If no storage layer is available
625
+ *
626
+ * @example
627
+ * ```typescript
628
+ * // Increment counter by 1
629
+ * const newValue = await engine.increment('page_views');
630
+ *
631
+ * // Increment with custom delta
632
+ * const credits = await engine.increment('user:credits', 10);
633
+ *
634
+ * // Decrement (negative delta)
635
+ * const remaining = await engine.increment('inventory', -1);
636
+ * ```
637
+ */
638
+ async increment(key, delta = 1, ttl) {
639
+ const startTime = Date.now();
640
+ const ttlSeconds = ttl ? Math.floor(ttl / 1000) : undefined;
641
+ let newValue;
642
+ try {
643
+ // Use atomic increment from the most authoritative layer available
644
+ if (this.l3Postgres?.increment) {
645
+ newValue = await this.l3Postgres.increment(key, delta, ttlSeconds);
646
+ this.metrics.totalOperations++;
647
+ // Propagate to upper layers for cache consistency
648
+ if (this.l2Redis) {
649
+ await this.l2Redis.set(key, newValue, ttlSeconds);
650
+ }
651
+ if (this.l1Memory) {
652
+ await this.l1Memory.set(key, newValue, ttlSeconds);
653
+ }
654
+ }
655
+ else if (this.l2Redis?.increment) {
656
+ newValue = await this.l2Redis.increment(key, delta, ttlSeconds);
657
+ this.metrics.totalOperations++;
658
+ // Propagate to memory layer
659
+ if (this.l1Memory) {
660
+ await this.l1Memory.set(key, newValue, ttlSeconds);
661
+ }
662
+ }
663
+ else if (this.l1Memory?.increment) {
664
+ newValue = await this.l1Memory.increment(key, delta, ttlSeconds);
665
+ this.metrics.totalOperations++;
666
+ }
667
+ else {
668
+ throw new Error('No storage layer available for increment operation');
669
+ }
670
+ const duration = Date.now() - startTime;
671
+ this.log('debug', `Incremented ${key} by ${delta}`, {
672
+ operation: 'increment',
673
+ key,
674
+ delta,
675
+ newValue,
676
+ duration,
677
+ success: true
678
+ });
679
+ return newValue;
680
+ }
681
+ catch (error) {
682
+ const err = error;
683
+ const duration = Date.now() - startTime;
684
+ this.log('error', `Error incrementing ${key}`, {
685
+ error: err.message,
686
+ operation: 'increment',
687
+ key,
688
+ delta,
689
+ duration,
690
+ success: false
691
+ });
692
+ throw error;
693
+ }
694
+ }
695
+ // Batch operations
696
+ async setBatch(operations) {
697
+ const settled = await Promise.allSettled(operations.map(async (op) => {
698
+ if (op.operation === 'set' && op.value !== undefined) {
699
+ const success = await this.set(op.key, op.value, op.options);
700
+ return { key: op.key, success };
701
+ }
702
+ return { key: op.key, success: false, error: 'Invalid operation' };
703
+ }));
704
+ return settled.map((result, index) => result.status === 'fulfilled'
705
+ ? result.value
706
+ : { key: operations[index].key, success: false, error: result.reason.message });
707
+ }
708
+ async getBatch(keys, options = {}) {
709
+ const settled = await Promise.allSettled(keys.map(async (key) => {
710
+ const value = await this.get(key, options);
711
+ return value !== null
712
+ ? { key, success: true, value }
713
+ : { key, success: false, value: null };
714
+ }));
715
+ return settled.map((result, index) => result.status === 'fulfilled'
716
+ ? result.value
717
+ : { key: keys[index], success: false, error: result.reason.message });
718
+ }
719
+ async deleteBatch(keys) {
720
+ const settled = await Promise.allSettled(keys.map(async (key) => {
721
+ const existed = await this.exists(key);
722
+ if (existed) {
723
+ const success = await this.delete(key);
724
+ return { key, success };
725
+ }
726
+ return { key, success: false };
727
+ }));
728
+ return settled.map((result, index) => result.status === 'fulfilled'
729
+ ? result.value
730
+ : { key: keys[index], success: false, error: result.reason.message });
731
+ }
732
+ // Query operations
733
+ async query(options) {
734
+ // This is a basic implementation - can be enhanced based on needs
735
+ const keys = await this.keys(options.pattern);
736
+ const items = [];
737
+ for (const key of keys) {
738
+ const value = await this.get(key);
739
+ if (value !== null) {
740
+ items.push({
741
+ key,
742
+ value,
743
+ metadata: {
744
+ value,
745
+ createdAt: new Date(), // This would need to be tracked
746
+ layer: (await this.getLayerInfo(key))?.layer || index_js_1.StorageLayer.MEMORY
747
+ }
748
+ });
749
+ }
750
+ }
751
+ // Apply sorting and pagination
752
+ const sortedItems = this.applySorting(items, options);
753
+ const paginatedItems = this.applyPagination(sortedItems, options);
754
+ return {
755
+ items: paginatedItems,
756
+ total: items.length,
757
+ hasMore: (options.offset || 0) + (options.limit || items.length) < items.length
758
+ };
759
+ }
760
+ /**
761
+ * Get all keys matching a pattern across all storage layers
762
+ *
763
+ * Collects keys from all available layers (Memory, Redis, PostgreSQL) and
764
+ * returns deduplicated results. Currently implemented for the Memory layer;
765
+ * Redis and PostgreSQL layer support will be added in future versions.
766
+ *
767
+ * @param pattern - Glob pattern for key matching (default: '*' returns all keys)
768
+ * @returns Promise resolving to array of unique matching keys
769
+ *
770
+ * @since 1.0.0
771
+ */
772
+ async keys(pattern = '*') {
773
+ const allKeys = new Set();
774
+ // Collect keys from Memory (L1) - always available
775
+ if (this.l1Memory.keys) {
776
+ try {
777
+ const memoryKeys = await this.l1Memory.keys(pattern);
778
+ for (const key of memoryKeys) {
779
+ allKeys.add(key);
780
+ }
781
+ }
782
+ catch (error) {
783
+ this.log('warn', 'Error collecting keys from Memory L1', {
784
+ error: error instanceof Error ? error.message : String(error)
785
+ });
786
+ }
787
+ }
788
+ // Collect keys from Redis (L2) - if keys() is implemented
789
+ const l2Layer = this.l2Redis;
790
+ if (l2Layer?.keys) {
791
+ try {
792
+ const redisKeys = await l2Layer.keys(pattern);
793
+ for (const key of redisKeys) {
794
+ allKeys.add(key);
795
+ }
796
+ }
797
+ catch (error) {
798
+ this.log('warn', 'Error collecting keys from Redis L2', {
799
+ error: error instanceof Error ? error.message : String(error)
800
+ });
801
+ }
802
+ }
803
+ // Collect keys from PostgreSQL (L3) - if keys() is implemented
804
+ const l3Layer = this.l3Postgres;
805
+ if (l3Layer?.keys) {
806
+ try {
807
+ const pgKeys = await l3Layer.keys(pattern);
808
+ for (const key of pgKeys) {
809
+ allKeys.add(key);
810
+ }
811
+ }
812
+ catch (error) {
813
+ this.log('warn', 'Error collecting keys from PostgreSQL L3', {
814
+ error: error instanceof Error ? error.message : String(error)
815
+ });
816
+ }
817
+ }
818
+ return Array.from(allKeys);
819
+ }
820
+ async clear(pattern = '*') {
821
+ let cleared = 0;
822
+ // For now, only support clearing all (pattern = '*')
823
+ // Pattern-based clearing would require keys() implementation in each layer
824
+ if (pattern === '*') {
825
+ // Clear memory (L1)
826
+ cleared = this.l1Memory.size();
827
+ await this.l1Memory.clear();
828
+ // Clear Redis (L2) - best effort
829
+ if (this.l2Redis) {
830
+ try {
831
+ await this.l2Redis.clear();
832
+ }
833
+ catch (error) {
834
+ this.log('warn', 'Error clearing Redis L2', {
835
+ error: error instanceof Error ? error.message : String(error)
836
+ });
837
+ }
838
+ }
839
+ // Clear PostgreSQL (L3) - best effort
840
+ if (this.l3Postgres) {
841
+ try {
842
+ await this.l3Postgres.clear();
843
+ }
844
+ catch (error) {
845
+ this.log('warn', 'Error clearing PostgreSQL L3', {
846
+ error: error instanceof Error ? error.message : String(error)
847
+ });
848
+ }
849
+ }
850
+ }
851
+ else {
852
+ // Pattern-based clearing - iterate through keys
853
+ const keys = await this.keys(pattern);
854
+ for (const key of keys) {
855
+ await this.delete(key);
856
+ cleared++;
857
+ }
858
+ }
859
+ return cleared;
860
+ }
861
+ // Metrics and monitoring
862
+ /**
863
+ * Get performance metrics for all layers or specific layer(s)
864
+ *
865
+ * Returns metrics about storage operations and performance. Can be filtered
866
+ * to return metrics for specific layers only.
867
+ *
868
+ * **Metrics by Layer:**
869
+ * - Memory: memoryHits, memoryMisses, memorySize, memoryMaxSize
870
+ * - Redis: redisHits, redisMisses
871
+ * - PostgreSQL: postgresHits, postgresMisses
872
+ * - Overall: totalOperations, cacheHitRatio, averageResponseTime
873
+ *
874
+ * @param layers - Optional layer(s) to get metrics for. If not provided, returns all metrics.
875
+ * Can be a single layer string, 'all', or array of layer strings.
876
+ * @returns Object containing requested metrics
877
+ *
878
+ * @example
879
+ * ```typescript
880
+ * // Get all metrics
881
+ * const metrics = engine.getMetrics();
882
+ * // { memoryHits, memoryMisses, redisHits, redisMisses, postgresHits, ... }
883
+ *
884
+ * // Get specific layer metrics
885
+ * const memoryMetrics = engine.getMetrics('memory');
886
+ * // { memoryHits, memoryMisses, memorySize, memoryMaxSize }
887
+ *
888
+ * // Get multiple layer metrics
889
+ * const cacheMetrics = engine.getMetrics(['memory', 'redis']);
890
+ * // { memoryHits, memoryMisses, memorySize, memoryMaxSize, redisHits, redisMisses, totalOperations, averageResponseTime, cacheHitRatio }
891
+ * ```
892
+ *
893
+ * @since 1.0.0
894
+ * @public
895
+ */
896
+ getMetrics(layers) {
897
+ // If no layers specified or 'all' specified, return all metrics
898
+ if (!layers || layers === 'all') {
899
+ return {
900
+ ...this.metrics,
901
+ memorySize: this.l1Memory.size(),
902
+ memoryMaxSize: this.l1Memory.getMaxSize(),
903
+ cacheHitRatio: this.calculateCacheHitRatio()
904
+ };
905
+ }
906
+ // Normalize layers to an array
907
+ const layersToGet = typeof layers === 'string' ? [layers] : layers;
908
+ // Build filtered metrics object
909
+ const filteredMetrics = {};
910
+ for (const layer of layersToGet) {
911
+ switch (layer) {
912
+ case 'memory':
913
+ filteredMetrics.memoryHits = this.metrics.memoryHits;
914
+ filteredMetrics.memoryMisses = this.metrics.memoryMisses;
915
+ filteredMetrics.memorySize = this.l1Memory.size();
916
+ filteredMetrics.memoryMaxSize = this.l1Memory.getMaxSize();
917
+ break;
918
+ case 'redis':
919
+ filteredMetrics.redisHits = this.metrics.redisHits;
920
+ filteredMetrics.redisMisses = this.metrics.redisMisses;
921
+ break;
922
+ case 'postgres':
923
+ filteredMetrics.postgresHits = this.metrics.postgresHits;
924
+ filteredMetrics.postgresMisses = this.metrics.postgresMisses;
925
+ break;
926
+ }
927
+ }
928
+ // Add overall metrics if multiple layers are requested
929
+ if (layersToGet.length > 1) {
930
+ filteredMetrics.totalOperations = this.metrics.totalOperations;
931
+ filteredMetrics.averageResponseTime = this.metrics.averageResponseTime;
932
+ filteredMetrics.cacheHitRatio = this.calculateCacheHitRatio(layersToGet);
933
+ }
934
+ return filteredMetrics;
935
+ }
936
+ resetMetrics() {
937
+ this.metrics = {
938
+ memoryHits: 0,
939
+ memoryMisses: 0,
940
+ redisHits: 0,
941
+ redisMisses: 0,
942
+ postgresHits: 0,
943
+ postgresMisses: 0,
944
+ totalOperations: 0,
945
+ averageResponseTime: 0
946
+ };
947
+ }
948
+ /**
949
+ * Perform health check on all storage layers or specific layer(s)
950
+ *
951
+ * Uses Promise.allSettled to check all layers independently without
952
+ * failing if one layer is down. Each layer's ping() method is called
953
+ * to verify its operational status.
954
+ *
955
+ * **Layer Health Checks:**
956
+ * - Memory (L1): Always healthy if app is running
957
+ * - Redis (L2): PING command verification
958
+ * - PostgreSQL (L3): SELECT 1 query verification
959
+ *
960
+ * @param layers - Optional layer(s) to check. If not provided, checks all layers.
961
+ * Can be a single layer string or array of layer strings.
962
+ * @returns Promise resolving to health status of requested layer(s)
963
+ *
964
+ * @example
965
+ * ```typescript
966
+ * // Check all layers
967
+ * const health = await engine.healthCheck();
968
+ * // { memory: true, redis: true, postgres: true }
969
+ *
970
+ * // Check specific layer
971
+ * const redisHealth = await engine.healthCheck('redis');
972
+ * // { redis: true }
973
+ *
974
+ * // Check multiple specific layers
975
+ * const cacheHealth = await engine.healthCheck(['memory', 'redis']);
976
+ * // { memory: true, redis: true }
977
+ *
978
+ * if (!health.redis) {
979
+ * console.warn('Redis layer is down, degraded performance expected');
980
+ * }
981
+ * ```
982
+ *
983
+ * @since 1.0.0
984
+ * @public
985
+ */
986
+ async healthCheck(layers) {
987
+ // Normalize layers to an array
988
+ let layersToCheck;
989
+ if (!layers) {
990
+ // No layers specified - check all
991
+ layersToCheck = ['memory', 'redis', 'postgres'];
992
+ }
993
+ else if (typeof layers === 'string') {
994
+ // Single layer specified
995
+ layersToCheck = [layers];
996
+ }
997
+ else {
998
+ // Array of layers specified
999
+ layersToCheck = layers;
1000
+ }
1001
+ // Build promises only for requested layers
1002
+ const promises = [];
1003
+ const layerNames = [];
1004
+ for (const layer of layersToCheck) {
1005
+ switch (layer) {
1006
+ case 'memory':
1007
+ promises.push(this.l1Memory.ping());
1008
+ layerNames.push('memory');
1009
+ break;
1010
+ case 'redis':
1011
+ promises.push(this.l2Redis ? this.l2Redis.ping() : Promise.resolve(false));
1012
+ layerNames.push('redis');
1013
+ break;
1014
+ case 'postgres':
1015
+ promises.push(this.l3Postgres ? this.l3Postgres.ping() : Promise.resolve(false));
1016
+ layerNames.push('postgres');
1017
+ break;
1018
+ }
1019
+ }
1020
+ const results = await Promise.allSettled(promises);
1021
+ // Build result object with only requested layers
1022
+ const healthStatus = {};
1023
+ for (let i = 0; i < layerNames.length; i++) {
1024
+ const result = results[i];
1025
+ healthStatus[layerNames[i]] = result.status === 'fulfilled' && result.value === true;
1026
+ }
1027
+ return healthStatus;
1028
+ }
1029
+ // Layer management
1030
+ async promote(key, targetLayer) {
1031
+ try {
1032
+ const value = await this.get(key);
1033
+ if (value === null)
1034
+ return false;
1035
+ switch (targetLayer) {
1036
+ case index_js_1.StorageLayer.MEMORY:
1037
+ await this.l1Memory.set(key, value);
1038
+ return true;
1039
+ case index_js_1.StorageLayer.REDIS:
1040
+ if (this.l2Redis) {
1041
+ await this.l2Redis.set(key, value);
1042
+ return true;
1043
+ }
1044
+ return false;
1045
+ case index_js_1.StorageLayer.POSTGRES:
1046
+ if (this.l3Postgres) {
1047
+ await this.l3Postgres.set(key, value);
1048
+ return true;
1049
+ }
1050
+ return false;
1051
+ default:
1052
+ return false;
1053
+ }
1054
+ }
1055
+ catch (error) {
1056
+ this.log('error', `Error promoting ${key} to ${targetLayer}`, {
1057
+ error: error instanceof Error ? error.message : String(error)
1058
+ });
1059
+ return false;
1060
+ }
1061
+ }
1062
+ async demote(key, targetLayer) {
1063
+ // For now, demote means remove from higher layers
1064
+ try {
1065
+ switch (targetLayer) {
1066
+ case index_js_1.StorageLayer.POSTGRES:
1067
+ // Remove from memory (L1) and Redis (L2)
1068
+ await this.l1Memory.delete(key);
1069
+ if (this.l2Redis) {
1070
+ await this.l2Redis.delete(key);
1071
+ }
1072
+ return true;
1073
+ case index_js_1.StorageLayer.REDIS:
1074
+ // Remove from memory (L1) only
1075
+ await this.l1Memory.delete(key);
1076
+ return true;
1077
+ default:
1078
+ return false;
1079
+ }
1080
+ }
1081
+ catch (error) {
1082
+ this.log('error', `Error demoting ${key} to ${targetLayer}`, {
1083
+ error: error instanceof Error ? error.message : String(error)
1084
+ });
1085
+ return false;
1086
+ }
1087
+ }
1088
+ async getLayerInfo(key) {
1089
+ // Check which layer has the key (L1 → L2 → L3)
1090
+ if (await this.l1Memory.exists(key)) {
1091
+ return { layer: index_js_1.StorageLayer.MEMORY };
1092
+ }
1093
+ if (this.l2Redis && await this.l2Redis.exists(key)) {
1094
+ return { layer: index_js_1.StorageLayer.REDIS };
1095
+ }
1096
+ if (this.l3Postgres && await this.l3Postgres.exists(key)) {
1097
+ return { layer: index_js_1.StorageLayer.POSTGRES };
1098
+ }
1099
+ return null;
1100
+ }
1101
+ // Private helper methods
1102
+ // Memory cleanup
1103
+ startMemoryCleanup() {
1104
+ const memoryTTL = this.config.memory?.ttl || 24 * 60 * 60 * 1000; // 24 hours default
1105
+ const cleanupInterval = memoryTTL / 24; // Clean up 24 times per TTL period
1106
+ this.cleanupInterval = setInterval(async () => {
1107
+ try {
1108
+ const cleaned = await this.l1Memory.cleanup();
1109
+ if (cleaned > 0) {
1110
+ this.log('debug', `Memory L1 cleanup completed - removed ${cleaned} expired entries`);
1111
+ }
1112
+ }
1113
+ catch (error) {
1114
+ this.log('error', 'Memory cleanup error', {
1115
+ error: error instanceof Error ? error.message : String(error)
1116
+ });
1117
+ }
1118
+ }, cleanupInterval);
1119
+ }
1120
+ // Utility methods
1121
+ updateResponseTime(duration) {
1122
+ // Exponential moving average with smoothing factor alpha
1123
+ const alpha = 0.2; // Smoothing factor (adjustable)
1124
+ this.metrics.averageResponseTime =
1125
+ alpha * duration + (1 - alpha) * this.metrics.averageResponseTime;
1126
+ }
1127
+ calculateCacheHitRatio(layers) {
1128
+ let totalHits = 0;
1129
+ let totalMisses = 0;
1130
+ // If no layers specified, calculate for all layers
1131
+ if (!layers) {
1132
+ totalHits = this.metrics.memoryHits + this.metrics.redisHits + this.metrics.postgresHits;
1133
+ totalMisses = this.metrics.memoryMisses + this.metrics.redisMisses + this.metrics.postgresMisses;
1134
+ }
1135
+ else {
1136
+ // Calculate only for specified layers
1137
+ for (const layer of layers) {
1138
+ switch (layer) {
1139
+ case 'memory':
1140
+ totalHits += this.metrics.memoryHits;
1141
+ totalMisses += this.metrics.memoryMisses;
1142
+ break;
1143
+ case 'redis':
1144
+ totalHits += this.metrics.redisHits;
1145
+ totalMisses += this.metrics.redisMisses;
1146
+ break;
1147
+ case 'postgres':
1148
+ totalHits += this.metrics.postgresHits;
1149
+ totalMisses += this.metrics.postgresMisses;
1150
+ break;
1151
+ }
1152
+ }
1153
+ }
1154
+ return totalHits + totalMisses > 0 ? totalHits / (totalHits + totalMisses) : 0;
1155
+ }
1156
+ applySorting(items, options) {
1157
+ if (!options.sortBy)
1158
+ return items;
1159
+ return items.sort((a, b) => {
1160
+ let aVal, bVal;
1161
+ switch (options.sortBy) {
1162
+ case 'key':
1163
+ aVal = a.key;
1164
+ bVal = b.key;
1165
+ break;
1166
+ case 'createdAt':
1167
+ aVal = a.metadata.createdAt;
1168
+ bVal = b.metadata.createdAt;
1169
+ break;
1170
+ default:
1171
+ return 0;
1172
+ }
1173
+ const order = options.sortOrder === 'desc' ? -1 : 1;
1174
+ return aVal < bVal ? -order : aVal > bVal ? order : 0;
1175
+ });
1176
+ }
1177
+ applyPagination(items, options) {
1178
+ const offset = options.offset || 0;
1179
+ const limit = options.limit;
1180
+ if (limit) {
1181
+ return items.slice(offset, offset + limit);
1182
+ }
1183
+ return items.slice(offset);
1184
+ }
1185
+ // Legacy compatibility methods (for migration)
1186
+ getStats() {
1187
+ return {
1188
+ memoryKeys: this.l1Memory.size(),
1189
+ connected: this.connected,
1190
+ layers: {
1191
+ memory: true,
1192
+ redis: this.l2Redis ? this.l2Redis.isConnected() : false,
1193
+ postgres: this.l3Postgres ? this.l3Postgres.isConnected() : false
1194
+ }
1195
+ };
1196
+ }
1197
+ async cleanupExpiredMemory() {
1198
+ return await this.l1Memory.cleanup();
1199
+ }
1200
+ }
1201
+ exports.StorageEngine = StorageEngine;
1202
+ //# sourceMappingURL=engine.js.map