import { env } from "@config/env";
import { getLogger } from "@utils/asyncLocalStorage";
import Redis from "ioredis";
let redisClient = null;
let isConnected = false;
/**
* Establish Redis connection for caching and pub/sub use cases.
* @category Database
*/
export function connectToRedis() {
const logger = getLogger();
if (isConnected && redisClient) {
logger.info("Redis already connected");
return;
}
try {
logger.info("Attempting to connect to Redis...");
redisClient = new Redis(env.REDIS_URI, {
db: env.REDIS_DB_INDEX,
});
setupRedisEventListeners(redisClient);
isConnected = true;
logger.info("Successfully connected to Redis");
}
catch (err) {
logger.error({
error: err instanceof Error ? err.message : "Unknown error",
}, "Failed to connect to Redis");
throw err;
}
}
/**
* Internal helper that wires Redis connection lifecycle logging.
* @category Database
* @param {Redis} client Connected Redis instance.
*/
function setupRedisEventListeners(client) {
const logger = getLogger();
client.on("connect", () => {
logger.info("Redis connection established");
});
client.on("ready", () => {
isConnected = true;
logger.info("Redis client ready");
});
client.on("error", (err) => {
logger.error({ error: err.message }, "Redis connection error");
});
client.on("close", () => {
isConnected = false;
logger.warn("Redis connection closed");
});
client.on("reconnecting", () => {
logger.info("Redis reconnecting...");
});
client.on("end", () => {
isConnected = false;
logger.warn("Redis connection ended");
});
}
/**
* Gracefully close the Redis connection if active.
* @category Database
* @returns {Promise<void>} Resolves when the connection quits.
*/
export async function closeRedisConnection() {
const logger = getLogger();
if (!redisClient || !isConnected) {
logger.info("Redis not connected, skipping close");
return;
}
try {
logger.info("Closing Redis connection...");
await redisClient.quit();
isConnected = false;
redisClient = null;
logger.info("Redis connection closed successfully");
}
catch (err) {
logger.error({
error: err instanceof Error ? err.message : "Unknown error",
}, "Error closing Redis connection");
}
}
/**
* Snapshot of the Redis client status.
* @category Database
* @returns {object} High-level connection flags.
*/
export function getRedisStatus() {
return {
isConnected,
status: redisClient?.status,
};
}
/**
* Convenience helpers for interacting with Redis commands.
* @category Database
*/
export const redisUtils = {
/**
* Write a value to Redis, optionally with TTL.
* @param {string} key Redis key to store.
* @param {string} value Serialized payload.
* @param {number} [ttlSeconds] Optional TTL in seconds.
* @returns {Promise<void>} Promise that resolves when the command completes.
*/
async set(key, value, ttlSeconds) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
if (ttlSeconds) {
await redisClient.setex(key, ttlSeconds, value);
}
else {
await redisClient.set(key, value);
}
},
/**
* Fetch a value from Redis.
* @param {string} key Redis key to read.
* @returns {Promise<string | null>} Stored string or null when missing.
*/
async get(key) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.get(key);
},
/**
* Delete a Redis key.
* @param {string} key Redis key to remove.
* @returns {Promise<number>} Number of keys removed.
*/
async del(key) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.del(key);
},
/**
* Determine if a key exists in Redis.
* @param {string} key Redis key to check.
* @returns {Promise<boolean>} True when the key exists.
*/
async exists(key) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
const result = await redisClient.exists(key);
return result === 1;
},
/**
* Publish a message to a channel.
* @param {string} channel Channel name.
* @param {string} message Payload to publish.
* @returns {Promise<number>} Subscribers that received the message.
*/
async publish(channel, message) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.publish(channel, message);
},
/**
* Subscribe to a channel and invoke a callback when messages arrive.
* @param {string} channel Channel to subscribe to.
* @param {Function} callback Handler executed per message.
* @returns {Promise<void>} Resolves once subscribed.
*/
async subscribe(channel, callback) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
await redisClient.subscribe(channel);
redisClient.on("message", (receivedChannel, message) => {
if (receivedChannel === channel) {
callback(message);
}
});
},
/**
* Add one or more members to a Redis set.
* @param {string} key Redis key of the set.
* @param {...string} members Members to add.
* @returns {Promise<number>} Number of elements that were added.
*/
async sadd(key, ...members) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
if (members.length === 0)
return 0;
return await redisClient.sadd(key, ...members);
},
/**
* Remove one or more members from a Redis set.
* @param {string} key Redis key of the set.
* @param {...string} members Members to remove.
* @returns {Promise<number>} Number of elements that were removed.
*/
async srem(key, ...members) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
if (members.length === 0)
return 0;
return await redisClient.srem(key, ...members);
},
/**
* Get all members of a Redis set.
* @param {string} key Redis key of the set.
* @returns {Promise<string[]>} Array of set members.
*/
async smembers(key) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.smembers(key);
},
/**
* Set a field in a Redis hash.
* @param {string} key Redis hash key.
* @param {string} field Hash field.
* @param {string} value Serialized value.
* @returns {Promise<number>} 1 when a new field is created, 0 when overwritten.
*/
async hset(key, field, value) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
const logger = getLogger();
const result = await redisClient.hset(key, field, value);
logger.info({ key, field, value, result }, "HSET result");
return result;
},
/**
* Get a field from a Redis hash.
* @param {string} key Redis hash key.
* @param {string} field Hash field.
* @returns {Promise<string | null>} The stored value or null.
*/
async hget(key, field) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.hget(key, field);
},
/**
* Get all fields and values from a Redis hash.
* @param {string} key Redis hash key.
* @returns {Promise<Record<string, string>>} Object of field → value.
*/
async hgetall(key) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.hgetall(key);
},
/**
* Delete one or more fields from a Redis hash.
* @param {string} key Redis hash key.
* @param {...string} fields Fields to delete.
* @returns {Promise<number>} Number of fields that were removed.
*/
async hdel(key, ...fields) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
if (fields.length === 0)
return 0;
return await redisClient.hdel(key, ...fields);
},
/**
* Increment a numeric field in a Redis hash.
* @param {string} key Redis hash key.
* @param {string} field Hash field.
* @param {number} increment Increment value (can be negative).
* @returns {Promise<number>} The value of the field after the increment.
*/
async hincrby(key, field, increment) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.hincrby(key, field, increment);
},
/**
* Set a TTL (time-to-live) on a key.
* @param {string} key Redis key.
* @param {number} ttlSeconds TTL in seconds.
* @returns {Promise<boolean>} True when the timeout was set.
*/
async expire(key, ttlSeconds) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
const result = await redisClient.expire(key, ttlSeconds);
return result === 1;
},
/**
* Add a member to a sorted set with a score.
* @param {string} key Redis key of the sorted set.
* @param {number} score Score (typically timestamp).
* @param {string} member Member to add.
* @returns {Promise<number>} Number of elements added.
*/
async zadd(key, score, member) {
if (!redisClient) {
throw new Error("Redis client not connected");
}
return await redisClient.zadd(key, score, member);
},
};
Source