Skip to content

Redis Factory

The Redis Factory is a library that allows you to generate redis connections from redis urls, both single and using sentinel.

Install the Redis Factory

Terminal window
flowcore component add library/redis-factory
Or install the Redis Factory manually

You can install the Redis Factory by running the following command:

Terminal window
bun add ioredis

add the Redis Factory to your projects lib folder

lib/redis-factory.ts
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
}

Usage

it is recommended to create builders in a separate file that you can import and use in your components

lib/redis-builder.ts
import env from "./env"
import { configureRedisInstance, getRedisInstance } from "./lib/redis-factory"
// single instance
const url = "redis://user:password@localhost:6379"
configureRedisInstance({ redisUrl: url, name: "cold-storage" })
export const getColdStorageRedisInstance = () => getRedisInstance({ redisUrl: url })
// sentinel instance
const sentinelUrl = "redis+sentinel://user:password@localhost:26379/mymaster"
configureRedisInstance({ redisUrl: sentinelUrl, name: "hot-storage" })
export const getHotStorageRedisInstance = () => getRedisInstance({ redisUrl: sentinelUrl })

Using a Single Redis Instance

src/index.ts
import { getColdStorageRedisInstance } from "@lib/redis-builder"
const redis = getColdStorageRedisInstance()
redis.set("key", "value")
redis.get("key")

Using Sentinel

src/index.ts
import { redisFactory } from "@lib/redis-factory"
const redis = getHotStorageRedisInstance()
redis.set("key", "value")
redis.get("key")