From de7bcccb8872cc7ca633cad164e303e20608459a Mon Sep 17 00:00:00 2001 From: shnimlz Date: Mon, 22 Sep 2025 22:00:11 -0500 Subject: [PATCH] feat: enhance error handling and retry logic for Discord bot connections --- package.json | 6 +- src/core/client.ts | 6 +- src/main.ts | 154 +++++++++++++++++++++++++++++++++++++++++---- tsconfig.json | 4 +- 4 files changed, 152 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 746aeb0..9dc6987 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "amayo", - "version": "0.10.210", + "version": "0.11.2", "description": "", "main": "src/main.ts", "scripts": { "start": "npx tsx watch src/main.ts", "dev": "npx tsx watch src/main.ts" }, - "keywords": [], + "keywords": [ ], "author": "", "license": "ISC", "type": "commonjs", @@ -25,4 +25,4 @@ "ts-node": "10.9.2", "typescript": "5.9.2" } -} +} \ No newline at end of file diff --git a/src/core/client.ts b/src/core/client.ts index ad90ad5..68e5ed6 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -32,7 +32,8 @@ class Amayo extends Client { async play () { if(!this.key) { - return console.error('No key provided'); + console.error('No key provided'); + throw new Error('Missing DISCORD TOKEN'); } else { // Ejemplo de cómo usarías prisma antes de iniciar sesión try { @@ -40,7 +41,8 @@ class Amayo extends Client { console.log('Successfully connected to the database.'); await this.login(this.key); } 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 } } } diff --git a/src/main.ts b/src/main.ts index 6f5e289..1e34c5a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,28 +1,160 @@ import Amayo from "./core/client"; import { loadCommands } from "./core/loader"; import { loadEvents } from "./core/loaderEvents"; -import { redisConnect } from "./core/redis"; +import { redis, redisConnect } from "./core/redis"; import { registeringCommands } from "./core/api/discordAPI"; import {loadComponents} from "./core/components"; 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(name: string, fn: () => Promise, opts?: { + retries?: number; + minDelayMs?: number; + maxDelayMs?: number; + factor?: number; + jitter?: boolean; + isRetryable?: (err: unknown, attempt: number) => boolean; +}): Promise { + 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() { console.log("🚀 Iniciando bot..."); - loadCommands(); // 1️⃣ Cargar comandos en la Collection - loadComponents() - loadEvents(); // 2️⃣ Cargar eventos + // Cargar recursos locales (no deberían tirar el proceso si fallan) + try { loadCommands(); } catch (e) { console.error('Error cargando comandos:', e); } + 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"); } -bootstrap().catch((err) => { - console.error("❌ Error en el arranque:", err); - process.exit(1); -}); +// Bucle de arranque resiliente: si bootstrap completo falla, reintenta sin matar el proceso +(async function startLoop() { + 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); + } + }); +})(); diff --git a/tsconfig.json b/tsconfig.json index 0e2c6a7..7e728fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2024", - "module": "ESNext", - "moduleResolution": "nodenext", + "module": "NodeNext", + "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true,