qmemory
Version:
A comprehensive production-ready Node.js utility library with MongoDB document operations, user ownership enforcement, Express.js HTTP utilities, environment-aware logging, and in-memory storage. Features 96%+ test coverage with comprehensive error handli
359 lines (313 loc) • 12.6 kB
JavaScript
/**
* Cache Utility Functions
* Redis-based caching with development mode bypass and fallback patterns
*
* This module provides caching functionality that automatically adapts to different
* environments - using Redis for production caching while bypassing cache in
* development for faster iteration cycles.
*
* Design philosophy:
* - Environment-aware caching: Redis in production, bypass in development
* - Graceful degradation when Redis is unavailable
* - Consistent error handling following library patterns
* - Performance optimization through intelligent cache strategies
*
* Cache strategies:
* - Simple key-value caching with TTL
* - JSON serialization for complex objects
* - Automatic fallback to direct execution on Redis errors
* - Development mode bypass for faster debugging
*/
const { logFunctionEntry, logFunctionExit, logFunctionError } = require('./logging-utils');
// Redis client instance - will be undefined in development or when Redis unavailable
let redisClient = null;
/**
* Initializes Redis client connection for caching operations
*
* This function attempts to establish a Redis connection for production caching.
* It handles connection errors gracefully and logs the connection status for monitoring.
*
* @param {Object} options - Redis connection options
* @param {string} options.host - Redis server host (default: localhost)
* @param {number} options.port - Redis server port (default: 6379)
* @param {string} options.password - Redis password if authentication required
* @param {number} options.db - Redis database number (default: 0)
* @returns {Promise<boolean>} True if connection successful, false otherwise
*/
async function initializeRedisClient(options = {}) {
logFunctionEntry('initializeRedisClient', { options });
// Skip Redis initialization in development mode for faster startup
if (process.env.NODE_ENV === 'development') {
console.log('[INFO] Development mode detected, skipping Redis initialization');
logFunctionExit('initializeRedisClient', false);
return false;
}
try {
// Dynamically import redis to avoid requiring it in development
const redis = require('redis');
const clientOptions = {
host: options.host || process.env.REDIS_HOST || 'localhost',
port: options.port || process.env.REDIS_PORT || 6379,
password: options.password || process.env.REDIS_PASSWORD,
db: options.db || process.env.REDIS_DB || 0,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
...options
};
redisClient = redis.createClient(clientOptions);
// Handle Redis connection events for monitoring
redisClient.on('connect', () => {
console.log('[INFO] Redis client connected successfully');
});
redisClient.on('error', (error) => {
console.warn('[WARN] Redis connection error:', error.message);
redisClient = null; // Reset client on error to trigger fallback
});
redisClient.on('end', () => {
console.log('[INFO] Redis connection closed');
redisClient = null;
});
await redisClient.connect();
console.log('[INFO] Redis cache initialized and ready');
logFunctionExit('initializeRedisClient', true);
return true;
} catch (error) {
logFunctionError('initializeRedisClient', error, { options });
console.warn('[WARN] Failed to initialize Redis cache, caching will be disabled:', error.message);
redisClient = null;
return false;
}
}
/**
* Disconnects Redis client gracefully
*
* Properly closes Redis connection to prevent connection leaks.
* Safe to call multiple times or when client is not connected.
*
* @returns {Promise<void>}
*/
async function disconnectRedis() {
logFunctionEntry('disconnectRedis');
if (redisClient) {
try {
await redisClient.quit();
console.log('[INFO] Redis client disconnected gracefully');
} catch (error) {
console.warn('[WARN] Error during Redis disconnect:', error.message);
} finally {
redisClient = null;
}
}
logFunctionExit('disconnectRedis');
}
/**
* Caches a function's result using Redis (in production) or bypasses cache (in development)
*
* This function provides intelligent caching that adapts to the environment:
* - Production: Uses Redis for persistent caching with TTL
* - Development: Bypasses cache for faster iteration and debugging
* - Fallback: Executes function directly if Redis is unavailable
*
* @param {string} key - Unique cache key for storing the result
* @param {number} ttl - Time-to-live in seconds for cache expiration
* @param {Function} fn - Async function to execute and cache the result
* @returns {Promise<*>} The cached result or fresh execution result
*
* @example
* // Cache expensive database query for 5 minutes
* const users = await withCache('users:active', 300, async () => {
* return await User.find({ status: 'active' });
* });
*
* @example
* // Cache API response for 1 hour with dynamic key
* const weather = await withCache(`weather:${city}`, 3600, async () => {
* return await fetchWeatherAPI(city);
* });
*/
async function withCache(key, ttl, fn) {
logFunctionEntry('withCache', { key, ttl, hasFn: typeof fn === 'function' });
// Validate input parameters
if (typeof key !== 'string' || !key.trim()) {
const error = new Error('Cache key must be a non-empty string');
logFunctionError('withCache', error, { key, ttl });
throw error;
}
if (typeof ttl !== 'number' || ttl <= 0) {
const error = new Error('TTL must be a positive number');
logFunctionError('withCache', error, { key, ttl });
throw error;
}
if (typeof fn !== 'function') {
const error = new Error('Function parameter must be a callable function');
logFunctionError('withCache', error, { key, ttl });
throw error;
}
// In development mode, always execute function without caching for faster iteration
if (process.env.NODE_ENV === 'development' || !redisClient) {
console.log(`[DEBUG] Cache bypass for key: ${key} (development mode or Redis unavailable)`);
const result = await fn();
logFunctionExit('withCache', { cached: false, hasResult: result !== undefined });
return result;
}
try {
// Attempt to retrieve cached result
console.log(`[DEBUG] Checking cache for key: ${key}`);
const cached = await redisClient.get(key);
if (cached !== null) {
console.log(`[DEBUG] Cache hit for key: ${key}`);
try {
const parsedResult = JSON.parse(cached);
logFunctionExit('withCache', { cached: true, hasResult: parsedResult !== undefined });
return parsedResult;
} catch (parseError) {
console.warn(`[WARN] Failed to parse cached data for key ${key}, executing fresh:`, parseError.message);
// Continue to execute function fresh if parsing fails
}
} else {
console.log(`[DEBUG] Cache miss for key: ${key}`);
}
// Execute function and cache the result
console.log(`[DEBUG] Executing function for key: ${key}`);
const result = await fn();
// Cache the result with TTL
try {
const serializedResult = JSON.stringify(result);
await redisClient.setEx(key, ttl, serializedResult);
console.log(`[DEBUG] Cached result for key: ${key} with TTL: ${ttl}s`);
} catch (cacheError) {
console.warn(`[WARN] Failed to cache result for key ${key}:`, cacheError.message);
// Continue execution even if caching fails
}
logFunctionExit('withCache', { cached: false, hasResult: result !== undefined });
return result;
} catch (error) {
// Log Redis errors but don't fail the operation
logFunctionError('withCache', error, { key, ttl });
console.warn(`[WARN] Redis cache error for key ${key}, falling back to direct execution:`, error.message);
// Fallback to direct function execution
const result = await fn();
logFunctionExit('withCache', { cached: false, fallback: true, hasResult: result !== undefined });
return result;
}
}
/**
* Invalidates a specific cache key or pattern
*
* Removes cached data to force fresh execution on next access.
* Supports both single key deletion and pattern-based deletion.
*
* @param {string} keyOrPattern - Cache key or pattern to invalidate
* @param {boolean} isPattern - Whether the key is a pattern (uses KEYS command)
* @returns {Promise<number>} Number of keys deleted
*
* @example
* // Invalidate specific cache key
* await invalidateCache('users:active');
*
* @example
* // Invalidate all weather cache entries
* await invalidateCache('weather:*', true);
*/
async function invalidateCache(keyOrPattern, isPattern = false) {
logFunctionEntry('invalidateCache', { keyOrPattern, isPattern });
if (process.env.NODE_ENV === 'development' || !redisClient) {
console.log(`[DEBUG] Cache invalidation skipped: ${keyOrPattern} (development mode or Redis unavailable)`);
logFunctionExit('invalidateCache', 0);
return 0;
}
try {
let deletedCount = 0;
if (isPattern) {
// Find keys matching pattern and delete them
const keys = await redisClient.keys(keyOrPattern);
if (keys.length > 0) {
deletedCount = await redisClient.del(keys);
console.log(`[DEBUG] Invalidated ${deletedCount} cache keys matching pattern: ${keyOrPattern}`);
} else {
console.log(`[DEBUG] No cache keys found matching pattern: ${keyOrPattern}`);
}
} else {
// Delete single key
deletedCount = await redisClient.del(keyOrPattern);
if (deletedCount > 0) {
console.log(`[DEBUG] Invalidated cache key: ${keyOrPattern}`);
} else {
console.log(`[DEBUG] Cache key not found: ${keyOrPattern}`);
}
}
logFunctionExit('invalidateCache', deletedCount);
return deletedCount;
} catch (error) {
logFunctionError('invalidateCache', error, { keyOrPattern, isPattern });
console.warn(`[WARN] Failed to invalidate cache ${keyOrPattern}:`, error.message);
return 0;
}
}
/**
* Gets cache statistics and health information
*
* Provides insights into cache performance, connection status, and Redis server info.
* Useful for monitoring and debugging cache behavior.
*
* @returns {Promise<Object>} Cache statistics object
*/
async function getCacheStats() {
logFunctionEntry('getCacheStats');
const stats = {
connected: false,
environment: process.env.NODE_ENV || 'development',
redisAvailable: !!redisClient
};
if (process.env.NODE_ENV === 'development') {
stats.message = 'Caching disabled in development mode';
logFunctionExit('getCacheStats', stats);
return stats;
}
if (!redisClient) {
stats.message = 'Redis client not initialized';
logFunctionExit('getCacheStats', stats);
return stats;
}
try {
// Test Redis connection
await redisClient.ping();
stats.connected = true;
// Get Redis server info
const info = await redisClient.info();
const lines = info.split('\r\n');
// Parse relevant Redis metrics
for (const line of lines) {
if (line.startsWith('used_memory_human:')) {
stats.memoryUsage = line.split(':')[1];
}
if (line.startsWith('connected_clients:')) {
stats.connectedClients = parseInt(line.split(':')[1]);
}
if (line.startsWith('total_commands_processed:')) {
stats.totalCommands = parseInt(line.split(':')[1]);
}
}
stats.message = 'Redis cache operational';
} catch (error) {
logFunctionError('getCacheStats', error);
stats.error = error.message;
stats.message = 'Redis cache error';
}
logFunctionExit('getCacheStats', stats);
return stats;
}
// Export cache utilities using object shorthand for clean module interface
// This pattern allows for easy extension with additional cache utilities
// while maintaining a simple import structure for consumers
module.exports = {
withCache, // main caching function for wrapping expensive operations
initializeRedisClient, // Redis connection setup for production environments
disconnectRedis, // graceful Redis connection cleanup
invalidateCache, // cache invalidation for fresh data requirements
getCacheStats, // cache monitoring and health check utilities
// Test helper for dependency injection (only used in tests)
__setRedisClient: (client) => {
redisClient = client;
}
};