mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-05-10 02:33:06 +00:00
Added code coments, refactored db.ts and redis.ts, and added two new commands
This commit is contained in:
parent
b3fbd2358b
commit
890ca26c78
30 changed files with 1899 additions and 462 deletions
307
src/db/redis.ts
307
src/db/redis.ts
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue