feat: enhance error handling and retry logic for Discord bot connections

This commit is contained in:
2025-09-22 22:00:11 -05:00
parent 468480478a
commit de7bcccb88
4 changed files with 152 additions and 18 deletions

View File

@@ -1,13 +1,13 @@
{ {
"name": "amayo", "name": "amayo",
"version": "0.10.210", "version": "0.11.2",
"description": "", "description": "",
"main": "src/main.ts", "main": "src/main.ts",
"scripts": { "scripts": {
"start": "npx tsx watch src/main.ts", "start": "npx tsx watch src/main.ts",
"dev": "npx tsx watch src/main.ts" "dev": "npx tsx watch src/main.ts"
}, },
"keywords": [], "keywords": [ ],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"type": "commonjs", "type": "commonjs",

View File

@@ -32,7 +32,8 @@ class Amayo extends Client {
async play () { async play () {
if(!this.key) { if(!this.key) {
return console.error('No key provided'); console.error('No key provided');
throw new Error('Missing DISCORD TOKEN');
} else { } else {
// Ejemplo de cómo usarías prisma antes de iniciar sesión // Ejemplo de cómo usarías prisma antes de iniciar sesión
try { try {
@@ -40,7 +41,8 @@ class Amayo extends Client {
console.log('Successfully connected to the database.'); console.log('Successfully connected to the database.');
await this.login(this.key); await this.login(this.key);
} catch (error) { } catch (error) {
console.error('Failed to connect to the database:', error); console.error('Failed to connect to DB or login to Discord:', error);
throw error; // Propaga para que withRetry en main.ts reintente
} }
} }
} }

View File

@@ -1,28 +1,160 @@
import Amayo from "./core/client"; import Amayo from "./core/client";
import { loadCommands } from "./core/loader"; import { loadCommands } from "./core/loader";
import { loadEvents } from "./core/loaderEvents"; import { loadEvents } from "./core/loaderEvents";
import { redisConnect } from "./core/redis"; import { redis, redisConnect } from "./core/redis";
import { registeringCommands } from "./core/api/discordAPI"; import { registeringCommands } from "./core/api/discordAPI";
import {loadComponents} from "./core/components"; import {loadComponents} from "./core/components";
export const bot = new Amayo(); export const bot = new Amayo();
// Listeners de robustez del cliente Discord
bot.on('error', (e) => console.error('🐞 Discord client error:', e));
bot.on('warn', (m) => console.warn('⚠️ Discord warn:', m));
// Cuando la sesión es invalidada, intentamos reconectar/login
bot.on('invalidated', () => {
console.error('🔄 Sesión de Discord invalidada. Reintentando login...');
withRetry('Re-login tras invalidated', () => bot.play(), { minDelayMs: 2000, maxDelayMs: 60_000 }).catch(() => {
console.error('No se pudo reloguear tras invalidated, se seguirá intentando en el bucle general.');
});
});
// Utilidad: reintentos con backoff exponencial + jitter
async function withRetry<T>(name: string, fn: () => Promise<T>, opts?: {
retries?: number;
minDelayMs?: number;
maxDelayMs?: number;
factor?: number;
jitter?: boolean;
isRetryable?: (err: unknown, attempt: number) => boolean;
}): Promise<T> {
const {
retries = Infinity,
minDelayMs = 1000,
maxDelayMs = 30_000,
factor = 1.8,
jitter = true,
isRetryable = () => true,
} = opts ?? {};
let attempt = 0;
let delay = minDelayMs;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
return await fn();
} catch (err) {
attempt++;
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
console.error(`${name} falló (intento ${attempt}) =>`, errMsg);
if (!isRetryable(err, attempt)) {
console.error(`${name}: error no recuperable, deteniendo reintentos.`);
throw err;
}
if (attempt >= retries) throw err;
// calcular backoff
let wait = delay;
if (jitter) {
const rand = Math.random() + 0.5; // 0.5x a 1.5x
wait = Math.min(maxDelayMs, Math.floor(delay * rand));
} else {
wait = Math.min(maxDelayMs, delay);
}
console.warn(`⏳ Reintentando ${name} en ${wait}ms...`);
await new Promise((r) => setTimeout(r, wait));
delay = Math.min(maxDelayMs, Math.floor(delay * factor));
}
}
}
// Handlers globales para robustez
process.on('unhandledRejection', (reason: any, p) => {
console.error('🚨 UnhandledRejection en Promise:', p, 'razón:', reason);
});
process.on('uncaughtException', (err) => {
console.error('🚨 UncaughtException:', err);
// No salimos; dejamos que el bot continúe vivo
});
process.on('multipleResolves', (type, promise, reason) => {
console.warn('⚠️ multipleResolves:', type, reason);
});
let shuttingDown = false;
async function gracefulShutdown() {
if (shuttingDown) return;
shuttingDown = true;
console.log('🛑 Apagado controlado iniciado...');
try {
// Cerrar Redis si procede
try {
if (redis?.isOpen) {
await redis.quit();
console.log('🔌 Redis cerrado');
}
} catch (e) {
console.warn('No se pudo cerrar Redis limpiamente:', e);
}
// Cerrar Prisma y Discord
try {
await bot.prisma.$disconnect();
} catch {}
try {
await bot.destroy();
} catch {}
} finally {
console.log('✅ Apagado controlado completo');
}
}
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
async function bootstrap() { async function bootstrap() {
console.log("🚀 Iniciando bot..."); console.log("🚀 Iniciando bot...");
loadCommands(); // 1⃣ Cargar comandos en la Collection // Cargar recursos locales (no deberían tirar el proceso si fallan)
loadComponents() try { loadCommands(); } catch (e) { console.error('Error cargando comandos:', e); }
loadEvents(); // 2⃣ Cargar eventos try { loadComponents(); } catch (e) { console.error('Error cargando componentes:', e); }
try { loadEvents(); } catch (e) { console.error('Error cargando eventos:', e); }
await registeringCommands(); // 3⃣ Registrar los slash en Discord // Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot
withRetry('Registrar slash commands', async () => {
await registeringCommands();
}).catch((e) => console.error('Registro de comandos agotó reintentos:', e));
await redisConnect(); // 4⃣ Conectar Redis // Conectar Redis con reintentos
await withRetry('Conectar a Redis', async () => {
await redisConnect();
});
// Login Discord + DB con reintentos (gestionado en Amayo.play -> conecta Prisma + login)
await withRetry('Login de Discord', async () => {
await bot.play();
}, {
isRetryable: (err) => {
const msg = err instanceof Error ? `${err.message}` : String(err);
// Si falta el TOKEN o token inválido, no tiene sentido reintentar sin cambiar config
return !/missing discord token|invalid token/i.test(msg);
}
});
await bot.play();
console.log("✅ Bot conectado a Discord"); console.log("✅ Bot conectado a Discord");
} }
bootstrap().catch((err) => { // Bucle de arranque resiliente: si bootstrap completo falla, reintenta sin matar el proceso
console.error("❌ Error en el arranque:", err); (async function startLoop() {
process.exit(1); await withRetry('Arranque', bootstrap, {
}); minDelayMs: 1000,
maxDelayMs: 60_000,
isRetryable: (err) => {
const msg = err instanceof Error ? `${err.message}` : String(err);
// No reintentar en bucle si el problema es falta/invalid token
return !/missing discord token|invalid token/i.test(msg);
}
});
})();

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2024", "target": "ES2024",
"module": "ESNext", "module": "NodeNext",
"moduleResolution": "nodenext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,