Added code coments, refactored db.ts and redis.ts, and added two new commands

This commit is contained in:
Ahmad 2025-03-16 20:31:43 -04:00
parent b3fbd2358b
commit 890ca26c78
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
30 changed files with 1899 additions and 462 deletions

View file

@ -1,9 +1,31 @@
import Redis from 'ioredis';
import { Client } from 'discord.js';
import { loadConfig } from '../util/configLoader.js';
import {
logManagerNotification,
NotificationType,
notifyManagers,
} from '../util/notificationHandler.js';
const config = loadConfig();
const redis = new Redis(config.redisConnectionString);
// Redis connection state
let isRedisAvailable = false;
let redis: Redis;
let connectionAttempts = 0;
const MAX_RETRY_ATTEMPTS = config.redis.retryAttempts;
const INITIAL_RETRY_DELAY = config.redis.initialRetryDelay;
let hasNotifiedDisconnect = false;
let discordClient: Client | null = null;
// ========================
// Redis Utility Classes and Helper Functions
// ========================
/**
* Custom error class for Redis errors
*/
class RedisError extends Error {
constructor(
message: string,
@ -14,77 +36,271 @@ class RedisError extends Error {
}
}
redis.on('error', (error: Error) => {
console.error('Redis connection error:', error);
throw new RedisError('Failed to connect to Redis instance: ', error);
});
/**
* Redis error handler
* @param errorMessage - The error message to log
* @param error - The error object
*/
const handleRedisError = (errorMessage: string, error: Error): null => {
console.error(`${errorMessage}:`, error);
throw new RedisError(errorMessage, error);
};
redis.on('connect', () => {
console.log('Successfully connected to Redis');
});
/**
* Sets the Discord client for sending notifications
* @param client - The Discord client
*/
export function setDiscordClient(client: Client): void {
discordClient = client;
}
/**
* Initializes the Redis connection with retry logic
*/
async function initializeRedisConnection() {
try {
if (redis && redis.status !== 'end' && redis.status !== 'close') {
return;
}
redis = new Redis(config.redis.redisConnectionString, {
retryStrategy(times) {
connectionAttempts = times;
if (times >= MAX_RETRY_ATTEMPTS) {
const message = `Failed to connect to Redis after ${times} attempts. Caching will be disabled.`;
console.warn(message);
if (!hasNotifiedDisconnect && discordClient) {
logManagerNotification(NotificationType.REDIS_CONNECTION_LOST);
notifyManagers(
discordClient,
NotificationType.REDIS_CONNECTION_LOST,
`Connection attempts exhausted after ${times} tries. Caching is now disabled.`,
);
hasNotifiedDisconnect = true;
}
return null;
}
const delay = Math.min(INITIAL_RETRY_DELAY * Math.pow(2, times), 30000);
console.log(
`Retrying Redis connection in ${delay}ms... (Attempt ${times + 1}/${MAX_RETRY_ATTEMPTS})`,
);
return delay;
},
maxRetriesPerRequest: 3,
enableOfflineQueue: true,
});
// ========================
// Redis Events
// ========================
redis.on('error', (error: Error) => {
console.error('Redis Connection Error:', error);
isRedisAvailable = false;
});
redis.on('connect', () => {
console.info('Successfully connected to Redis');
isRedisAvailable = true;
connectionAttempts = 0;
if (hasNotifiedDisconnect && discordClient) {
logManagerNotification(NotificationType.REDIS_CONNECTION_RESTORED);
notifyManagers(
discordClient,
NotificationType.REDIS_CONNECTION_RESTORED,
);
hasNotifiedDisconnect = false;
}
});
redis.on('close', () => {
console.warn('Redis connection closed');
isRedisAvailable = false;
// Try to reconnect after some time if we've not exceeded max attempts
if (connectionAttempts < MAX_RETRY_ATTEMPTS) {
const delay = Math.min(
INITIAL_RETRY_DELAY * Math.pow(2, connectionAttempts),
30000,
);
setTimeout(initializeRedisConnection, delay);
} else if (!hasNotifiedDisconnect && discordClient) {
logManagerNotification(NotificationType.REDIS_CONNECTION_LOST);
notifyManagers(
discordClient,
NotificationType.REDIS_CONNECTION_LOST,
'Connection closed and max retry attempts reached.',
);
hasNotifiedDisconnect = true;
}
});
redis.on('reconnecting', () => {
console.info('Attempting to reconnect to Redis...');
});
} catch (error) {
console.error('Failed to initialize Redis:', error);
isRedisAvailable = false;
if (!hasNotifiedDisconnect && discordClient) {
logManagerNotification(
NotificationType.REDIS_CONNECTION_LOST,
`Error: ${error}`,
);
notifyManagers(
discordClient,
NotificationType.REDIS_CONNECTION_LOST,
`Initialization error: ${error}`,
);
hasNotifiedDisconnect = true;
}
}
}
// Initialize Redis connection
initializeRedisConnection();
/**
* Check if Redis is currently available, and attempt to reconnect if not
* @returns - True if Redis is connected and available
*/
export async function ensureRedisConnection(): Promise<boolean> {
if (!isRedisAvailable) {
await initializeRedisConnection();
}
return isRedisAvailable;
}
// ========================
// Redis Functions
// ========================
/**
* Function to set a key in Redis
* @param key - The key to set
* @param value - The value to set
* @param ttl - The time to live for the key
* @returns - 'OK' if successful
*/
export async function set(
key: string,
value: string,
ttl?: number,
): Promise<'OK'> {
try {
await redis.set(key, value);
if (ttl) await redis.expire(key, ttl);
} catch (error) {
console.error('Redis set error: ', error);
throw new RedisError(`Failed to set key: ${key}, `, error as Error);
): Promise<'OK' | null> {
if (!(await ensureRedisConnection())) {
console.warn('Redis unavailable, skipping set operation');
return null;
}
try {
await redis.set(`bot:${key}`, value);
if (ttl) await redis.expire(`bot:${key}`, ttl);
return 'OK';
} catch (error) {
return handleRedisError(`Failed to set key: ${key}`, error as Error);
}
return Promise.resolve('OK');
}
/**
* Function to set a key in Redis with a JSON value
* @param key - The key to set
* @param value - The value to set
* @param ttl - The time to live for the key
* @returns - 'OK' if successful
*/
export async function setJson<T>(
key: string,
value: T,
ttl?: number,
): Promise<'OK'> {
): Promise<'OK' | null> {
return await set(key, JSON.stringify(value), ttl);
}
export async function incr(key: string): Promise<number> {
/**
* Increments a key in Redis
* @param key - The key to increment
* @returns - The new value of the key, or null if Redis is unavailable
*/
export async function incr(key: string): Promise<number | null> {
if (!(await ensureRedisConnection())) {
console.warn('Redis unavailable, skipping increment operation');
return null;
}
try {
return await redis.incr(key);
return await redis.incr(`bot:${key}`);
} catch (error) {
console.error('Redis increment error: ', error);
throw new RedisError(`Failed to increment key: ${key}, `, error as Error);
return handleRedisError(`Failed to increment key: ${key}`, error as Error);
}
}
export async function exists(key: string): Promise<boolean> {
/**
* Checks if a key exists in Redis
* @param key - The key to check
* @returns - True if the key exists, false otherwise, or null if Redis is unavailable
*/
export async function exists(key: string): Promise<boolean | null> {
if (!(await ensureRedisConnection())) {
console.warn('Redis unavailable, skipping exists operation');
return null;
}
try {
return (await redis.exists(key)) === 1;
return (await redis.exists(`bot:${key}`)) === 1;
} catch (error) {
console.error('Redis exists error: ', error);
throw new RedisError(
`Failed to check if key exists: ${key}, `,
return handleRedisError(
`Failed to check if key exists: ${key}`,
error as Error,
);
}
}
/**
* Gets the value of a key in Redis
* @param key - The key to get
* @returns - The value of the key, or null if the key does not exist or Redis is unavailable
*/
export async function get(key: string): Promise<string | null> {
if (!(await ensureRedisConnection())) {
console.warn('Redis unavailable, skipping get operation');
return null;
}
try {
return await redis.get(key);
return await redis.get(`bot:${key}`);
} catch (error) {
console.error('Redis get error: ', error);
throw new RedisError(`Failed to get key: ${key}, `, error as Error);
return handleRedisError(`Failed to get key: ${key}`, error as Error);
}
}
export async function mget(...keys: string[]): Promise<(string | null)[]> {
/**
* Gets the values of multiple keys in Redis
* @param keys - The keys to get
* @returns - The values of the keys, or null if Redis is unavailable
*/
export async function mget(
...keys: string[]
): Promise<(string | null)[] | null> {
if (!(await ensureRedisConnection())) {
console.warn('Redis unavailable, skipping mget operation');
return null;
}
try {
return await redis.mget(keys);
return await redis.mget(...keys.map((key) => `bot:${key}`));
} catch (error) {
console.error('Redis mget error: ', error);
throw new RedisError(`Failed to get keys: ${keys}, `, error as Error);
return handleRedisError('Failed to get keys', error as Error);
}
}
/**
* Gets the value of a key in Redis and parses it as a JSON object
* @param key - The key to get
* @returns - The parsed JSON value of the key, or null if the key does not exist or Redis is unavailable
*/
export async function getJson<T>(key: string): Promise<T | null> {
const value = await get(key);
if (!value) return null;
@ -95,11 +311,28 @@ export async function getJson<T>(key: string): Promise<T | null> {
}
}
export async function del(key: string): Promise<number> {
/**
* Deletes a key in Redis
* @param key - The key to delete
* @returns - The number of keys that were deleted, or null if Redis is unavailable
*/
export async function del(key: string): Promise<number | null> {
if (!(await ensureRedisConnection())) {
console.warn('Redis unavailable, skipping delete operation');
return null;
}
try {
return await redis.del(key);
return await redis.del(`bot:${key}`);
} catch (error) {
console.error('Redis del error: ', error);
throw new RedisError(`Failed to delete key: ${key}, `, error as Error);
return handleRedisError(`Failed to delete key: ${key}`, error as Error);
}
}
/**
* Check if Redis is currently available
* @returns - True if Redis is connected and available
*/
export function isRedisConnected(): boolean {
return isRedisAvailable;
}