UNPKG

@flowcore/generator-library-template

Version:

A Yeoman generator for adding flowcore library components to a typescript service or application

261 lines (229 loc) 7.95 kB
import { createLogger } from "@/logger.builder" import Redis from "ioredis" /** * Configuration options for Redis connection. */ export type RedisConfiguration = { /** The Redis connection URL */ redisUrl: string /** Optional name for the Redis instance (used for logging) */ name?: string /** Maximum delay between retry attempts in milliseconds */ maxRetryDelay?: number /** Maximum number of retry attempts */ maxRetries?: number /** Connection timeout in milliseconds */ connectionTimeout?: number /** Command execution timeout in milliseconds */ commandTimeout?: number } /** * Creates a new Redis client instance based on the provided configuration. * Supports both standard Redis and Redis Sentinel connections. * * @param options - The Redis configuration options * @param options.redisUrl - The Redis connection URL * @param options.name - Optional name for the Redis instance * @param options.maxRetryDelay - Maximum delay between retry attempts * @param options.maxRetries - Maximum number of retry attempts * @param options.connectionTimeout - Connection timeout * @param options.commandTimeout - Command execution timeout * @returns A configured Redis client instance */ export const redisFactory = ({ redisUrl, name, maxRetryDelay, maxRetries, connectionTimeout, commandTimeout, }: RedisConfiguration): Redis => { const logger = createLogger(name ?? "redis") let redis: Redis if (redisUrl.startsWith("redis+sentinel://")) { const [credentialsAndHost, masterSet] = redisUrl.split("//")[1].split("/") let credentials: string | undefined let hostInfo: string | undefined let port: string | undefined if (credentialsAndHost.includes("@")) { ;[credentials, hostInfo] = credentialsAndHost.split("@") } else { ;[hostInfo, port] = credentialsAndHost.split(":") } let username: string | undefined let password: string | undefined let host: string if (credentials?.includes(":")) { ;[username, password] = credentials.split(":") ;[host, port] = hostInfo.split(":") } else { ;[host, port] = credentialsAndHost.split(":") } redis = redisSentinelFactory({ sentinelUrl: host, port: Number.parseInt(port), username, password, setName: masterSet, maxRetryDelay, maxRetries, connectionTimeout, commandTimeout, }) } else { redis = new Redis(redisUrl, { retryStrategy: (times) => { if (times > (maxRetries ?? 10)) { return null } const maxDelay = maxRetryDelay ?? 2000 const delay = Math.min(times * 50, maxDelay) return delay }, reconnectOnError: (err) => { const targetError = "READONLY" return err.message.includes(targetError) }, maxRetriesPerRequest: maxRetries ?? 10, connectTimeout: connectionTimeout ?? 20000, commandTimeout: commandTimeout ?? 5000, }) } // Connection events redis.on("connect", () => { logger.info("Connected to Redis server") }) redis.on("ready", () => { logger.info("Redis client is ready to handle commands") }) redis.on("error", (error) => { logger.error(`Redis failed with ${error.message}`, { error }) }) redis.on("close", () => { logger.info("Connection to Redis server was closed") }) redis.on("reconnecting", (timeToReconnect: number) => { logger.info(`Reconnecting to Redis in ${timeToReconnect}ms...`) }) redis.on("end", () => { logger.info("Redis client connection ended") }) // Sentinel-specific events redis.on("+node", (node) => { logger.debug("New node discovered:", node) }) redis.on("-node", (node) => { logger.debug("Node removed:", node) }) redis.on("+sdown", (node) => { logger.warn("Node is subjectively down:", node) }) redis.on("-sdown", (node) => { logger.warn("Node is subjectively up:", node) }) redis.on("+failover-start", () => { logger.warn("Failover process has started") }) redis.on("+failover-end", () => { logger.warn("Failover process has completed") }) return redis } /** * Creates a Redis client configured for Sentinel mode. * * @param options - The Redis Sentinel configuration options * @param options.sentinelUrl - The URL of the Sentinel server * @param options.port - The port number for the Sentinel server * @param options.username - Optional username for authentication * @param options.password - Optional password for authentication * @param options.setName - The name of the Sentinel set * @param options.maxRetryDelay - Maximum delay between retry attempts * @param options.maxRetries - Maximum number of retry attempts * @param options.connectionTimeout - Connection timeout * @param options.commandTimeout - Command execution timeout * @returns A configured Redis client instance in Sentinel mode */ export const redisSentinelFactory = (options: { sentinelUrl: string port?: number username?: string password?: string setName?: string maxRetryDelay?: number maxRetries?: number connectionTimeout?: number commandTimeout?: number }) => { const configuration = { ...options, setName: options.setName ?? "mymaster", } return new Redis({ sentinels: [{ host: configuration.sentinelUrl, port: configuration.port ?? 26379 }], retryStrategy: (times) => { if (times > (configuration.maxRetries ?? 10)) { return null } const maxDelay = configuration.maxRetryDelay ?? 2000 const delay = Math.min(times * 50, maxDelay) return delay }, reconnectOnError: (err) => { const targetError = "READONLY" return err.message.includes(targetError) }, maxRetriesPerRequest: configuration.maxRetries ?? 10, connectTimeout: configuration.connectionTimeout ?? 20000, commandTimeout: configuration.commandTimeout ?? 5000, ...(configuration.password && { sentinelPassword: configuration.password }), ...(configuration.password && { password: configuration.password }), ...(configuration.username && { username: configuration.username }), name: configuration.setName, }) } /** * Stores Redis client instances for reuse */ const redisPool = new Map<string, Redis>() /** * Stores Redis configurations for each connection URL */ const redisConfigurationCache = new Map<string, RedisConfiguration>() const poolLogger = createLogger("redis-pool-factory") /** * Configures a Redis instance with the specified options. * If an instance with the same URL already exists, it won't be reconfigured unless override is true. * * @param options - The Redis configuration options * @param override - Whether to override existing configuration */ export const configureRedisInstance = (options: RedisConfiguration, override?: boolean) => { if (redisConfigurationCache.has(options.redisUrl) && !override) { poolLogger.warn("Redis configuration already exists", { redisUrl: options.redisUrl }) return } redisConfigurationCache.set(options.redisUrl, options) } /** * Retrieves a Redis instance for the specified URL. * If an instance doesn't exist, it creates one using the cached configuration. * * @param options - Object containing the Redis URL * @param options.redisUrl - The Redis connection URL * @returns A Redis client instance * @throws Error if no configuration exists for the specified URL */ export const getRedisInstance = (options: { redisUrl: string }) => { let redis = redisPool.get(options.redisUrl) if (!redis) { const configuration = redisConfigurationCache.get(options.redisUrl) if (!configuration) { poolLogger.error("Redis configuration not found", { redisUrl: options.redisUrl }) throw new Error("Redis configuration not found") } redis = redisFactory(configuration) redisPool.set(options.redisUrl, redis) } return redis }