@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
662 lines (598 loc) • 17.8 kB
text/typescript
/**
* Redis storage provider implementation
*/
import {
StorageProvider,
RedisConfig,
DataQuery,
StoredData,
CleanupResult,
StorageStatus,
DataMetadata,
HealthStatus,
AlertSeverity
} from '../../types';
import { StorageError, StorageErrorCode } from '../../utils/errors';
/**
* Redis storage provider implementation
*/
export class RedisStorageProvider implements StorageProvider {
private client: any; // In a real implementation, this would be a Redis client
private initialized = false;
private lastOperation: Date = new Date();
private metrics = {
operationsPerSecond: 0,
averageLatency: 0,
errorRate: 0,
averageQueryTime: 0,
queriesPerSecond: 0
};
/**
* Create a new Redis storage provider
*
* @param config - Redis configuration
*/
constructor(private config: RedisConfig) {}
/**
* Initialize the provider
*/
async initialize(): Promise<void> {
if (this.initialized) {
return;
}
try {
// In a real implementation, this would create a Redis client
this.client = {
connect: async () => {},
set: async (key: string, value: string) => {},
get: async (key: string) => {},
del: async (key: string) => {},
keys: async (pattern: string) => [],
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 StorageError(
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: any, metadata: DataMetadata): Promise<string> {
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 StorageError(
StorageErrorCode.STORE_FAILED,
'Failed to store data in Redis',
{ error }
);
}
}
/**
* Retrieve data
*
* @param query - Data query
* @returns Stored data
*/
async retrieve(query: DataQuery): Promise<StoredData[]> {
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: string) => 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 StorageError(
StorageErrorCode.RETRIEVE_FAILED,
'Failed to retrieve data from Redis',
{ 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');
}
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 StorageError(
StorageErrorCode.UPDATE_FAILED,
'Failed to update data in Redis',
{ error }
);
}
}
/**
* Delete data
*
* @param id - Data ID
*/
async delete(id: string): Promise<void> {
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: string) => this.client.del(key)));
} catch (error) {
console.error('Failed to delete data from Redis:', error);
throw new StorageError(
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: 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 Redis:', error);
throw new StorageError(
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: string, retentionDays: number): Promise<CleanupResult> {
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: string) => 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: string[] = [];
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 StorageError(
StorageErrorCode.CLEANUP_FAILED,
'Failed to clean up data in Redis',
{ error }
);
}
}
/**
* Get storage status
*
* @returns Storage status
*/
async getStatus(): Promise<StorageStatus> {
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: HealthStatus.HEALTHY,
providers: {
hot: HealthStatus.HEALTHY,
warm: HealthStatus.HEALTHY,
cold: HealthStatus.HEALTHY
},
issues: {
severity: 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: HealthStatus.UNHEALTHY,
providers: {
hot: HealthStatus.UNHEALTHY,
warm: HealthStatus.UNHEALTHY,
cold: HealthStatus.UNHEALTHY
},
issues: {
severity: 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(): Promise<void> {
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 StorageError(
StorageErrorCode.CONNECTION_FAILED,
'Failed to close Redis provider',
{ error }
);
}
}
/**
* Ensure provider is initialized
*
* @throws StorageError if not initialized
*/
private ensureInitialized(): void {
if (!this.initialized) {
throw new StorageError(
StorageErrorCode.CONNECTION_FAILED,
'Redis provider not initialized',
{}
);
}
}
}
/**
* Create a new Redis storage provider
*
* @param config - Redis configuration
* @returns Redis storage provider
*/
export function createRedisProvider(config: RedisConfig): StorageProvider {
return new RedisStorageProvider(config);
}