UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

360 lines (359 loc) 13.7 kB
/** * Redis Database Adapter * * Implements IDatabaseAdapter for Redis key-value store. * Part of Task 0.4: Database Query Abstraction Layer (MVP) * * UPDATED: Now uses ConnectionPoolManager for proper connection pool initialization, * health checks, automatic reconnection with exponential backoff, and connection metrics. * * SECURITY: Supports Redis authentication via requirepass with secure password handling */ import { randomUUID } from 'crypto'; import { DatabaseErrorCode, createDatabaseError, createSuccessResult, createFailedResult, mapRedisError } from './errors.js'; import { ConnectionPoolManager } from './connection-pool-manager.js'; import { v4 as uuidv4 } from 'uuid'; export class RedisAdapter { poolManager = null; client = null; config; connected = false; errorAggregator; correlationId; constructor(config, errorAggregator){ this.config = config; this.errorAggregator = errorAggregator; this.correlationId = uuidv4(); } /** * Track error with error aggregator * @private */ trackError(error, operation, context) { if (this.errorAggregator) { const dbError = error.code ? error : createDatabaseError(DatabaseErrorCode.QUERY_FAILED, `Redis ${operation} failed`, error instanceof Error ? error : new Error(String(error)), context); this.errorAggregator.addError('redis', dbError, { ...context, operation, correlationId: this.correlationId }); } } /** * Record successful operation with error aggregator * @private */ recordSuccess() { if (this.errorAggregator) { this.errorAggregator.recordSuccess('redis'); } } getType() { return 'redis'; } async connect() { try { // Initialize connection pool manager this.poolManager = new ConnectionPoolManager(this.config); await this.poolManager.initialize(); // Get the Redis client from pool manager this.client = await this.poolManager.acquire(); // Start health checks (ping every 30s) this.poolManager.startHealthChecks(); this.connected = true; this.recordSuccess(); } catch (err) { const error = createDatabaseError(DatabaseErrorCode.CONNECTION_FAILED, 'Failed to connect to Redis', err instanceof Error ? err : new Error(String(err)), { config: this.config, correlationId: this.correlationId }); this.trackError(error, 'connect'); throw error; } } async disconnect() { if (this.poolManager) { await this.poolManager.shutdown(); this.poolManager = null; this.client = null; this.connected = false; } } isConnected() { return this.connected && this.poolManager !== null && this.client !== null; } /** * Get connection pool statistics */ getPoolStats() { return this.poolManager?.getStats(); } async get(key) { this.ensureConnected(); try { const value = await this.client.get(key); if (value === null) { return null; } // Try to parse as JSON, fall back to raw string try { this.recordSuccess(); return JSON.parse(value); } catch { this.recordSuccess(); return value; } } catch (err) { const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to get key: ${key}`, err instanceof Error ? err : new Error(String(err)), { key, correlationId: this.correlationId }); this.trackError(error, 'get', { key }); throw error; } } async list(pattern, options) { this.ensureConnected(); try { const keys = await this.client.keys(pattern); if (keys.length === 0) { return []; } const values = await this.client.mGet(keys); const results = values.map((value, index)=>{ if (value === null) { return null; } try { return JSON.parse(value); } catch { return value; } }).filter((v)=>v !== null); // Apply limit and offset const start = options?.offset || 0; const end = options?.limit ? start + options.limit : undefined; this.recordSuccess(); return results.slice(start, end); } catch (err) { const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to list keys: ${pattern}`, err instanceof Error ? err : new Error(String(err)), { pattern, options, correlationId: this.correlationId }); this.trackError(error, 'list', { pattern }); throw error; } } async query(pattern, filters) { // For Redis, query is similar to list but with additional filtering const results = await this.list(pattern); // Apply filters return results.filter((item)=>{ return filters.every((filter)=>{ const value = item[filter.field]; switch(filter.operator){ case 'eq': return value === filter.value; case 'ne': return value !== filter.value; case 'gt': return value > filter.value; case 'gte': return value >= filter.value; case 'lt': return value < filter.value; case 'lte': return value <= filter.value; case 'in': return Array.isArray(filter.value) && filter.value.includes(value); case 'like': return String(value).includes(String(filter.value)); default: return true; } }); }); } async insert(key, data) { this.ensureConnected(); try { const value = typeof data === 'string' ? data : JSON.stringify(data); await this.client.set(key, value); this.recordSuccess(); return createSuccessResult(data, 1); } catch (err) { const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to insert key: ${key}`, err instanceof Error ? err : new Error(String(err)), { key, data, correlationId: this.correlationId }); this.trackError(error, 'insert', { key }); return createFailedResult(error); } } async insertMany(pattern, data) { this.ensureConnected(); try { const pipeline = this.client.multi(); data.forEach((item, index)=>{ const key = `${pattern}:${index}`; const value = typeof item === 'string' ? item : JSON.stringify(item); pipeline.set(key, value); }); await pipeline.exec(); this.recordSuccess(); return createSuccessResult(data, data.length); } catch (err) { const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to insert multiple keys with pattern: ${pattern}`, err instanceof Error ? err : new Error(String(err)), { pattern, count: data.length, correlationId: this.correlationId }); this.trackError(error, 'insertMany', { pattern, count: data.length }); return createFailedResult(error); } } async update(key, data) { this.ensureConnected(); try { // Get existing data const existing = await this.get(key); if (existing === null) { const error = createDatabaseError(DatabaseErrorCode.NOT_FOUND, `Key not found: ${key}`, undefined, { key, correlationId: this.correlationId }); this.trackError(error, 'update', { key }); return createFailedResult(error); } // Merge with updates const updated = { ...existing, ...data }; const value = JSON.stringify(updated); await this.client.set(key, value); this.recordSuccess(); return createSuccessResult(updated, 1); } catch (err) { const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to update key: ${key}`, err instanceof Error ? err : new Error(String(err)), { key, data, correlationId: this.correlationId }); this.trackError(error, 'update', { key }); return createFailedResult(error); } } async delete(_table, key) { this.ensureConnected(); try { const count = await this.client.del(key); if (count === 0) { const error = createDatabaseError(DatabaseErrorCode.NOT_FOUND, `Key not found: ${key}`, undefined, { key, correlationId: this.correlationId }); this.trackError(error, 'delete', { key }); return createFailedResult(error); } this.recordSuccess(); return createSuccessResult(undefined, count); } catch (err) { const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to delete key: ${key}`, err instanceof Error ? err : new Error(String(err)), { key, correlationId: this.correlationId }); this.trackError(error, 'delete', { key }); return createFailedResult(error); } } async raw(command, params) { this.ensureConnected(); try { const result = await this.client.sendCommand([ command, ...params || [] ]); this.recordSuccess(); return result; } catch (err) { const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to execute raw command: ${command}`, err instanceof Error ? err : new Error(String(err)), { command, params, correlationId: this.correlationId }); this.trackError(error, 'raw', { command }); throw error; } } async beginTransaction() { return { id: `redis-tx-${randomUUID()}`, databases: [ 'redis' ], startTime: new Date(), status: 'pending' }; } async prepareTransaction(context) { try { // Redis doesn't support traditional two-phase commit // PREPARE validation: Check if Redis is available and can accept commands this.ensureConnected(); // Test connection and command execution await this.client.ping(); // Mark as prepared context.status = 'prepared'; context.preparedAt = new Date(); return true; } catch (err) { // Prepare failed - typically due to connection issues throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Failed to prepare transaction - Redis unavailable', err instanceof Error ? err : new Error(String(err)), { transactionId: context.id }); } } async commitTransaction(context) { // Redis transactions are handled via MULTI/EXEC // This is a placeholder for cross-database transaction support context.status = 'committed'; } async rollbackTransaction(context) { // Redis doesn't support traditional rollback // This is a placeholder for cross-database transaction support context.status = 'rolled_back'; } ensureConnected() { if (!this.isConnected()) { throw createDatabaseError(DatabaseErrorCode.CONNECTION_FAILED, 'Not connected to Redis', undefined, { config: this.config }); } } } //# sourceMappingURL=redis-adapter.js.map