UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

602 lines (551 loc) 16.1 kB
/** * InfluxDB storage provider implementation */ import { StorageProvider, InfluxDBConfig, DataQuery, StoredData, CleanupResult, StorageStatus, DataMetadata, HealthStatus, AlertSeverity } from '../../types'; import { StorageError, StorageErrorCode } from '../../utils/errors'; /** * InfluxDB storage provider implementation */ export class InfluxDBStorageProvider implements StorageProvider { private client: any; // In a real implementation, this would be an InfluxDB client private initialized = false; private lastOperation: Date = new Date(); private metrics = { operationsPerSecond: 0, averageLatency: 0, errorRate: 0, averageQueryTime: 0, queriesPerSecond: 0 }; /** * Create a new InfluxDB storage provider * * @param config - InfluxDB configuration */ constructor(private config: InfluxDBConfig) {} /** * Initialize the provider */ async initialize(): Promise<void> { if (this.initialized) { return; } try { // In a real implementation, this would create an InfluxDB client this.client = { connect: async () => {}, write: async (point: any) => {}, query: async (query: string) => [], shutdown: async () => {}, health: async () => ({ status: 'pass' }), getStatus: async () => ({ up: true }) }; // Connect to InfluxDB await this.client.connect(); this.initialized = true; console.log('InfluxDB provider initialized'); } catch (error) { console.error('Failed to initialize InfluxDB provider:', error); throw new StorageError( StorageErrorCode.CONNECTION_FAILED, 'Failed to initialize InfluxDB provider', { error } ); } } /** * Store data * * @param data - Data to store * @param metadata - Data metadata * @returns Data ID */ async store(data: any, metadata: DataMetadata): Promise<string> { this.ensureInitialized(); try { const id = metadata.id || crypto.randomUUID(); // In a real implementation, this would create an InfluxDB point const point = { measurement: metadata.category, tags: { id, ...metadata.tags }, fields: { data: JSON.stringify(data), metadata: JSON.stringify({ ...metadata, id }) }, timestamp: new Date(metadata.timestamp) }; // Write point to InfluxDB await this.client.write(point); return id; } catch (error) { console.error('Failed to store data in InfluxDB:', error); throw new StorageError( StorageErrorCode.STORE_FAILED, 'Failed to store data in InfluxDB', { error } ); } } /** * Retrieve data * * @param query - Data query * @returns Stored data */ async retrieve(query: DataQuery): Promise<StoredData[]> { this.ensureInitialized(); try { // Build Flux query let fluxQuery = `from(bucket: "${this.config.bucket}")`; // Add measurement filter if (query.category) { fluxQuery += `\n |> filter(fn: (r) => r._measurement == "${query.category}")`; } // Add ID filter if (query.id) { fluxQuery += `\n |> filter(fn: (r) => r.id == "${query.id}")`; } // Add time range filter if (query.timeRange) { const start = query.timeRange.start || '-30d'; const end = query.timeRange.end || 'now()'; fluxQuery += `\n |> range(start: ${start}, stop: ${end})`; } else { fluxQuery += `\n |> range(start: -30d)`; } // Add tag filters if (query.tags) { for (const [key, value] of Object.entries(query.tags)) { fluxQuery += `\n |> filter(fn: (r) => r.${key} == "${value}")`; } } // Execute query const result = await this.client.query(fluxQuery); // Process results const data: StoredData[] = []; for (const row of result) { try { const parsedData = JSON.parse(row.data); const parsedMetadata = JSON.parse(row.metadata); data.push({ data: parsedData, metadata: parsedMetadata }); } catch (error) { console.warn('Failed to parse data from InfluxDB:', error); } } // Apply sorting if (query.sort) { data.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 || data.length; return data.slice(offset, offset + limit); } return data; } catch (error) { console.error('Failed to retrieve data from InfluxDB:', error); throw new StorageError( StorageErrorCode.RETRIEVE_FAILED, 'Failed to retrieve data from InfluxDB', { error } ); } } /** * Update data * * @param data - Data to update */ async update(data: StoredData): Promise<void> { this.ensureInitialized(); try { const id = data.metadata.id; if (!id) { throw new Error('Data ID is required for update'); } // In InfluxDB, we can't update data directly, so we delete and re-insert // First, retrieve the existing data to check if it exists const existingData = await this.retrieve({ id } as DataQuery); if (existingData.length === 0) { throw new Error(`Data with ID ${id} not found`); } // Delete existing data (not a real implementation, just a placeholder) await this.delete(id); // Store new data await this.store(data.data, data.metadata); } catch (error) { console.error('Failed to update data in InfluxDB:', error); throw new StorageError( StorageErrorCode.UPDATE_FAILED, 'Failed to update data in InfluxDB', { error } ); } } /** * Delete data * * @param id - Data ID */ async delete(id: string): Promise<void> { this.ensureInitialized(); try { // Build Flux delete query const fluxQuery = ` from(bucket: "${this.config.bucket}") |> filter(fn: (r) => r.id == "${id}") |> drop() `; // Execute query await this.client.query(fluxQuery); } catch (error) { console.error('Failed to delete data from InfluxDB:', error); throw new StorageError( StorageErrorCode.DELETE_FAILED, 'Failed to delete data from InfluxDB', { error } ); } } /** * Store multiple data items * * @param items - Data items to store * @returns Data IDs */ async storeBatch(items: Array<{data: any, metadata: DataMetadata}>): Promise<string[]> { 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 InfluxDB:', error); throw new StorageError( StorageErrorCode.STORE_FAILED, 'Failed to store batch data in InfluxDB', { error } ); } } /** * Clean up old data * * @param category - Data category * @param retentionDays - Retention period in days * @returns Cleanup result */ async cleanup(category: string, retentionDays: number): Promise<CleanupResult> { this.ensureInitialized(); try { // Build Flux delete query const fluxQuery = ` from(bucket: "${this.config.bucket}") |> filter(fn: (r) => r._measurement == "${category}") |> filter(fn: (r) => r._time < ${-retentionDays}d) |> drop() `; // Execute query await this.client.query(fluxQuery); // In a real implementation, we would get the number of deleted points return { itemsRemoved: 0, bytesFreed: 0, duration: 0 }; } catch (error) { console.error('Failed to clean up data in InfluxDB:', error); throw new StorageError( StorageErrorCode.CLEANUP_FAILED, 'Failed to clean up data in InfluxDB', { error } ); } } /** * Get storage status * * @returns Storage status */ async getStatus(): Promise<StorageStatus> { try { // Get InfluxDB health const health = await this.client.health(); const status = await this.client.getStatus(); return { connected: status.up, healthy: health.status === 'pass', lastOperation: this.lastOperation, metrics: { operationsPerSecond: this.metrics.operationsPerSecond, averageLatency: this.metrics.averageLatency, errorRate: this.metrics.errorRate, storageUsed: 0, storageAvailable: 0 }, queryPerformance: { hot: { averageQueryTime: 0, queriesPerSecond: 0 }, warm: { averageQueryTime: this.metrics.averageQueryTime, queriesPerSecond: this.metrics.queriesPerSecond }, 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: this.metrics.averageQueryTime, queriesPerSecond: this.metrics.queriesPerSecond }, cold: { averageQueryTime: 0, queriesPerSecond: 0 }, overall: HealthStatus.HEALTHY, providers: { hot: HealthStatus.HEALTHY, warm: HealthStatus.HEALTHY, cold: HealthStatus.HEALTHY }, issues: { severity: AlertSeverity.INFO, message: 'InfluxDB storage', component: 'influxdb', 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 InfluxDB status:', error); return { connected: false, healthy: false, error: `Failed to get InfluxDB 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: HealthStatus.UNHEALTHY, providers: { hot: HealthStatus.UNHEALTHY, warm: HealthStatus.UNHEALTHY, cold: HealthStatus.UNHEALTHY }, issues: { severity: AlertSeverity.ALARM, message: `Failed to get InfluxDB status: ${error}`, component: 'influxdb', 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(): Promise<void> { if (!this.initialized) { return; } try { await this.client.shutdown(); this.initialized = false; console.log('InfluxDB provider closed'); } catch (error) { console.error('Failed to close InfluxDB provider:', error); throw new StorageError( StorageErrorCode.CONNECTION_FAILED, 'Failed to close InfluxDB provider', { error } ); } } /** * Ensure provider is initialized * * @throws StorageError if not initialized */ private ensureInitialized(): void { if (!this.initialized) { throw new StorageError( StorageErrorCode.CONNECTION_FAILED, 'InfluxDB provider not initialized', {} ); } } } /** * Create a new InfluxDB storage provider * * @param config - InfluxDB configuration * @returns InfluxDB storage provider */ export function createInfluxDBProvider(config: InfluxDBConfig): StorageProvider { return new InfluxDBStorageProvider(config); }