UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

570 lines 21.3 kB
"use strict"; /** * Redis storage provider implementation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.createRedisProvider = exports.RedisStorageProvider = void 0; const types_1 = require("../../types"); const errors_1 = require("../../utils/errors"); /** * Redis storage provider implementation */ class RedisStorageProvider { /** * Create a new Redis storage provider * * @param config - Redis configuration */ constructor(config) { this.config = config; this.initialized = false; this.lastOperation = new Date(); this.metrics = { operationsPerSecond: 0, averageLatency: 0, errorRate: 0, averageQueryTime: 0, queriesPerSecond: 0 }; } /** * Initialize the provider */ async initialize() { if (this.initialized) { return; } try { // In a real implementation, this would create a Redis client this.client = { connect: async () => { }, set: async (key, value) => { }, get: async (key) => { }, del: async (key) => { }, keys: async (pattern) => [], quit: async () => { }, status: 'ready', info: async () => '', exists: async () => true }; // Connect to Redis await this.client.connect(); this.initialized = true; console.log('Redis provider initialized'); } catch (error) { console.error('Failed to initialize Redis provider:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.CONNECTION_FAILED, 'Failed to initialize Redis provider', { error }); } } /** * Store data * * @param data - Data to store * @param metadata - Data metadata * @returns Data ID */ async store(data, metadata) { this.ensureInitialized(); try { const id = metadata.id || crypto.randomUUID(); const key = `${metadata.category}:${id}`; // Store data with metadata const value = JSON.stringify({ data, metadata: { ...metadata, id } }); await this.client.set(key, value); // Set TTL if provided if (metadata.tags?.ttl) { const ttl = parseInt(metadata.tags.ttl); if (!isNaN(ttl)) { await this.client.expire(key, ttl); } } return id; } catch (error) { console.error('Failed to store data in Redis:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.STORE_FAILED, 'Failed to store data in Redis', { error }); } } /** * Retrieve data * * @param query - Data query * @returns Stored data */ async retrieve(query) { this.ensureInitialized(); try { // Handle direct ID lookup if (query.id) { const pattern = query.category ? `${query.category}:${query.id}` : `*:${query.id}`; const keys = await this.client.keys(pattern); if (keys.length === 0) { return []; } const value = await this.client.get(keys[0]); if (!value) { return []; } return [JSON.parse(value)]; } // Handle category-based lookup const pattern = query.category ? `${query.category}:*` : '*'; const keys = await this.client.keys(pattern); if (keys.length === 0) { return []; } // Get all values const values = await Promise.all(keys.map((key) => this.client.get(key))); const results = values .filter(Boolean) .map(value => JSON.parse(value)); // Apply filters let filteredResults = results; // Filter by time range if (query.timeRange) { filteredResults = filteredResults.filter(item => { const timestamp = new Date(item.metadata.timestamp).getTime(); if (query.timeRange?.start && timestamp < new Date(query.timeRange.start).getTime()) { return false; } if (query.timeRange?.end && timestamp > new Date(query.timeRange.end).getTime()) { return false; } return true; }); } // Filter by tags if (query.tags) { filteredResults = filteredResults.filter(item => { return Object.entries(query.tags || {}).every(([key, value]) => { return item.metadata.tags[key] === value; }); }); } // Apply sorting if (query.sort) { filteredResults.sort((a, b) => { const aValue = query.sort?.field === 'timestamp' ? new Date(a.metadata.timestamp).getTime() : a.data[query.sort?.field || '']; const bValue = query.sort?.field === 'timestamp' ? new Date(b.metadata.timestamp).getTime() : b.data[query.sort?.field || '']; if (query.sort?.order === 'desc') { return bValue - aValue; } else { return aValue - bValue; } }); } // Apply pagination if (query.offset || query.limit) { const offset = query.offset || 0; const limit = query.limit || filteredResults.length; filteredResults = filteredResults.slice(offset, offset + limit); } return filteredResults; } catch (error) { console.error('Failed to retrieve data from Redis:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.RETRIEVE_FAILED, 'Failed to retrieve data from Redis', { error }); } } /** * Update data * * @param data - Data to update */ async update(data) { this.ensureInitialized(); try { const id = data.metadata.id; if (!id) { throw new Error('Data ID is required for update'); } const key = `${data.metadata.category}:${id}`; // Check if data exists const exists = await this.client.exists(key); if (!exists) { throw new Error(`Data with ID ${id} not found`); } // Update data const value = JSON.stringify(data); await this.client.set(key, value); } catch (error) { console.error('Failed to update data in Redis:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.UPDATE_FAILED, 'Failed to update data in Redis', { error }); } } /** * Delete data * * @param id - Data ID */ async delete(id) { this.ensureInitialized(); try { // Find keys matching the ID const keys = await this.client.keys(`*:${id}`); if (keys.length === 0) { throw new Error(`Data with ID ${id} not found`); } // Delete all matching keys await Promise.all(keys.map((key) => this.client.del(key))); } catch (error) { console.error('Failed to delete data from Redis:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.DELETE_FAILED, 'Failed to delete data from Redis', { error }); } } /** * Store multiple data items * * @param items - Data items to store * @returns Data IDs */ async storeBatch(items) { this.ensureInitialized(); try { // Store each item const ids = await Promise.all(items.map(item => this.store(item.data, item.metadata))); return ids; } catch (error) { console.error('Failed to store batch data in Redis:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.STORE_FAILED, 'Failed to store batch data in Redis', { error }); } } /** * Clean up old data * * @param category - Data category * @param retentionDays - Retention period in days * @returns Cleanup result */ async cleanup(category, retentionDays) { this.ensureInitialized(); try { const pattern = `${category}:*`; const keys = await this.client.keys(pattern); if (keys.length === 0) { return { itemsRemoved: 0, bytesFreed: 0, duration: 0, errors: [] }; } // Get all values const values = await Promise.all(keys.map((key) => this.client.get(key))); const items = values .filter(Boolean) .map(value => JSON.parse(value)); // Find items older than retention period const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - retentionDays); const itemsToDelete = items.filter(item => { const timestamp = new Date(item.metadata.timestamp); return timestamp < cutoff; }); if (itemsToDelete.length === 0) { return { itemsRemoved: 0, bytesFreed: 0, duration: 0, errors: [] }; } // Delete items const startTime = Date.now(); const errors = []; let bytesFreed = 0; for (const item of itemsToDelete) { try { const key = `${category}:${item.metadata.id}`; const size = JSON.stringify(item).length; await this.client.del(key); bytesFreed += size; } catch (error) { errors.push(`Failed to delete item ${item.metadata.id}: ${error}`); } } return { itemsRemoved: itemsToDelete.length - errors.length, bytesFreed, duration: Date.now() - startTime, errors: errors.length > 0 ? errors : undefined }; } catch (error) { console.error('Failed to clean up data in Redis:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.CLEANUP_FAILED, 'Failed to clean up data in Redis', { error }); } } /** * Get storage status * * @returns Storage status */ async getStatus() { try { // In a real implementation, this would get Redis status const info = await this.client.info(); const memory = await this.client.info('memory'); // Parse memory info const usedMemoryMatch = memory ? memory.match(/used_memory:(\d+)/) : null; const maxMemoryMatch = memory ? memory.match(/maxmemory:(\d+)/) : null; const usedMemory = usedMemoryMatch ? parseInt(usedMemoryMatch[1], 10) : 0; const maxMemory = maxMemoryMatch ? parseInt(maxMemoryMatch[1], 10) : 0; return { connected: this.client.status === 'ready', healthy: this.client.status === 'ready', lastOperation: this.lastOperation, metrics: { operationsPerSecond: this.metrics.operationsPerSecond, averageLatency: this.metrics.averageLatency, errorRate: this.metrics.errorRate, storageUsed: usedMemory, storageAvailable: maxMemory }, queryPerformance: { hot: { averageQueryTime: this.metrics.averageQueryTime, queriesPerSecond: this.metrics.queriesPerSecond }, warm: { averageQueryTime: 0, queriesPerSecond: 0 }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, pendingItems: 0, averageBatchSize: 0, totalBatches: 0, failedBatches: 0, itemsRemoved: 0, bytesFreed: 0, duration: 0, hotStorageUsage: 0, warmStorageUsage: 0, coldStorageUsage: 0, totalStorageUsage: 0, hotStorageCapacity: 0, warmStorageCapacity: 0, coldStorageCapacity: 0, totalStorageCapacity: 0, operationsPerSecond: 0, averageLatency: 0, errorRate: 0, queryPerformance: { hot: { averageQueryTime: 0, queriesPerSecond: 0 }, warm: { averageQueryTime: 0, queriesPerSecond: 0 }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, overall: types_1.HealthStatus.HEALTHY, providers: { hot: types_1.HealthStatus.HEALTHY, warm: types_1.HealthStatus.HEALTHY, cold: types_1.HealthStatus.HEALTHY }, issues: { severity: types_1.AlertSeverity.INFO, message: 'Redis storage', component: 'redis', timestamp: new Date() }, circuitBreaker: { state: {}, failures: {}, lastFailure: {} }, retry: { retryCount: {}, successCount: {}, failureCount: {}, averageDelay: {} }, fallback: { fallbackCount: {}, successCount: {}, failureCount: {} }, timeout: { timeoutCount: {}, averageDuration: {} }, bulkhead: { concurrentOperations: {}, queueSize: {}, rejectionCount: {} }, rateLimit: { limitedCount: {}, bypassCount: {}, waitTimeMs: {}, avgTokensPerSecond: {}, currentTokens: {} } } } }; } catch (error) { console.error('Failed to get Redis status:', error); return { connected: false, healthy: false, error: `Failed to get Redis status: ${error}`, queryPerformance: { hot: { averageQueryTime: 0, queriesPerSecond: 0 }, warm: { averageQueryTime: 0, queriesPerSecond: 0 }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, pendingItems: 0, averageBatchSize: 0, totalBatches: 0, failedBatches: 0, itemsRemoved: 0, bytesFreed: 0, duration: 0, hotStorageUsage: 0, warmStorageUsage: 0, coldStorageUsage: 0, totalStorageUsage: 0, hotStorageCapacity: 0, warmStorageCapacity: 0, coldStorageCapacity: 0, totalStorageCapacity: 0, operationsPerSecond: 0, averageLatency: 0, errorRate: 0, queryPerformance: { hot: { averageQueryTime: 0, queriesPerSecond: 0 }, warm: { averageQueryTime: 0, queriesPerSecond: 0 }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, overall: types_1.HealthStatus.UNHEALTHY, providers: { hot: types_1.HealthStatus.UNHEALTHY, warm: types_1.HealthStatus.UNHEALTHY, cold: types_1.HealthStatus.UNHEALTHY }, issues: { severity: types_1.AlertSeverity.ALARM, message: `Failed to get Redis status: ${error}`, component: 'redis', timestamp: new Date() }, circuitBreaker: { state: {}, failures: {}, lastFailure: {} }, retry: { retryCount: {}, successCount: {}, failureCount: {}, averageDelay: {} }, fallback: { fallbackCount: {}, successCount: {}, failureCount: {} }, timeout: { timeoutCount: {}, averageDuration: {} }, bulkhead: { concurrentOperations: {}, queueSize: {}, rejectionCount: {} }, rateLimit: { limitedCount: {}, bypassCount: {}, waitTimeMs: {}, avgTokensPerSecond: {}, currentTokens: {} } } } }; } } /** * Shutdown the provider */ async shutdown() { if (!this.initialized) { return; } try { await this.client.quit(); this.initialized = false; console.log('Redis provider closed'); } catch (error) { console.error('Failed to close Redis provider:', error); throw new errors_1.StorageError(errors_1.StorageErrorCode.CONNECTION_FAILED, 'Failed to close Redis provider', { error }); } } /** * Ensure provider is initialized * * @throws StorageError if not initialized */ ensureInitialized() { if (!this.initialized) { throw new errors_1.StorageError(errors_1.StorageErrorCode.CONNECTION_FAILED, 'Redis provider not initialized', {}); } } } exports.RedisStorageProvider = RedisStorageProvider; /** * Create a new Redis storage provider * * @param config - Redis configuration * @returns Redis storage provider */ function createRedisProvider(config) { return new RedisStorageProvider(config); } exports.createRedisProvider = createRedisProvider; //# sourceMappingURL=redis.js.map