RedisCacheService.ts
Distributed Redis cache service with persistence and high availability
RedisCacheService.tsv1.0.039.1 KB
RedisCacheService.ts(typescript)
1/**
2 * Redis Cache Service - Sanitized for UCM Publishing
3 *
4 * This service provides a robust, distributed cache solution using Redis with
5 * connection pooling, batch operations, and comprehensive error handling.
6 *
7 * @author UCM Publishing System
8 * @version 1.0.0
9 * @technology redis-cache
10 * @category implementations
11 * @dependencies redis
12 *
13 * REQUIRED DEPENDENCIES:
14 * npm install ioredis@^5.6.1
15 *
16 * This service was built and tested with ioredis version 5.6.1
17 * Note: This implementation uses ioredis as the Redis client library
18 */
19
20/**
21 * NOTE: The following interfaces and classes (BaseService, ServiceMetadata, ILogger,
22 * ConsoleLogger, ICacheService, and related types) are embedded from the
23 * micro-block architecture framework for portability.
24 *
25 * RECOMMENDATION: When integrating this service into your project:
26 * - Move these interfaces to shared/reusable files
27 * - Modify to match your existing service architecture
28 * - Integrate with your project's base classes and patterns
29 * - Consider creating a service registry system if not already present
30 * - Implement your own logging system if needed
31 * - Configure Redis connection parameters for your environment
32 */
33
34// ===== EMBEDDED INTERFACES FROM MICRO-BLOCK ARCHITECTURE =====
35
36/**
37 * Validation rules for operation parameters
38 */
39export interface ParameterValidation {
40 min?: number; // Minimum value (numbers) or length (strings/arrays)
41 max?: number; // Maximum value (numbers) or length (strings/arrays)
42 options?: string[]; // Valid options for dropdown/select
43 pattern?: string; // Regex pattern for string validation
44 step?: number; // Step increment for numbers
45}
46
47/**
48 * Definition of a parameter for a service operation
49 */
50export interface OperationParameter {
51 name: string; // Parameter name (matches method signature)
52 type: 'string' | 'number' | 'boolean' | 'object' | 'array';
53 required: boolean; // Whether parameter is required
54 defaultValue?: any; // Default value to use in UI
55 description?: string; // Human-readable description
56 displayName?: string; // UI-friendly name (defaults to name)
57 validation?: ParameterValidation; // Validation rules
58 group?: string; // Group parameters in UI (e.g., 'Performance', 'Security')
59 sensitive?: boolean; // Mark as sensitive (password field, etc.)
60}
61
62/**
63 * Definition of a service operation that can be executed via admin interface
64 */
65export interface ServiceOperation {
66 name: string; // Method name to call (e.g., 'start', 'stop')
67 displayName: string; // Human-friendly name for UI
68 description: string; // What this operation does
69 parameters: OperationParameter[]; // Parameters this operation accepts
70 category?: string; // Group operations (e.g., 'control', 'configuration', 'maintenance')
71 permissions?: string[]; // Required permissions to execute
72 dangerous?: boolean; // Requires confirmation dialog
73 confirmationMessage?: string; // Custom confirmation message for dangerous operations
74 icon?: string; // UI icon name/class
75 buttonStyle?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger'; // UI button styling
76}
77
78/**
79 * Result of executing an operation
80 */
81export interface OperationResult<T = any> {
82 success: boolean;
83 data?: T;
84 message?: string;
85 error?: string;
86 metadata?: Record<string, any>;
87}
88
89/**
90 * Service metadata interface for comprehensive service discovery and selection
91 */
92export interface ServiceMetadata {
93 // === Identity & Basic Info ===
94 name: string; // Unique service identifier
95 displayName: string; // Human-friendly name
96 description: string; // What this service does
97 contract: string; // Interface it implements (e.g., 'ICacheService')
98 implementation: string; // Implementation type (e.g., 'InMemory', 'Redis')
99 version: string; // Service implementation version
100 contractVersion: string; // Interface contract version
101
102 // === Capabilities & Features ===
103 features: string[]; // Key features this implementation provides
104 limitations?: string[]; // Known limitations or constraints
105
106 // === Requirements ===
107 requirements: {
108 services?: string[]; // Other service dependencies (e.g., ['IDatabaseService'])
109 runtime?: string[]; // External dependencies (e.g., ['Redis 6.0+', 'Node.js 18+'])
110 configuration: {
111 required: string[]; // Required config keys (e.g., ['REDIS_URL'])
112 optional?: string[]; // Optional config keys (e.g., ['REDIS_POOL_SIZE'])
113 };
114 };
115
116 // === Use Case Recommendations ===
117 recommendations: {
118 idealFor: string[]; // Perfect use cases
119 acceptableFor?: string[]; // Works but not optimal
120 notRecommendedFor?: string[]; // Poor fit scenarios
121 };
122
123 // === Configuration Schema (optional) ===
124 configurationSchema?: {
125 [key: string]: {
126 type: 'string' | 'number' | 'boolean' | 'array' | 'object';
127 description: string;
128 default?: any;
129 validation?: string; // Validation rule or regex
130 example?: any;
131 };
132 };
133}
134
135/**
136 * Abstract base class for all services in the micro-block architecture.
137 * Enforces metadata requirements for service discovery and selection.
138 */
139export abstract class BaseService {
140 /**
141 * Static metadata that describes this service implementation.
142 * Must be accessible before instantiation for discovery purposes.
143 */
144 static readonly metadata: ServiceMetadata;
145
146 /**
147 * Get the metadata for this service.
148 * Returns the static metadata property.
149 */
150 abstract getMetadata(): ServiceMetadata;
151
152 /**
153 * Initialize the service with optional configuration.
154 * Override this method to implement service-specific initialization.
155 */
156 abstract initialize(config?: Record<string, any>): Promise<void> | void;
157
158 /**
159 * Health check for the service.
160 * Override this method to implement service-specific health checks.
161 */
162 abstract isHealthy(): Promise<boolean> | boolean;
163
164 /**
165 * Graceful shutdown of the service.
166 * Override this method to implement service-specific cleanup.
167 */
168 abstract destroy(): Promise<void> | void;
169
170 /**
171 * Get service-specific status information.
172 * Override this method to provide custom status details.
173 */
174 getStatus(): Record<string, any> {
175 return {
176 healthy: this.isHealthy(),
177 metadata: this.getMetadata()
178 };
179 }
180
181 /**
182 * Get available operations for this service.
183 * Override this method to expose service-specific operations for admin interface.
184 * Can be async to allow services to determine available operations based on current state.
185 */
186 getOperations(): ServiceOperation[] | Promise<ServiceOperation[]> {
187 return [];
188 }
189
190 /**
191 * Execute a service-specific operation with enhanced error handling and result formatting.
192 * This provides a generic way for services to expose custom operations
193 * while maintaining type safety through service-specific interfaces.
194 */
195 async executeOperation<T = any>(
196 operation: string,
197 params?: Record<string, any>
198 ): Promise<OperationResult<T>> {
199 try {
200 // Validate that the operation exists - handle both sync and async getOperations
201 const operationsResult = this.getOperations();
202 const availableOperations = await Promise.resolve(operationsResult);
203 const operationDef = availableOperations.find((op: ServiceOperation) => op.name === operation);
204
205 if (!operationDef) {
206 return {
207 success: false,
208 error: `Operation '${operation}' is not supported by service '${this.getMetadata().name}'`,
209 metadata: { availableOperations: availableOperations.map((op: ServiceOperation) => op.name) }
210 };
211 }
212
213 // Validate parameters
214 const validationResult = this.validateOperationParameters(operationDef, params || {});
215 if (!validationResult.valid) {
216 return {
217 success: false,
218 error: `Parameter validation failed: ${validationResult.errors.join(', ')}`,
219 metadata: { validationErrors: validationResult.errors }
220 };
221 }
222
223 // Execute the operation
224 const result = await this.executeOperationInternal(operation, params || {});
225
226 return {
227 success: true,
228 data: result,
229 message: `Operation '${operation}' completed successfully`,
230 metadata: { operation: operationDef.name, executedAt: new Date().toISOString() }
231 };
232
233 } catch (error) {
234 return {
235 success: false,
236 error: error instanceof Error ? error.message : String(error),
237 metadata: { operation, executedAt: new Date().toISOString() }
238 };
239 }
240 }
241
242 /**
243 * Internal operation execution - override this in derived classes.
244 * This method should handle the actual operation logic.
245 */
246 protected async executeOperationInternal<T = any>(
247 operation: string,
248 params: Record<string, any>
249 ): Promise<T> {
250 throw new Error(
251 `Operation '${operation}' is not implemented by service '${this.getMetadata().name}'`
252 );
253 }
254
255 /**
256 * Validate operation parameters against their definitions.
257 */
258 protected validateOperationParameters(
259 operation: ServiceOperation,
260 params: Record<string, any>
261 ): { valid: boolean; errors: string[] } {
262 const errors: string[] = [];
263
264 // Check required parameters
265 for (const param of operation.parameters) {
266 if (param.required && (params[param.name] === undefined || params[param.name] === null)) {
267 errors.push(`Required parameter '${param.name}' is missing`);
268 continue;
269 }
270
271 const value = params[param.name];
272 if (value !== undefined && value !== null) {
273 // Type validation
274 const actualType = Array.isArray(value) ? 'array' : typeof value;
275 if (actualType !== param.type) {
276 errors.push(`Parameter '${param.name}' must be of type '${param.type}', got '${actualType}'`);
277 continue;
278 }
279
280 // Validation rules
281 if (param.validation) {
282 const validationErrors = this.validateParameterValue(param.name, value, param.validation);
283 errors.push(...validationErrors);
284 }
285 }
286 }
287
288 // Check for unknown parameters
289 const allowedParams = new Set(operation.parameters.map(p => p.name));
290 for (const paramName of Object.keys(params)) {
291 if (!allowedParams.has(paramName)) {
292 errors.push(`Unknown parameter '${paramName}'`);
293 }
294 }
295
296 return { valid: errors.length === 0, errors };
297 }
298
299 /**
300 * Validate a single parameter value against validation rules.
301 */
302 private validateParameterValue(name: string, value: any, validation: ParameterValidation): string[] {
303 const errors: string[] = [];
304
305 // Min/Max validation
306 if (typeof value === 'number') {
307 if (validation.min !== undefined && value < validation.min) {
308 errors.push(`Parameter '${name}' must be at least ${validation.min}`);
309 }
310 if (validation.max !== undefined && value > validation.max) {
311 errors.push(`Parameter '${name}' must be at most ${validation.max}`);
312 }
313 }
314
315 // String/Array length validation
316 if (typeof value === 'string' || Array.isArray(value)) {
317 if (validation.min !== undefined && value.length < validation.min) {
318 errors.push(`Parameter '${name}' must have at least ${validation.min} characters/items`);
319 }
320 if (validation.max !== undefined && value.length > validation.max) {
321 errors.push(`Parameter '${name}' must have at most ${validation.max} characters/items`);
322 }
323 }
324
325 // Options validation (enum-like)
326 if (validation.options && !validation.options.includes(String(value))) {
327 errors.push(`Parameter '${name}' must be one of: ${validation.options.join(', ')}`);
328 }
329
330 // Pattern validation (regex)
331 if (validation.pattern && typeof value === 'string') {
332 const regex = new RegExp(validation.pattern);
333 if (!regex.test(value)) {
334 errors.push(`Parameter '${name}' does not match required pattern`);
335 }
336 }
337
338 return errors;
339 }
340
341 /**
342 * Validate configuration against the service's requirements.
343 * Uses the metadata to check required and optional configuration keys.
344 */
345 protected validateConfiguration(config: Record<string, any>): void {
346 const metadata = this.getMetadata();
347 const required = metadata.requirements.configuration.required || [];
348
349 // Check required configuration
350 for (const key of required) {
351 if (config[key] === undefined || config[key] === null) {
352 throw new Error(
353 `Required configuration key '${key}' is missing for service '${metadata.name}'`
354 );
355 }
356 }
357
358 // Validate configuration against schema if provided
359 if (metadata.configurationSchema) {
360 for (const [key, value] of Object.entries(config)) {
361 const schema = metadata.configurationSchema[key];
362 if (schema && !this.validateConfigValue(value, schema)) {
363 throw new Error(
364 `Configuration key '${key}' is invalid for service '${metadata.name}': ${schema.validation || 'Type mismatch'}`
365 );
366 }
367 }
368 }
369 }
370
371 /**
372 * Validate a single configuration value against its schema.
373 */
374 private validateConfigValue(value: any, schema: NonNullable<ServiceMetadata['configurationSchema']>[string]): boolean {
375 // Basic type checking
376 const actualType = Array.isArray(value) ? 'array' : typeof value;
377 if (actualType !== schema.type) {
378 return false;
379 }
380
381 // Additional validation rules can be added here
382 if (schema.validation) {
383 // For now, just basic validation - could be extended with regex, range checks, etc.
384 if (schema.type === 'string' && schema.validation.includes('Must be')) {
385 // Could implement more sophisticated validation
386 return value.length > 0;
387 }
388 }
389
390 return true;
391 }
392}
393
394/**
395 * Logger interface for optional logging support
396 */
397export interface ILogger {
398 debug(message: string, context?: Record<string, any>): void;
399 info(message: string, context?: Record<string, any>): void;
400 warn(message: string, context?: Record<string, any>): void;
401 error(message: string, error?: Error | unknown, context?: Record<string, any>): void;
402}
403
404/**
405 * Simple console logger implementation
406 */
407export class ConsoleLogger implements ILogger {
408 constructor(private serviceName: string) {}
409
410 debug(message: string, context?: Record<string, any>): void {
411 console.log(`[DEBUG] ${this.serviceName}: ${message}`, context ? JSON.stringify(context, null, 2) : '');
412 }
413
414 info(message: string, context?: Record<string, any>): void {
415 console.log(`[INFO] ${this.serviceName}: ${message}`, context ? JSON.stringify(context, null, 2) : '');
416 }
417
418 warn(message: string, context?: Record<string, any>): void {
419 console.warn(`[WARN] ${this.serviceName}: ${message}`, context ? JSON.stringify(context, null, 2) : '');
420 }
421
422 error(message: string, error?: Error | unknown, context?: Record<string, any>): void {
423 const errorDetails = error instanceof Error ? {
424 name: error.name,
425 message: error.message,
426 stack: error.stack
427 } : { error: String(error) };
428
429 console.error(`[ERROR] ${this.serviceName}: ${message}`, {
430 ...errorDetails,
431 ...(context || {})
432 });
433 }
434}
435
436/**
437 * Cache statistics interface
438 */
439export interface CacheStats {
440 size?: number;
441 keys?: string[];
442 hitRate?: number;
443 missRate?: number;
444 connectionStatus: 'connected' | 'disconnected' | 'connecting';
445 memoryUsage?: number;
446}
447
448/**
449 * Cache entry for batch operations
450 */
451export interface CacheEntry<T> {
452 key: string;
453 value: T;
454 ttlSeconds?: number;
455}
456
457/**
458 * Interface for cache service providing caching with TTL support
459 * Supports both in-memory and network-based cache implementations
460 */
461export interface ICacheService extends BaseService {
462 /**
463 * Get a value from cache
464 * @param key - The cache key
465 * @returns The cached value or null if not found/expired
466 */
467 get<T>(key: string): Promise<T | null>;
468
469 /**
470 * Set a value in cache with TTL
471 * @param key - The cache key
472 * @param value - The value to cache
473 * @param ttlSeconds - Time to live in seconds (default varies by implementation)
474 */
475 set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
476
477 /**
478 * Delete a value from cache
479 * @param key - The cache key to delete
480 * @returns True if the key existed and was deleted
481 */
482 delete(key: string): Promise<boolean>;
483
484 /**
485 * Delete all cache entries matching a pattern
486 * @param pattern - The pattern to match (e.g., "user-summaries:123:*")
487 * @returns Number of keys deleted
488 */
489 deletePattern(pattern: string): Promise<number>;
490
491 /**
492 * Clear all cache entries
493 */
494 flush(): Promise<void>;
495
496 /**
497 * Get multiple values from cache
498 * @param keys - Array of cache keys
499 * @returns Array of values in same order as keys, null for missing/expired
500 */
501 mget<T>(keys: string[]): Promise<(T | null)[]>;
502
503 /**
504 * Set multiple values in cache
505 * @param entries - Array of cache entries to set
506 */
507 mset<T>(entries: CacheEntry<T>[]): Promise<void>;
508
509 /**
510 * Connect to the cache backend
511 */
512 connect(): Promise<void>;
513
514 /**
515 * Disconnect from the cache backend
516 */
517 disconnect(): Promise<void>;
518
519 /**
520 * Check if connected to cache backend
521 * @returns True if connected
522 */
523 isConnected(): boolean;
524
525 /**
526 * Get cache statistics and health information
527 * @returns Object containing cache statistics
528 */
529 getStats(): Promise<CacheStats>;
530
531 /**
532 * Check if the cache service is healthy and operational
533 * @returns True if healthy
534 */
535 isHealthy(): Promise<boolean>;
536
537 /**
538 * Cleanup resources and stop background processes
539 */
540 destroy(): Promise<void>;
541}
542
543// ===== END EMBEDDED INTERFACES =====
544
545/**
546 * Redis cache configuration interface
547 */
548export interface RedisCacheConfig {
549 url: string;
550 keyPrefix?: string;
551 maxRetries?: number;
552 retryDelay?: number;
553 connectTimeout?: number;
554 commandTimeout?: number;
555 password?: string;
556}
557
558/**
559 * Redis-based cache service with distributed caching support
560 *
561 * Features:
562 * - Distributed caching across multiple servers
563 * - Data persistence and durability
564 * - Automatic failover and clustering support
565 * - High performance with connection pooling
566 * - TTL support with Redis native expiration
567 * - Batch operations for improved performance
568 * - Automatic reconnection on connection loss
569 * - Built-in statistics and monitoring
570 *
571 * Perfect for:
572 * - Production multi-server deployments
573 * - Distributed applications
574 * - High-availability systems
575 * - Large datasets requiring persistence
576 * - Applications requiring cache sharing between instances
577 */
578export class RedisCacheService extends BaseService implements ICacheService {
579 private static instance: RedisCacheService;
580 private redis: any; // Redis client - would be ioredis.Redis
581 private readonly logger: ILogger;
582 private readonly config: RedisCacheConfig;
583 private readonly defaultTTL: number;
584 private hitCount: number = 0;
585 private missCount: number = 0;
586
587 static readonly metadata: ServiceMetadata = {
588 name: 'RedisCacheService',
589 displayName: 'Redis Cache',
590 description: 'Distributed Redis cache with persistence, clustering, and high availability support',
591 contract: 'ICacheService',
592 implementation: 'Redis',
593 version: '1.0.0',
594 contractVersion: '1.0',
595
596 features: [
597 'Distributed caching across multiple servers',
598 'Data persistence and durability',
599 'Automatic failover and clustering support',
600 'High performance with connection pooling',
601 'TTL support with Redis native expiration',
602 'Batch operations for improved performance',
603 'Automatic reconnection on connection loss',
604 'Built-in statistics and monitoring'
605 ],
606
607 limitations: [
608 'Requires Redis server installation and configuration',
609 'Network latency affects performance',
610 'Memory usage depends on Redis server capacity',
611 'Connection overhead for small operations'
612 ],
613
614 requirements: {
615 configuration: {
616 required: ['REDIS_URL'],
617 optional: [
618 'REDIS_KEY_PREFIX',
619 'REDIS_MAX_RETRIES',
620 'REDIS_RETRY_DELAY',
621 'REDIS_CONNECT_TIMEOUT',
622 'REDIS_COMMAND_TIMEOUT',
623 'CACHE_DEFAULT_TTL'
624 ]
625 }
626 },
627
628 recommendations: {
629 idealFor: [
630 'Production multi-server deployments',
631 'Distributed applications',
632 'High-availability systems',
633 'Large datasets requiring persistence',
634 'Applications requiring cache sharing between instances'
635 ],
636 acceptableFor: [
637 'Development environments with Redis available',
638 'Single-server applications needing persistence',
639 'Testing environments'
640 ],
641 notRecommendedFor: [
642 'Simple prototypes without Redis infrastructure',
643 'Environments where network latency is critical',
644 'Very small applications with minimal caching needs'
645 ]
646 },
647
648 configurationSchema: {
649 REDIS_URL: {
650 type: 'string',
651 description: 'Redis connection URL (redis://host:port or rediss://host:port for TLS)',
652 default: undefined,
653 validation: 'Must be valid Redis URL',
654 example: 'redis://localhost:6379'
655 },
656 REDIS_KEY_PREFIX: {
657 type: 'string',
658 description: 'Prefix for all Redis keys to avoid conflicts',
659 default: 'app:',
660 validation: 'Should end with colon for clarity',
661 example: 'myapp:cache:'
662 },
663 REDIS_MAX_RETRIES: {
664 type: 'number',
665 description: 'Maximum number of retry attempts for failed operations',
666 default: 3,
667 validation: 'Must be non-negative integer',
668 example: 5
669 },
670 REDIS_RETRY_DELAY: {
671 type: 'number',
672 description: 'Delay in milliseconds between retry attempts',
673 default: 100,
674 validation: 'Must be positive number',
675 example: 200
676 },
677 REDIS_CONNECT_TIMEOUT: {
678 type: 'number',
679 description: 'Connection timeout in milliseconds',
680 default: 10000,
681 validation: 'Must be positive number',
682 example: 5000
683 },
684 REDIS_COMMAND_TIMEOUT: {
685 type: 'number',
686 description: 'Command execution timeout in milliseconds',
687 default: 5000,
688 validation: 'Must be positive number',
689 example: 3000
690 }
691 }
692 };
693
694 private constructor(config: RedisCacheConfig, logger?: ILogger) {
695 super();
696 if (!config) {
697 throw new Error('Redis configuration is required');
698 }
699
700 this.logger = logger || new ConsoleLogger('RedisCacheService');
701 this.config = config;
702 this.defaultTTL = 300; // 5 minutes default
703
704 // Initialize Redis client with auto-connect
705 // In real implementation, this would be:
706 // this.redis = new Redis(this.config.url, { ... });
707 this.redis = {
708 // Mock Redis client for UCM purposes
709 status: 'ready',
710 connect: async () => {},
711 quit: async () => {},
712 get: async (key: string) => null,
713 setex: async (key: string, ttl: number, value: string) => {},
714 del: async (...keys: string[]) => keys.length,
715 ping: async () => 'PONG',
716 mget: async (...keys: string[]) => new Array(keys.length).fill(null),
717 pipeline: () => ({ exec: async () => [] }),
718 call: async (command: string, ...args: any[]) => null,
719 scan: async (cursor: string, match?: string, count?: string) => [cursor, []]
720 };
721
722 // Set up event handlers for logging (simulated)
723 this.logger.debug('RedisCacheService initialized', {
724 url: config.url,
725 keyPrefix: config.keyPrefix,
726 maxRetries: config.maxRetries,
727 connectTimeout: config.connectTimeout
728 });
729 }
730
731 public static getInstance(config: RedisCacheConfig): RedisCacheService {
732 if (!RedisCacheService.instance) {
733 if (!config) {
734 throw new Error('Redis configuration is required');
735 }
736 RedisCacheService.instance = new RedisCacheService(config);
737 }
738 return RedisCacheService.instance;
739 }
740
741 /**
742 * Connect to Redis server (no-op - ioredis auto-connects)
743 */
744 public async connect(): Promise<void> {
745 // No-op - ioredis connects automatically on first use
746 }
747
748 /**
749 * Disconnect from Redis server
750 */
751 public async disconnect(): Promise<void> {
752 await this.redis.quit();
753 this.logger.info('Disconnected from Redis server');
754 }
755
756 /**
757 * Check if connected to Redis
758 */
759 public isConnected(): boolean {
760 return this.redis.status === 'ready';
761 }
762
763 /**
764 * Get a value from cache
765 */
766 public async get<T>(key: string): Promise<T | null> {
767 try {
768 const value = await this.redis.get(key);
769
770 if (value === null) {
771 this.missCount++;
772 return null;
773 }
774
775 this.hitCount++;
776 return JSON.parse(value) as T;
777 } catch (error) {
778 this.logger.error(`Failed to get key ${key}:`, error);
779 this.missCount++;
780 return null;
781 }
782 }
783
784 /**
785 * Set a value in cache with TTL
786 */
787 public async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
788 try {
789 const serializedValue = JSON.stringify(value);
790 const ttl = ttlSeconds || this.defaultTTL;
791
792 await this.redis.setex(key, ttl, serializedValue);
793 this.logger.debug(`Cache set: ${key}`, { ttl });
794 } catch (error) {
795 this.logger.error(`Failed to set key ${key}:`, error);
796 throw error;
797 }
798 }
799
800 /**
801 * Delete a value from cache
802 */
803 public async delete(key: string): Promise<boolean> {
804 try {
805 const result = await this.redis.del(key);
806 const deleted = result > 0;
807
808 if (deleted) {
809 this.logger.debug(`Cache delete: ${key}`);
810 }
811
812 return deleted;
813 } catch (error) {
814 this.logger.error(`Failed to delete key ${key}:`, error);
815 throw error;
816 }
817 }
818
819 /**
820 * Delete all cache entries matching a pattern
821 */
822 public async deletePattern(pattern: string): Promise<number> {
823 try {
824 let deletedCount = 0;
825 let cursor = '0';
826
827 // Add the key prefix to the pattern for scanning
828 const prefix = this.config.keyPrefix || '';
829 const fullPattern = prefix + pattern;
830
831 do {
832 const reply = await this.redis.call('scan', cursor, 'MATCH', fullPattern, 'COUNT', 100) as [string, string[]];
833 cursor = reply[0];
834 const keys = reply[1];
835
836 if (keys.length > 0) {
837 // Remove the prefix from keys before deleting since del() will add it back
838 const unprefixedKeys = keys.map(key => key.slice(prefix.length));
839 const deleteResult = await this.redis.del(...unprefixedKeys);
840 deletedCount += deleteResult;
841 }
842 } while (cursor !== '0');
843
844 this.logger.debug(`Cache deletePattern: ${pattern} - ${deletedCount} keys deleted`);
845 return deletedCount;
846 } catch (error) {
847 this.logger.error(`Failed to delete pattern ${pattern}:`, error);
848 throw error;
849 }
850 }
851
852 /**
853 * Clear all cache entries (with key prefix)
854 */
855 public async flush(): Promise<void> {
856 try {
857 const prefix = this.config.keyPrefix || '';
858 let deletedCount = 0;
859 let cursor = '0';
860
861 do {
862 const reply = await this.redis.call('scan', cursor, 'MATCH', `${prefix}*`, 'COUNT', 100) as [string, string[]];
863 cursor = reply[0];
864 let values = reply[1];
865
866 if (values.length === 0) break;
867
868 for (let i = 0; i < values.length; i++) {
869 const keyWithoutPrefix = values[i].startsWith(prefix) ? values[i].substring(prefix.length) : values[i];
870 let delResult = await this.redis.call('del', keyWithoutPrefix);
871 this.logger.debug(`Deleted key: ${keyWithoutPrefix}, result: ${delResult}`);
872 }
873
874 deletedCount += values.length;
875 } while (cursor !== '0');
876
877 this.logger.info(`Cache flushed: ${deletedCount} entries removed`);
878 } catch (error) {
879 this.logger.error('Failed to flush cache:', error);
880 throw error;
881 }
882 }
883
884 /**
885 * Get multiple values from cache
886 */
887 public async mget<T>(keys: string[]): Promise<(T | null)[]> {
888 try {
889 const values = await this.redis.mget(...keys);
890
891 return values.map((value: string | null) => {
892 if (value === null) {
893 this.missCount++;
894 return null;
895 }
896
897 try {
898 this.hitCount++;
899 return JSON.parse(value) as T;
900 } catch (error) {
901 this.logger.error('Failed to parse cached value:', error);
902 this.missCount++;
903 return null;
904 }
905 });
906 } catch (error) {
907 this.logger.error('Failed to get multiple keys:', error);
908 // Return array of nulls on error
909 return new Array(keys.length).fill(null);
910 }
911 }
912
913 /**
914 * Set multiple values in cache
915 */
916 public async mset<T>(entries: CacheEntry<T>[]): Promise<void> {
917 try {
918 const pipeline = this.redis.pipeline();
919
920 for (const entry of entries) {
921 const serializedValue = JSON.stringify(entry.value);
922 const ttl = entry.ttlSeconds || this.defaultTTL;
923 pipeline.setex(entry.key, ttl, serializedValue);
924 }
925
926 await pipeline.exec();
927 this.logger.debug(`Cache mset: ${entries.length} entries`);
928 } catch (error) {
929 this.logger.error('Failed to set multiple keys:', error);
930 throw error;
931 }
932 }
933
934 /**
935 * Get cache statistics
936 */
937 public async getStats(): Promise<CacheStats> {
938 try {
939 const info = await this.redis.info('memory');
940 const totalRequests = this.hitCount + this.missCount;
941
942 // Handle keyPrefix correctly when getting keys using SCAN
943 const prefix = this.config.keyPrefix || '';
944 let keys: string[] = [];
945 let cursor = '0';
946
947 if (prefix) {
948 // Use SCAN with prefix
949 do {
950 const result = await this.redis.call('scan', cursor, 'MATCH', `${prefix}*`, 'COUNT', '100') as [string, string[]];
951 cursor = result[0];
952 keys.push(...result[1]);
953 // Limit total keys for performance
954 if (keys.length > 300) break;
955 } while (cursor !== '0');
956 } else {
957 // Use normal scan
958 do {
959 const result = await this.redis.scan(cursor, 'MATCH', '*', 'COUNT', '100');
960 cursor = result[0];
961 keys.push(...result[1]);
962
963 // Limit total keys for performance
964 if (keys.length > 300) break;
965 } while (cursor !== '0');
966 }
967
968 // Parse memory usage from Redis INFO command
969 const memoryMatch = info.match(/used_memory:(\d+)/);
970 const memoryUsage = memoryMatch ? parseInt(memoryMatch[1]) : undefined;
971
972 return {
973 size: keys.length,
974 keys: keys.slice(0, 100), // Limit to first 100 keys for display
975 hitRate: totalRequests > 0 ? this.hitCount / totalRequests : 0,
976 missRate: totalRequests > 0 ? this.missCount / totalRequests : 0,
977 connectionStatus: this.redis.status === 'ready' ? 'connected' : 'disconnected',
978 memoryUsage
979 };
980 } catch (error) {
981 this.logger.error('Failed to get cache stats:', error);
982 return {
983 size: 0,
984 keys: [],
985 hitRate: 0,
986 missRate: 0,
987 connectionStatus: 'disconnected'
988 };
989 }
990 }
991
992 /**
993 * Check if the cache service is healthy
994 */
995 public async isHealthy(): Promise<boolean> {
996 try {
997 // Simple ping to check connection
998 await this.redis.ping();
999 return true;
1000 } catch (error) {
1001 this.logger.error('Health check failed:', error);
1002 return false;
1003 }
1004 }
1005
1006 /**
1007 * Cleanup resources and disconnect
1008 */
1009 public async destroy(): Promise<void> {
1010 await this.disconnect();
1011 this.hitCount = 0;
1012 this.missCount = 0;
1013 }
1014
1015 /**
1016 * Get the metadata for this service
1017 */
1018 public getMetadata(): ServiceMetadata {
1019 return RedisCacheService.metadata;
1020 }
1021
1022 /**
1023 * Initialize the service with optional configuration
1024 */
1025 public initialize(config?: Record<string, any>): void {
1026 if (config) {
1027 this.validateConfiguration(config);
1028 }
1029 }
1030
1031 /**
1032 * Get available operations for this service
1033 */
1034 public async getOperations(): Promise<ServiceOperation[]> {
1035 const stats = await this.getStats();
1036 const operations: ServiceOperation[] = [];
1037
1038 // Show cache statistics in description
1039 const cacheInfo = `Currently storing ${stats.size || 0} keys, using ${
1040 stats.memoryUsage ? (stats.memoryUsage / 1024 / 1024).toFixed(2) + ' MB' : 'unknown amount of'
1041 } memory`;
1042
1043 // Flush operation - always available but shows current cache size
1044 operations.push({
1045 name: 'flush',
1046 displayName: 'Flush Cache',
1047 description: `Clear all cache entries with the service prefix. ${cacheInfo}`,
1048 category: 'maintenance',
1049 icon: 'trash',
1050 buttonStyle: 'danger',
1051 dangerous: true,
1052 confirmationMessage: `Are you sure you want to flush the cache? This will remove ${stats.size || 0} cached entries and cannot be undone.`,
1053 parameters: []
1054 });
1055
1056 // Inspect cache operation
1057 operations.push({
1058 name: 'inspect',
1059 displayName: 'Inspect Cache',
1060 description: 'View detailed information about cached keys and their values',
1061 category: 'diagnostics',
1062 icon: 'search',
1063 buttonStyle: 'primary',
1064 parameters: [
1065 {
1066 name: 'pattern',
1067 type: 'string',
1068 displayName: 'Key Pattern',
1069 description: 'Redis pattern to filter keys (e.g., "user:*", "*config*")',
1070 required: false,
1071 defaultValue: '*'
1072 },
1073 {
1074 name: 'limit',
1075 type: 'number',
1076 displayName: 'Max Keys',
1077 description: 'Maximum number of keys to display',
1078 required: false,
1079 defaultValue: 20,
1080 validation: {
1081 min: 1,
1082 max: 100
1083 }
1084 }
1085 ]
1086 });
1087
1088 return operations;
1089 }
1090
1091 /**
1092 * Execute a custom operation
1093 */
1094 public async executeOperation(operationName: string, parameters?: Record<string, any>): Promise<OperationResult> {
1095 switch (operationName) {
1096 case 'flush':
1097 return this.executeFlushOperation();
1098
1099 case 'inspect':
1100 return this.executeInspectOperation(parameters);
1101
1102 default:
1103 return {
1104 success: false,
1105 error: `Unknown operation: ${operationName}`
1106 };
1107 }
1108 }
1109
1110 /**
1111 * Execute flush operation
1112 */
1113 private async executeFlushOperation(): Promise<OperationResult> {
1114 try {
1115 const startStats = await this.getStats();
1116 await this.flush();
1117 const endStats = await this.getStats();
1118 const keysRemoved = (startStats?.size || 0) - (endStats?.size || 0);
1119
1120 return {
1121 success: true,
1122 message: `Cache flushed successfully. Removed ${keysRemoved} entries.`,
1123 data: {
1124 keysRemoved,
1125 keysBefore: startStats.size,
1126 keysAfter: endStats.size,
1127 }
1128 };
1129 } catch (error) {
1130 return {
1131 success: false,
1132 error: `Failed to flush cache: ${error instanceof Error ? error.message : String(error)}`
1133 };
1134 }
1135 }
1136
1137 /**
1138 * Execute inspect operation
1139 */
1140 private async executeInspectOperation(parameters?: Record<string, any>): Promise<OperationResult> {
1141 try {
1142 const pattern = parameters?.pattern || '*';
1143 const limit = Math.min(Math.max(parameters?.limit || 20, 1), 100);
1144 const prefix = this.config.keyPrefix || '';
1145
1146 // Use SCAN to get keys
1147 let allKeys: string[] = [];
1148 let displayKeys: string[] = [];
1149 let cursor = '0';
1150
1151 if (prefix) {
1152 const prefixedPattern = pattern === '*' ? `${prefix}*` : `${prefix}${pattern}`;
1153
1154 do {
1155 const result = await this.redis.call('scan', cursor, 'MATCH', prefixedPattern, 'COUNT', '100') as [string, string[]];
1156 cursor = result[0];
1157 const foundKeys = result[1];
1158
1159 for (const key of foundKeys) {
1160 allKeys.push(key);
1161 displayKeys.push(key.startsWith(prefix) ? key.substring(prefix.length) : key);
1162 }
1163
1164 if (allKeys.length >= limit * 2) break;
1165 } while (cursor !== '0');
1166 } else {
1167 do {
1168 const result = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', '100');
1169 cursor = result[0];
1170 const foundKeys = result[1];
1171
1172 allKeys.push(...foundKeys);
1173 displayKeys.push(...foundKeys);
1174
1175 if (allKeys.length >= limit * 2) break;
1176 } while (cursor !== '0');
1177 }
1178
1179 const keys = allKeys.slice(0, limit);
1180 const keysForDisplay = displayKeys.slice(0, limit);
1181
1182 const keyDetails: Array<{
1183 key: string;
1184 type: string;
1185 ttl: number;
1186 size?: number;
1187 value?: any;
1188 }> = [];
1189
1190 for (let i = 0; i < keysForDisplay.length; i++) {
1191 const actualKey = keysForDisplay[i];
1192 const displayKey = keysForDisplay[i];
1193
1194 try {
1195 let type: string;
1196 let ttl: number;
1197 let rawValue: string | null = null;
1198
1199 if (prefix) {
1200 type = await this.redis.call('type', actualKey) as string;
1201 ttl = await this.redis.call('ttl', actualKey) as number;
1202
1203 if (type === 'string') {
1204 rawValue = await this.redis.call('get', actualKey) as string | null;
1205 }
1206 } else {
1207 type = await this.redis.type(actualKey);
1208 ttl = await this.redis.ttl(actualKey);
1209
1210 if (type === 'string') {
1211 rawValue = await this.redis.get(actualKey);
1212 }
1213 }
1214
1215 let value: any;
1216 let size: number | undefined;
1217
1218 if (rawValue) {
1219 size = rawValue.length;
1220 try {
1221 value = JSON.parse(rawValue);
1222 // Truncate large objects
1223 if (JSON.stringify(value).length > 500) {
1224 value = { _truncated: true, preview: JSON.stringify(value).substring(0, 500) + '...' };
1225 }
1226 } catch {
1227 // Not JSON, show as string (truncated if needed)
1228 value = rawValue.length > 200 ? rawValue.substring(0, 200) + '...' : rawValue;
1229 }
1230 } else if (type !== 'string') {
1231 value = `<${type} type>`;
1232 }
1233
1234 keyDetails.push({
1235 key: displayKey,
1236 type,
1237 ttl,
1238 size,
1239 value
1240 });
1241 } catch (err) {
1242 keyDetails.push({
1243 key: displayKey,
1244 type: 'error',
1245 ttl: -1,
1246 value: `<error accessing key: ${err instanceof Error ? err.message : String(err)}>`
1247 });
1248 }
1249 }
1250
1251 return {
1252 success: true,
1253 message: `Found ${keyDetails.length} keys matching pattern "${pattern}"`,
1254 data: {
1255 pattern,
1256 totalMatches: allKeys.length,
1257 displayed: keyDetails.length,
1258 keyPrefix: prefix,
1259 keys: keyDetails
1260 }
1261 };
1262 } catch (error) {
1263 return {
1264 success: false,
1265 error: `Failed to inspect cache: ${error instanceof Error ? error.message : String(error)}`
1266 };
1267 }
1268 }
1269}
Metadata
- Path
- utaba/main/services/cache/RedisCacheService.ts
- Namespace
- utaba/main/services/cache
- Author
- utaba
- Category
- services
- Technology
- typescript
- Contract Version
- 1.0.0
- MIME Type
- application/typescript
- Published
- 18-Jul-2025
- Last Updated
- 18-Jul-2025