UNPKG

@jonaskahn/maestro

Version:

Job orchestration made simple for Node.js message workflows

296 lines (265 loc) 9.5 kB
/** * @license * Copyleft (c) 2025 Jonas Kahn. All rights are not reserved. * * This source code is licensed under the MIT License found in the * LICENSE file in the root directory of this source tree. * * Cache Client Factory * * Factory service for creating and configuring appropriate cache implementations. * Supports multiple cache providers with fallback mechanisms, environment-based configuration, * and validation to ensure proper cache setup. Currently supports Redis with planned * implementations for Memcached and in-memory caching. */ const logger = require("../../services/logger-service"); /** * Cache Client Factory * * Static factory class that creates and configures cache implementations * based on provided configuration or environment variables. Handles provider * selection, configuration validation, and fallback strategies when primary * cache providers are unavailable. */ class CacheClientFactory { /** * Creates a cache client instance based on configuration * * Validates configuration, determines appropriate implementation, * and instantiates the corresponding cache client. Implements fallback * to Redis if specified implementation fails or is unsupported. * * @param {Object} config - Cache configuration object * @param {string} [config.implementation] - Cache implementation type ('redis', 'memcached', 'memory') * @param {string} config.keyPrefix - Key prefix for organization * @param {number} [config.processingTtl] - Processing key TTL in milliseconds * @param {number} [config.suppressionTtl] - Suppression key TTL in milliseconds * @param {Object} [config.connectionOptions] - Implementation-specific connection options * @returns {AbstractCache} Cache layer instance * @throws {Error} When cache creation fails or configuration is invalid */ static createClient(config = {}) { try { this.validateConfiguration(config); const implementation = this._resolveImplementation(config); switch (implementation.toLowerCase()) { case "redis": return this._createRedisClient(config); default: logger.logWarning(`Unsupported cache implementation: ${implementation}. Falling back to Redis.`); return this._createRedisClient({ ...config, implementation: "redis", }); } } catch (error) { logger.logError("Failed to create cache _client", error); if (config.implementation !== "redis") { logger.logWarning("Attempting fallback to Redis cache..."); try { return this._createRedisClient({ ...config, implementation: "redis", }); } catch (fallbackError) { logger.logError("Redis fallback also failed", fallbackError); throw new Error( `Cache client creation failed: ${error.message}. Fallback to Redis also failed: ${fallbackError.message}` ); } } throw error; } } /** * Resolves cache implementation from config and environment * @param {Object} config - Configuration object * @returns {string} Implementation type * @private */ static _resolveImplementation(config) { return config.implementation || process.env.MO_CACHE_IMPLEMENTATION || "redis"; } /** * Creates Redis cache client * @param {Object} config - Configuration object * @returns {RedisCacheClient} Redis cache layer instance * @private */ static _createRedisClient(config) { const RedisCacheClient = require("./redis-cache-client"); try { return new RedisCacheClient({ ...config, implementation: "redis", }); } catch (error) { logger.logError("Failed to create Redis cache _client", error); throw new Error(`Redis cache client creation failed: ${error.message}`); } } /** * Creates Memcached client * @param {Object} config - Configuration object * @returns {MemcachedCacheClient} Memcached cache layer instance * @private */ static _createMemcachedClient(config) { const MemcachedCacheClient = require("./memcached-cache-_client"); try { return new MemcachedCacheClient({ ...config, implementation: "memcached", }); } catch (error) { logger.logError("Failed to create Memcached cache _client", error); throw new Error(`Memcached cache client creation failed: ${error.message}`); } } /** * Creates Memory cache client * @param {Object} config - Configuration object * @returns {MemoryCacheClient} Memory cache layer instance * @private */ static _createMemoryClient(config) { const MemoryCacheClient = require("./memory-cache-_client"); try { logger.logWarning( "Memory cache is for development/testing only. Not suitable for production multi-instance deployments." ); return new MemoryCacheClient({ ...config, implementation: "memory", }); } catch (error) { logger.logError("Failed to create Memory cache _client", error); throw new Error(`Memory cache client creation failed: ${error.message}`); } } /** * Validates cache configuration against required fields and constraints * * Ensures the configuration has all required fields, validates TTL values, * and checks for supported implementations. Issues warnings for unsupported * implementations but doesn't throw errors to allow fallback behavior. * * @param {Object} config - Configuration to validate * @throws {Error} If configuration is invalid */ static validateConfiguration(config) { if (!config || typeof config !== "object") { throw new Error("Cache configuration must be an object"); } if (!config.keyPrefix || typeof config.keyPrefix !== "string") { throw new Error("Cache configuration must include a keyPrefix string"); } if (config.keyPrefix.trim().length === 0) { throw new Error("Cache keyPrefix cannot be empty"); } const implementation = this._resolveImplementation(config); const supportedImplementations = this.getSupportedImplementations(); if (!supportedImplementations.includes(implementation.toLowerCase())) { logger.logWarning( `Unsupported cache implementation: ${implementation}. ` + `Supported implementations: ${supportedImplementations.join(", ")}` ); } if (config.processingTtl !== undefined) { this._validateTtl(config.processingTtl, "processingTtl"); } if (config.suppressionTtl !== undefined) { this._validateTtl(config.suppressionTtl, "suppressionTtl"); } if (config.connectionOptions && typeof config.connectionOptions !== "object") { throw new Error("Cache connectionOptions must be an object"); } } /** * Validates TTL value for proper range and type * @param {any} ttl - TTL value to validate * @param {string} fieldName - Name of the field for error messages * @private */ static _validateTtl(ttl, fieldName) { if (typeof ttl !== "number" || ttl <= 0 || !Number.isInteger(ttl)) { throw new Error(`Cache ${fieldName} must be a positive integer (milliseconds)`); } if (ttl > 86400 * 30 * 1000) { logger.logWarning( `Cache ${fieldName} is quite high (${ttl} milliseconds = ${Math.round(ttl / (86400 * 1000))} days). Consider if this is intentional.` ); } } /** * Gets list of supported cache implementations * @returns {Array<string>} Array of supported implementation names */ static getSupportedImplementations() { return ["redis", "memcached", "memory"]; } /** * Checks if implementation is supported * @param {string} implementation - Implementation name to check * @returns {boolean} True if implementation is supported */ static isImplementationSupported(implementation) { if (typeof implementation !== "string") { return false; } return this.getSupportedImplementations().includes(implementation.toLowerCase()); } /** * Gets default configuration for specified implementation * * Provides sensible defaults for each supported cache implementation, * including key prefixes, TTLs, and connection options. * * @param {string} implementation - Implementation type * @returns {Object} Default configuration object */ static getDefaultConfig(implementation = "redis") { const baseConfig = { keyPrefix: "MO_DEFAULT", processingTtl: 10 * 1000, suppressionTtl: 20 * 1000, retryOptions: { retries: 3, retryDelay: 1000, }, }; const implementationDefaults = { redis: { implementation: "redis", connectionOptions: { url: "redis://localhost:6379", db: 0, maxRetriesPerRequest: 3, lazyConnect: true, }, }, memcached: { implementation: "memcached", connectionOptions: { servers: "localhost:11211", options: { timeout: 3000, retries: 2, }, }, }, memory: { implementation: "memory", connectionOptions: { maxSize: 1000, defaultTtl: 3600 * 1000, }, }, }; return { ...baseConfig, ...implementationDefaults[implementation.toLowerCase()], }; } } module.exports = CacheClientFactory;