@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,981 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NuvexClient = void 0;
37
+ /**
38
+ * Nuvex - Client Implementation
39
+ * Next-gen Unified Vault Experience
40
+ *
41
+ * High-level client operations for any Node.js application using the StorageEngine
42
+ * multi-layer architecture. Provides application-centric methods for storing and
43
+ * retrieving data with built-in health checks, metrics, and maintenance operations.
44
+ *
45
+ * Core Features:
46
+ * - Generic key-value operations with intelligent caching
47
+ * - Health monitoring and diagnostics
48
+ * - Automatic cleanup and maintenance
49
+ * - Configuration management
50
+ * - Backup and restore capabilities
51
+ *
52
+ * @author Waren Gonzaga, WG Technology Labs
53
+ * @since 2025
54
+ */
55
+ const engine_js_1 = require("./engine.js");
56
+ /**
57
+ * Nuvex Client - High-level storage operations
58
+ *
59
+ * Provides a high-level interface for interacting with the multi-layer storage
60
+ * architecture. Implements the Store interface with additional convenience methods,
61
+ * health monitoring, backup/restore capabilities, and singleton pattern support.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // Initialize as singleton
66
+ * const client = await NuvexClient.initialize({
67
+ * postgres: { host: 'localhost', port: 5432, database: 'myapp' },
68
+ * redis: { url: 'redis://localhost:6379' },
69
+ * memory: { ttl: 3600000, maxSize: 10000 }
70
+ * });
71
+ *
72
+ * // Store and retrieve data
73
+ * await client.set('user:123', { name: 'John', email: 'john@example.com' });
74
+ * const user = await client.get('user:123');
75
+ *
76
+ * // Use namespacing
77
+ * await client.setNamespaced('users', '123', userData);
78
+ * const userData = await client.getNamespaced('users', '123');
79
+ *
80
+ * // Perform health checks
81
+ * const health = await client.healthCheck();
82
+ * console.log('Storage layers healthy:', health.overall);
83
+ * ```
84
+ *
85
+ * @class NuvexClient
86
+ * @implements {IStore}
87
+ * @author Waren Gonzaga, WG Technology Labs
88
+ * @since 2025
89
+ */
90
+ class NuvexClient {
91
+ constructor(config) {
92
+ this.config = config;
93
+ this.logger = config.logging?.enabled ? (config.logging.logger || null) : null;
94
+ this.storage = new engine_js_1.StorageEngine(config);
95
+ }
96
+ log(level, message, meta) {
97
+ if (this.logger) {
98
+ this.logger[level](message, meta);
99
+ }
100
+ }
101
+ /**
102
+ * Initialize the Store singleton instance
103
+ *
104
+ * Creates a new NuvexClient instance if one doesn't exist and connects to all
105
+ * configured storage layers. This method ensures only one instance exists
106
+ * throughout the application lifecycle.
107
+ *
108
+ * @param config - Configuration object for all storage layers
109
+ * @returns Promise that resolves to the initialized NuvexClient instance
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * const client = await NuvexClient.initialize({
114
+ * postgres: { host: 'localhost', port: 5432, database: 'myapp' },
115
+ * redis: { url: 'redis://localhost:6379' },
116
+ * memory: { ttl: 3600000, maxSize: 10000 }
117
+ * });
118
+ * ```
119
+ *
120
+ * @since 1.0.0
121
+ */
122
+ static async initialize(config) {
123
+ if (!NuvexClient.instance) {
124
+ NuvexClient.instance = new NuvexClient(config);
125
+ await NuvexClient.instance.storage.connect();
126
+ }
127
+ return NuvexClient.instance;
128
+ }
129
+ /**
130
+ * Get the singleton instance
131
+ *
132
+ * Returns the existing NuvexClient instance. Must be called after initialize().
133
+ *
134
+ * @returns The singleton NuvexClient instance
135
+ * @throws {Error} If the store has not been initialized
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * // After initialization
140
+ * const client = NuvexClient.getInstance();
141
+ * await client.set('key', 'value');
142
+ * ```
143
+ *
144
+ * @since 1.0.0
145
+ */
146
+ static getInstance() {
147
+ if (!NuvexClient.instance) {
148
+ throw new Error('Store not initialized. Call NuvexClient.initialize() first.');
149
+ }
150
+ return NuvexClient.instance;
151
+ }
152
+ /**
153
+ * Create a new Store instance (non-singleton)
154
+ *
155
+ * Creates a new NuvexClient instance without affecting the singleton.
156
+ * Useful for testing or when multiple isolated instances are needed.
157
+ *
158
+ * @param config - Configuration object for all storage layers
159
+ * @returns Promise that resolves to a new NuvexClient instance
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const testClient = await NuvexClient.create({
164
+ * postgres: testDbConfig,
165
+ * memory: { ttl: 1000 }
166
+ * });
167
+ * ```
168
+ *
169
+ * @since 1.0.0
170
+ */
171
+ static async create(config) {
172
+ const store = new NuvexClient(config);
173
+ await store.storage.connect();
174
+ return store;
175
+ }
176
+ // Connection management
177
+ /**
178
+ * Connect to all configured storage layers
179
+ *
180
+ * Establishes connections to PostgreSQL, Redis (if configured), and initializes
181
+ * the memory cache. This method is automatically called by initialize() and create().
182
+ *
183
+ * @returns Promise that resolves when all connections are established
184
+ * @throws {Error} If any required storage layer fails to connect
185
+ *
186
+ * @since 1.0.0
187
+ */
188
+ async connect() {
189
+ await this.storage.connect();
190
+ this.log('info', 'Nuvex Client connected');
191
+ }
192
+ /**
193
+ * Disconnect from all storage layers
194
+ *
195
+ * Cleanly closes all connections and clears the memory cache.
196
+ * Should be called during application shutdown.
197
+ *
198
+ * @returns Promise that resolves when all connections are closed
199
+ *
200
+ * @since 1.0.0
201
+ */
202
+ async disconnect() {
203
+ await this.storage.disconnect();
204
+ this.log('info', 'Nuvex Client disconnected');
205
+ }
206
+ /**
207
+ * Check if the client is connected to storage layers
208
+ *
209
+ * @returns True if connected to at least the primary storage layer
210
+ *
211
+ * @since 1.0.0
212
+ */
213
+ isConnected() {
214
+ return this.storage.isConnected();
215
+ }
216
+ // Basic operations (delegated to UnifiedStorage)
217
+ /**
218
+ * Store a value in the multi-layer storage system
219
+ *
220
+ * Stores the value across all available storage layers (Memory → Redis → PostgreSQL)
221
+ * with intelligent TTL management and layer-specific optimizations.
222
+ *
223
+ * @template T - The type of the value being stored
224
+ * @param key - Unique identifier for the stored value
225
+ * @param value - The value to store (will be JSON serialized)
226
+ * @param options - Optional storage configuration
227
+ * @returns Promise that resolves to true if stored successfully
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * // Store with default TTL
232
+ * await client.set('user:123', { name: 'John', email: 'john@example.com' });
233
+ *
234
+ * // Store with custom TTL (60 seconds)
235
+ * await client.set('session:abc', sessionData, { ttl: 60 });
236
+ *
237
+ * // Store only in memory layer
238
+ * await client.set('cache:temp', data, { layer: StorageLayer.MEMORY });
239
+ * ```
240
+ *
241
+ * @since 1.0.0
242
+ */
243
+ async set(key, value, options) {
244
+ return this.storage.set(key, value, options);
245
+ }
246
+ /**
247
+ * Retrieve a value from the multi-layer storage system
248
+ *
249
+ * Searches for the value across storage layers in order (Memory → Redis → PostgreSQL)
250
+ * and automatically promotes the value to higher layers for faster future access.
251
+ *
252
+ * @template T - The expected type of the retrieved value
253
+ * @param key - Unique identifier of the value to retrieve
254
+ * @param options - Optional retrieval configuration
255
+ * @returns Promise that resolves to the value or null if not found
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * // Get from any layer
260
+ * const user = await client.get<UserType>('user:123');
261
+ *
262
+ * // Get only from PostgreSQL, skip cache
263
+ * const freshData = await client.get('data:key', { skipCache: true });
264
+ *
265
+ * // Get only from memory layer
266
+ * const cachedData = await client.get('cache:key', { layer: StorageLayer.MEMORY });
267
+ * ```
268
+ *
269
+ * @since 1.0.0
270
+ */
271
+ async get(key, options = {}) {
272
+ return this.storage.get(key, options);
273
+ }
274
+ /**
275
+ * Delete a value from all storage layers
276
+ *
277
+ * Removes the value from all storage layers to ensure consistency.
278
+ *
279
+ * @param key - Unique identifier of the value to delete
280
+ * @param options - Optional deletion configuration
281
+ * @returns Promise that resolves to true if deleted successfully
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * // Delete from all layers
286
+ * await client.delete('user:123');
287
+ *
288
+ * // Delete only from memory layer
289
+ * await client.delete('cache:temp', { layer: StorageLayer.MEMORY });
290
+ * ```
291
+ *
292
+ * @since 1.0.0
293
+ */
294
+ async delete(key, options = {}) {
295
+ return this.storage.delete(key, options);
296
+ }
297
+ /**
298
+ * Check if a key exists in any storage layer
299
+ *
300
+ * @param key - Unique identifier to check for existence
301
+ * @param options - Optional configuration to check specific layer
302
+ * @returns Promise that resolves to true if the key exists
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * if (await client.exists('user:123')) {
307
+ * console.log('User exists');
308
+ * }
309
+ * ```
310
+ *
311
+ * @since 1.0.0
312
+ */
313
+ async exists(key, options = {}) {
314
+ return this.storage.exists(key, options);
315
+ }
316
+ /**
317
+ * Set or update the expiration time for a key
318
+ *
319
+ * @param key - Unique identifier of the value
320
+ * @param ttl - Time to live in seconds
321
+ * @returns Promise that resolves to true if expiration was set successfully
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * // Expire in 1 hour
326
+ * await client.expire('session:abc', 3600);
327
+ * ```
328
+ *
329
+ * @since 1.0.0
330
+ */
331
+ async expire(key, ttl) {
332
+ return this.storage.expire(key, ttl);
333
+ }
334
+ // Batch operations
335
+ /**
336
+ * Execute multiple set operations in a batch
337
+ *
338
+ * Efficiently executes multiple storage operations with automatic error handling
339
+ * and transaction-like behavior where possible.
340
+ *
341
+ * @param operations - Array of batch operations to execute
342
+ * @returns Promise that resolves to an array of results for each operation
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const results = await client.setBatch([
347
+ * { operation: 'set', key: 'user:1', value: userData1 },
348
+ * { operation: 'set', key: 'user:2', value: userData2, options: { ttl: 3600 } }
349
+ * ]);
350
+ *
351
+ * results.forEach((result, index) => {
352
+ * console.log(`Operation ${index}: ${result.success ? 'Success' : 'Failed'}`);
353
+ * });
354
+ * ```
355
+ *
356
+ * @since 1.0.0
357
+ */
358
+ async setBatch(operations) {
359
+ return this.storage.setBatch(operations);
360
+ }
361
+ /**
362
+ * Retrieve multiple values in a batch
363
+ *
364
+ * Efficiently retrieves multiple values with layer optimization and
365
+ * automatic cache promotion.
366
+ *
367
+ * @param keys - Array of keys to retrieve
368
+ * @param options - Optional configuration applied to all operations
369
+ * @returns Promise that resolves to an array of results for each key
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * const results = await client.getBatch(['user:1', 'user:2', 'user:3']);
374
+ * const users = results
375
+ * .filter(result => result.success && result.value)
376
+ * .map(result => result.value);
377
+ * ```
378
+ *
379
+ * @since 1.0.0
380
+ */
381
+ async getBatch(keys, options = {}) {
382
+ return this.storage.getBatch(keys, options);
383
+ }
384
+ /**
385
+ * Delete multiple values in a batch
386
+ *
387
+ * Efficiently deletes multiple values from all storage layers.
388
+ *
389
+ * @param keys - Array of keys to delete
390
+ * @returns Promise that resolves to an array of results for each key
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * const results = await client.deleteBatch(['temp:1', 'temp:2', 'temp:3']);
395
+ * const deletedCount = results.filter(r => r.success).length;
396
+ * ```
397
+ *
398
+ * @since 1.0.0
399
+ */
400
+ async deleteBatch(keys) {
401
+ return this.storage.deleteBatch(keys);
402
+ }
403
+ // Query operations
404
+ async query(options) {
405
+ return this.storage.query(options);
406
+ }
407
+ async keys(pattern) {
408
+ return this.storage.keys(pattern);
409
+ }
410
+ async clear(pattern) {
411
+ this.log('warn', `Clearing storage${pattern ? ` with pattern: ${pattern}` : ' (all keys)'}`);
412
+ return this.storage.clear(pattern);
413
+ }
414
+ // Metrics and monitoring
415
+ /**
416
+ * Get performance metrics for all layers or specific layer(s)
417
+ *
418
+ * Returns metrics about storage operations and performance. Can be filtered
419
+ * to return metrics for specific layers only.
420
+ *
421
+ * @param layers - Optional layer(s) to get metrics for. If not provided, returns all metrics.
422
+ * Can be a single layer string, 'all', or array of layer strings.
423
+ * @returns Object containing requested metrics
424
+ *
425
+ * @example
426
+ * ```typescript
427
+ * // Get all metrics
428
+ * const metrics = client.getMetrics();
429
+ *
430
+ * // Get specific layer metrics
431
+ * const memoryMetrics = client.getMetrics('memory');
432
+ * // { memoryHits, memoryMisses, memorySize, memoryMaxSize }
433
+ *
434
+ * // Get multiple layer metrics
435
+ * const cacheMetrics = client.getMetrics(['memory', 'redis']);
436
+ * // { memoryHits, memoryMisses, memorySize, memoryMaxSize, redisHits, redisMisses, totalOperations, averageResponseTime, cacheHitRatio }
437
+ * ```
438
+ *
439
+ * @since 1.0.0
440
+ */
441
+ getMetrics(layers) {
442
+ return this.storage.getMetrics(layers);
443
+ }
444
+ resetMetrics() {
445
+ this.storage.resetMetrics();
446
+ this.log('info', 'Storage metrics reset');
447
+ }
448
+ // Layer management
449
+ async promote(key, targetLayer) {
450
+ return this.storage.promote(key, targetLayer);
451
+ }
452
+ async demote(key, targetLayer) {
453
+ return this.storage.demote(key, targetLayer);
454
+ }
455
+ async getLayerInfo(key) {
456
+ return this.storage.getLayerInfo(key);
457
+ }
458
+ // Store-specific methods
459
+ /**
460
+ * Configure the store with new settings
461
+ */
462
+ async configure(config) {
463
+ // Merge with existing config
464
+ this.config = { ...this.config, ...config };
465
+ // Update logger if changed
466
+ if (config.logging) {
467
+ this.logger = config.logging.enabled ? (config.logging.logger || null) : null;
468
+ }
469
+ this.log('info', 'Client configuration updated');
470
+ }
471
+ /**
472
+ * Get the underlying storage engine
473
+ *
474
+ * @internal This method is intended for internal and testing use only.
475
+ * It provides direct access to the StorageEngine instance.
476
+ *
477
+ * @returns The StorageEngine instance used by this client
478
+ */
479
+ getEngine() {
480
+ return this.storage;
481
+ }
482
+ /**
483
+ * Get current configuration
484
+ */
485
+ getConfig() {
486
+ return { ...this.config };
487
+ }
488
+ /**
489
+ * Health check for all storage layers or specific layer(s)
490
+ *
491
+ * Performs comprehensive health checks on configured storage layers
492
+ * using the underlying engine's ping() methods for each layer.
493
+ *
494
+ * @param layers - Optional layer(s) to check. If not provided, checks all layers.
495
+ * Can be a single layer string or array of layer strings.
496
+ * @returns Promise that resolves to health status for requested layer(s)
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * // Check all layers
501
+ * const health = await client.healthCheck();
502
+ * // { memory: true, redis: true, postgres: true }
503
+ *
504
+ * // Check specific layer
505
+ * const redisHealth = await client.healthCheck('redis');
506
+ * // { redis: true }
507
+ *
508
+ * // Check multiple layers
509
+ * const cacheHealth = await client.healthCheck(['memory', 'redis']);
510
+ * // { memory: true, redis: true }
511
+ *
512
+ * if (!health.redis) {
513
+ * console.error('Redis layer is down');
514
+ * }
515
+ * ```
516
+ *
517
+ * @since 1.0.0
518
+ */
519
+ async healthCheck(layers) {
520
+ return this.storage.healthCheck(layers);
521
+ }
522
+ /**
523
+ * Cleanup expired entries and optimize storage
524
+ */
525
+ async cleanup() {
526
+ this.log('info', 'Starting storage cleanup');
527
+ let cleaned = 0;
528
+ let errors = 0;
529
+ try {
530
+ // Clean up expired memory entries
531
+ const memoryCleanup = await this.storage.cleanupExpiredMemory();
532
+ cleaned += memoryCleanup;
533
+ this.log('info', `Cleanup completed: ${cleaned} entries cleaned, ${errors} errors`);
534
+ return { cleaned, errors };
535
+ }
536
+ catch (error) {
537
+ errors++;
538
+ this.log('error', 'Cleanup failed', { error: error.message });
539
+ return { cleaned, errors };
540
+ }
541
+ }
542
+ /**
543
+ * Compact storage and optimize performance
544
+ */
545
+ async compact() {
546
+ this.log('info', 'Starting storage compaction');
547
+ try {
548
+ // For now, compaction means cleanup + metrics reset
549
+ await this.cleanup();
550
+ this.resetMetrics();
551
+ this.log('info', 'Storage compaction completed');
552
+ }
553
+ catch (error) {
554
+ this.log('error', 'Storage compaction failed', { error: error.message });
555
+ throw error;
556
+ }
557
+ }
558
+ /**
559
+ * Backup storage data to external location with incremental support
560
+ */
561
+ async backup(destination, options) {
562
+ this.log('info', 'Starting storage backup');
563
+ try {
564
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
565
+ const backupId = destination || `nuvex-backup-${timestamp}`;
566
+ const { incremental = false, compression = true } = options || {}; // Get last backup timestamp for incremental backups
567
+ let lastBackupTime = null;
568
+ if (incremental) {
569
+ try {
570
+ const lastBackupMetadata = await this.storage.get('__nuvex_last_backup_metadata', {});
571
+ if (lastBackupMetadata && typeof lastBackupMetadata === 'object' && 'timestamp' in lastBackupMetadata) {
572
+ lastBackupTime = new Date(lastBackupMetadata.timestamp);
573
+ }
574
+ }
575
+ catch (error) {
576
+ this.log('warn', 'Could not retrieve last backup metadata, performing full backup', { error: error.message });
577
+ }
578
+ }
579
+ // Get all keys
580
+ const allKeys = await this.storage.keys();
581
+ const backupData = {};
582
+ let keysProcessed = 0;
583
+ let keysSkipped = 0;
584
+ // Backup data with metadata and TTL information
585
+ for (const key of allKeys) {
586
+ // Skip internal backup metadata keys
587
+ if (key.startsWith('__nuvex_') || key.startsWith('__backup:')) {
588
+ keysSkipped++;
589
+ continue;
590
+ }
591
+ const value = await this.storage.get(key, {});
592
+ const layerInfo = await this.storage.getLayerInfo(key);
593
+ if (value !== null) {
594
+ const itemData = {
595
+ value,
596
+ layerInfo,
597
+ createdAt: new Date().toISOString(),
598
+ version: '1.0.0'
599
+ };
600
+ // For incremental backups, include only changed data
601
+ if (incremental && lastBackupTime) {
602
+ // This is a simplified approach - in production, you'd track modification times
603
+ // For now, we'll include all data but mark it as incremental
604
+ const enhancedItemData = itemData;
605
+ enhancedItemData.backupType = 'incremental';
606
+ enhancedItemData.lastBackupTime = lastBackupTime.toISOString();
607
+ }
608
+ backupData[key] = itemData;
609
+ keysProcessed++;
610
+ }
611
+ }
612
+ // Create backup metadata
613
+ const backupMetadata = {
614
+ id: backupId,
615
+ createdAt: new Date().toISOString(),
616
+ keyCount: keysProcessed,
617
+ keysSkipped,
618
+ version: '1.0.0',
619
+ type: incremental ? 'incremental' : 'full',
620
+ lastBackupTime: lastBackupTime?.toISOString() || null,
621
+ compression,
622
+ totalKeys: allKeys.length
623
+ };
624
+ // Create complete backup package
625
+ const backupPackage = {
626
+ metadata: backupMetadata,
627
+ data: backupData
628
+ };
629
+ // Export to external storage (filesystem example)
630
+ const fs = await Promise.resolve().then(() => __importStar(require('fs'))).catch(() => null);
631
+ const path = await Promise.resolve().then(() => __importStar(require('path'))).catch(() => null);
632
+ if (fs && path) {
633
+ const backupDir = path.join(process.cwd(), 'nuvex-backups');
634
+ // Ensure backup directory exists
635
+ await fs.promises.mkdir(backupDir, { recursive: true });
636
+ const backupFilePath = path.join(backupDir, `${backupId}.json`);
637
+ const dataToWrite = JSON.stringify(backupPackage, null, 2);
638
+ // Apply compression if requested
639
+ if (compression) {
640
+ try {
641
+ const zlib = await Promise.resolve().then(() => __importStar(require('zlib')));
642
+ const compressed = zlib.gzipSync(Buffer.from(dataToWrite));
643
+ await fs.promises.writeFile(`${backupFilePath}.gz`, compressed);
644
+ this.log('info', `Backup compressed and saved to ${backupFilePath}.gz`);
645
+ }
646
+ catch (compressionError) {
647
+ this.log('warn', 'Compression failed, saving uncompressed', { error: compressionError.message });
648
+ await fs.promises.writeFile(backupFilePath, dataToWrite);
649
+ }
650
+ }
651
+ else {
652
+ await fs.promises.writeFile(backupFilePath, dataToWrite);
653
+ }
654
+ // Update last backup metadata for incremental backups
655
+ await this.storage.set('__nuvex_last_backup_metadata', {
656
+ backupId,
657
+ timestamp: new Date().toISOString(),
658
+ type: incremental ? 'incremental' : 'full'
659
+ }, {});
660
+ this.log('info', `Backup completed: ${backupId}`, {
661
+ keyCount: keysProcessed,
662
+ keysSkipped,
663
+ type: incremental ? 'incremental' : 'full',
664
+ compressed: compression
665
+ });
666
+ return backupId;
667
+ }
668
+ else {
669
+ // Fallback: store in memory/internal storage with warning
670
+ this.log('warn', 'File system not available, storing backup metadata internally (not recommended for production)');
671
+ await this.storage.set(`__backup:${backupId}`, backupMetadata, {});
672
+ return backupId;
673
+ }
674
+ }
675
+ catch (error) {
676
+ this.log('error', 'Backup failed', { error: error.message });
677
+ throw error;
678
+ }
679
+ }
680
+ /**
681
+ * Restore storage data from external backup location
682
+ */
683
+ async restore(source, options) {
684
+ this.log('info', `Starting restore from backup: ${source}`);
685
+ try {
686
+ const { clearExisting = false, dryRun = false } = options || {};
687
+ // Try to load backup from external storage first
688
+ const fs = await Promise.resolve().then(() => __importStar(require('fs'))).catch(() => null);
689
+ const path = await Promise.resolve().then(() => __importStar(require('path'))).catch(() => null);
690
+ let backupPackage = null;
691
+ if (fs && path) {
692
+ const backupDir = path.join(process.cwd(), 'nuvex-backups');
693
+ const backupFilePath = path.join(backupDir, `${source}.json`);
694
+ const compressedFilePath = `${backupFilePath}.gz`;
695
+ // Try compressed file first
696
+ const compressedExists = await fs.promises.access(compressedFilePath).then(() => true).catch(() => false);
697
+ if (compressedExists) {
698
+ try {
699
+ const zlib = await Promise.resolve().then(() => __importStar(require('zlib')));
700
+ const compressedData = await fs.promises.readFile(compressedFilePath);
701
+ const decompressed = zlib.gunzipSync(compressedData);
702
+ backupPackage = JSON.parse(decompressed.toString());
703
+ this.log('info', 'Loaded compressed backup file');
704
+ }
705
+ catch (decompressionError) {
706
+ this.log('error', 'Failed to decompress backup file', { error: decompressionError.message });
707
+ throw decompressionError;
708
+ }
709
+ }
710
+ else {
711
+ const uncompressedExists = await fs.promises.access(backupFilePath).then(() => true).catch(() => false);
712
+ if (uncompressedExists) {
713
+ const backupData = await fs.promises.readFile(backupFilePath, 'utf8');
714
+ backupPackage = JSON.parse(backupData);
715
+ this.log('info', 'Loaded uncompressed backup file');
716
+ }
717
+ }
718
+ }
719
+ // Fallback: try to load from internal storage
720
+ if (!backupPackage) {
721
+ this.log('warn', 'External backup not found, checking internal storage');
722
+ const internalBackupMetadata = await this.storage.get(`__backup:${source}`, {});
723
+ if (!internalBackupMetadata) {
724
+ throw new Error(`Backup not found: ${source}`);
725
+ }
726
+ // For internal backups, we don't have the actual data, just metadata
727
+ this.log('warn', 'Internal backup found but data restoration from internal storage is limited');
728
+ return false;
729
+ }
730
+ const { metadata, data } = backupPackage;
731
+ if (!metadata || !data) {
732
+ throw new Error('Invalid backup format: missing metadata or data');
733
+ }
734
+ this.log('info', 'Backup metadata loaded', {
735
+ id: metadata.id,
736
+ createdAt: metadata.createdAt,
737
+ keyCount: metadata.keyCount,
738
+ type: metadata.type,
739
+ version: metadata.version
740
+ });
741
+ if (dryRun) {
742
+ this.log('info', 'Dry run mode: would restore the following keys', {
743
+ keys: Object.keys(data),
744
+ count: Object.keys(data).length
745
+ });
746
+ return true;
747
+ }
748
+ // Clear existing data if requested
749
+ if (clearExisting) {
750
+ this.log('warn', 'Clearing existing storage before restore');
751
+ await this.storage.clear();
752
+ }
753
+ // Restore data with proper metadata and TTL
754
+ let restoredCount = 0;
755
+ let errorCount = 0;
756
+ for (const [key, itemData] of Object.entries(data)) {
757
+ try {
758
+ const item = itemData;
759
+ const { value, layerInfo } = item;
760
+ // Restore to the original layer if possible
761
+ const restoreOptions = {};
762
+ if (layerInfo?.layer) {
763
+ restoreOptions.layer = layerInfo.layer;
764
+ }
765
+ // Restore TTL if available
766
+ if (layerInfo?.ttl && layerInfo.ttl > 0) {
767
+ restoreOptions.ttl = layerInfo.ttl;
768
+ }
769
+ const success = await this.storage.set(key, value, restoreOptions);
770
+ if (success) {
771
+ restoredCount++;
772
+ }
773
+ else {
774
+ errorCount++;
775
+ this.log('warn', `Failed to restore key: ${key}`);
776
+ }
777
+ }
778
+ catch (keyError) {
779
+ errorCount++;
780
+ this.log('error', `Error restoring key: ${key}`, { error: keyError.message });
781
+ }
782
+ }
783
+ // Update restoration metadata
784
+ await this.storage.set('__nuvex_last_restore_metadata', {
785
+ backupId: metadata.id,
786
+ restoredAt: new Date().toISOString(),
787
+ restoredCount,
788
+ errorCount,
789
+ totalKeys: metadata.keyCount
790
+ }, {});
791
+ this.log('info', `Restore completed from backup: ${source}`, {
792
+ restoredCount,
793
+ errorCount,
794
+ totalKeys: metadata.keyCount,
795
+ successRate: `${((restoredCount / metadata.keyCount) * 100).toFixed(2)}%`
796
+ });
797
+ return errorCount === 0;
798
+ }
799
+ catch (error) {
800
+ this.log('error', 'Restore failed', { error: error.message });
801
+ return false;
802
+ }
803
+ }
804
+ // Convenience methods for common patterns
805
+ /**
806
+ * Namespace-aware set operation
807
+ */
808
+ async setNamespaced(namespace, key, value, options = {}) {
809
+ return this.storage.set(`${namespace}:${key}`, value, options);
810
+ }
811
+ /**
812
+ * Namespace-aware get operation
813
+ */
814
+ async getNamespaced(namespace, key, options = {}) {
815
+ return this.storage.get(`${namespace}:${key}`, options);
816
+ }
817
+ /**
818
+ * Get all keys in a namespace
819
+ */
820
+ async getNamespaceKeys(namespace) {
821
+ const allKeys = await this.keys(`${namespace}:*`);
822
+ return allKeys.map(key => key.replace(`${namespace}:`, ''));
823
+ }
824
+ /**
825
+ * Clear entire namespace
826
+ */
827
+ async clearNamespace(namespace) {
828
+ return this.clear(`${namespace}:*`);
829
+ }
830
+ /**
831
+ * Atomically increment a numeric value
832
+ *
833
+ * This method provides true atomic increments that are safe for concurrent access
834
+ * across all storage layers. Uses native atomic operations from Redis (INCRBY) and
835
+ * PostgreSQL (UPDATE with row locks) when available.
836
+ *
837
+ * **Thread-Safety:**
838
+ * - ✅ Safe for concurrent increments to the same key
839
+ * - ✅ No lost updates in high-concurrency scenarios
840
+ * - ✅ Works correctly across multiple instances
841
+ *
842
+ * **Important:** The key must contain a numeric value (or not exist).
843
+ * Incrementing a non-numeric value will throw an error.
844
+ *
845
+ * **How It Works:**
846
+ * 1. Uses atomic increment at the authoritative layer (PostgreSQL or Redis)
847
+ * 2. Propagates the new value to cache layers for consistency
848
+ * 3. Returns the exact new value after increment
849
+ *
850
+ * **Example Usage:**
851
+ * ```typescript
852
+ * // ✅ SAFE: Concurrent increments work correctly
853
+ * await Promise.all([
854
+ * client.increment('counter'), // atomic: 5 → 6
855
+ * client.increment('counter') // atomic: 6 → 7
856
+ * ]);
857
+ * // Result: 7 (all increments counted correctly)
858
+ *
859
+ * // Custom delta
860
+ * await client.increment('page_views', 5);
861
+ *
862
+ * // With TTL (in milliseconds)
863
+ * await client.increment('rate_limit', 1, 3600000);
864
+ * ```
865
+ *
866
+ * **Use Cases:**
867
+ * - ✅ High-concurrency counters (page views, API calls)
868
+ * - ✅ Critical operations (user credits, inventory)
869
+ * - ✅ Financial operations requiring exactness
870
+ * - ✅ Distributed systems with multiple instances
871
+ *
872
+ * @param key - The key to increment (must contain numeric value or not exist)
873
+ * @param delta - The amount to increment by (default: 1)
874
+ * @param ttl - Optional TTL in milliseconds
875
+ * @returns Promise resolving to the new value after increment
876
+ * @throws {Error} If the key contains a non-numeric value
877
+ */
878
+ async increment(key, delta = 1, ttl) {
879
+ return this.storage.increment(key, delta, ttl);
880
+ }
881
+ /**
882
+ * Atomically decrement a numeric value
883
+ *
884
+ * This is a convenience method that uses atomic increment with a negative delta.
885
+ * Provides the same thread-safety guarantees as increment().
886
+ *
887
+ * @param key - The key to decrement
888
+ * @param delta - The amount to decrement by (default: 1)
889
+ * @param ttl - Optional TTL in milliseconds
890
+ * @returns Promise resolving to the new value after decrement
891
+ *
892
+ * @example
893
+ * ```typescript
894
+ * // Decrement by 1
895
+ * await client.decrement('inventory');
896
+ *
897
+ * // Decrement by custom amount
898
+ * await client.decrement('stock', 5);
899
+ * ```
900
+ */
901
+ async decrement(key, delta = 1, ttl) {
902
+ return this.increment(key, -delta, ttl);
903
+ }
904
+ /**
905
+ * Set if not exists
906
+ *
907
+ * Stores a value only if the key does not already exist. Useful for
908
+ * initializing values that should not be overwritten.
909
+ *
910
+ * @note This implementation uses a check-then-set pattern which has a
911
+ * TOCTOU (Time-of-Check to Time-of-Use) race condition. Between the
912
+ * `exists()` and `set()` calls, another client could write to the same key.
913
+ * For critical use cases requiring true atomic set-if-not-exists semantics,
914
+ * use Redis `SET key value NX` or PostgreSQL `INSERT ... ON CONFLICT DO NOTHING`
915
+ * directly via the storage layer.
916
+ *
917
+ * @template T - The type of the value being stored
918
+ * @param key - Unique identifier for the stored value
919
+ * @param value - The value to store if key doesn't exist
920
+ * @param options - Optional storage configuration
921
+ * @returns Promise resolving to true if value was set, false if key already existed
922
+ *
923
+ * @example
924
+ * ```typescript
925
+ * // Initialize a counter only if it doesn't exist
926
+ * await client.setIfNotExists('counter', 0);
927
+ * ```
928
+ */
929
+ async setIfNotExists(key, value, options) {
930
+ const exists = await this.storage.exists(key, options);
931
+ if (!exists) {
932
+ return this.storage.set(key, value, options);
933
+ }
934
+ return false;
935
+ }
936
+ /**
937
+ * Get multiple keys with a common prefix
938
+ */
939
+ async getByPrefix(prefix, options = {}) {
940
+ const keys = await this.keys(`${prefix}*`);
941
+ const result = {};
942
+ for (const key of keys) {
943
+ const value = await this.storage.get(key, options);
944
+ if (value !== null) {
945
+ result[key] = value;
946
+ }
947
+ }
948
+ return result;
949
+ }
950
+ // Static convenience methods
951
+ static async set(key, value, options) {
952
+ return NuvexClient.getInstance().set(key, value, options);
953
+ }
954
+ static async get(key, options = {}) {
955
+ return NuvexClient.getInstance().get(key, options);
956
+ }
957
+ static async delete(key, options = {}) {
958
+ return NuvexClient.getInstance().delete(key, options);
959
+ }
960
+ static async exists(key, options = {}) {
961
+ return NuvexClient.getInstance().exists(key, options);
962
+ }
963
+ static async healthCheck(layers) {
964
+ return NuvexClient.getInstance().healthCheck(layers);
965
+ }
966
+ static getMetrics(layers) {
967
+ return NuvexClient.getInstance().getMetrics(layers);
968
+ }
969
+ /**
970
+ * Shutdown the store and cleanup resources
971
+ */
972
+ static async shutdown() {
973
+ if (NuvexClient.instance) {
974
+ await NuvexClient.instance.disconnect();
975
+ NuvexClient.instance = null;
976
+ }
977
+ }
978
+ }
979
+ exports.NuvexClient = NuvexClient;
980
+ NuvexClient.instance = null;
981
+ //# sourceMappingURL=client.js.map