import "dotenv/config"; import Amayo from "./core/client"; import { loadCommands } from "./core/loaders/loader"; import { loadEvents } from "./core/loaders/loaderEvents"; import { redis, redisConnect } from "./core/database/redis"; import { registeringCommands } from "./core/api/discordAPI"; import { loadComponents } from "./core/lib/components"; import { startMemoryMonitor } from "./core/memory/memoryMonitor"; import { memoryOptimizer } from "./core/memory/memoryOptimizer"; import { startReminderPoller } from "./core/api/reminders"; import { ensureRemindersSchema } from "./core/api/remindersSchema"; import { cleanExpiredGuildCache } from "./core/database/guildCache"; import logger from "./core/lib/logger"; import { applyModalSubmitInteractionPatch } from "./core/patches/discordModalPatch"; import { server } from "./server/server"; import loadFeatureFlags from "./core/loaders/featureFlagsLoader"; // Activar monitor de memoria si se define la variable const __memInt = parseInt(process.env.MEMORY_LOG_INTERVAL_SECONDS || "0", 10); if (__memInt > 0) { startMemoryMonitor({ intervalSeconds: __memInt }); } // Activar optimizador de memoria adicional if (process.env.ENABLE_MEMORY_OPTIMIZER === "true") { memoryOptimizer.start(); } // Apply safety patch for ModalSubmitInteraction members resolution before anything else try { applyModalSubmitInteractionPatch(); } catch (e) { logger.warn( { err: e }, "No se pudo aplicar el patch de ModalSubmitInteraction" ); } export const bot = new Amayo(); // Listeners de robustez del cliente Discord bot.on("error", (e) => logger.error({ err: e }, "🐞 Discord client error")); bot.on("warn", (m) => logger.warn("⚠️ Discord warn: %s", m)); // Evitar reintentos de re-login simultáneos let relogging = false; // Cuando la sesión es invalidada, intentamos reconectar/login bot.on("invalidated", () => { if (relogging) return; relogging = true; logger.error("🔄 Sesión de Discord invalidada. Reintentando login..."); withRetry("Re-login tras invalidated", () => bot.play(), { minDelayMs: 2000, maxDelayMs: 60_000, }) .catch(() => { logger.error( "No se pudo reloguear tras invalidated, se seguirá intentando en el bucle general." ); }) .finally(() => { relogging = false; }); }); // 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); logger.error(`❌ ${name} falló (intento ${attempt}) => %s`, errMsg); if (!isRetryable(err, attempt)) { logger.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); } logger.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) => { logger.error({ promise: p, reason }, "🚨 UnhandledRejection en Promise"); }); process.on("uncaughtException", (err) => { logger.error({ err }, "🚨 UncaughtException"); // No salimos; dejamos que el bot continúe vivo }); process.on("multipleResolves", (type, promise, reason: any) => { // Ignorar resoluciones sin razón (ruido) if (type === "resolve" && (reason === undefined || reason === null)) { return; } const msg = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason); const stack = reason && (reason as any).stack ? String((reason as any).stack) : ""; const isAbortErr = reason && ((reason as any).code === "ABORT_ERR" || /AbortError|operation was aborted/i.test(msg)); const isDiscordWs = /@discordjs\/ws|WebSocketShard/.test(stack); if (isAbortErr && isDiscordWs) { // Ruido benigno de reconexiones del WS de Discord: ignorar return; } logger.warn("⚠️ multipleResolves: %s %s", type, msg); }); let shuttingDown = false; async function gracefulShutdown() { if (shuttingDown) return; shuttingDown = true; logger.info("🛑 Apagado controlado iniciado..."); try { // Detener optimizador de memoria memoryOptimizer.stop(); // Cerrar Redis si procede try { if (redis?.isOpen) { await redis.quit(); logger.info("🔌 Redis cerrado"); } } catch (e) { logger.warn({ err: e }, "No se pudo cerrar Redis limpiamente"); } // Cerrar Prisma y Discord try { await bot.prisma.$disconnect(); } catch {} try { await bot.destroy(); } catch {} } finally { logger.info("✅ Apagado controlado completo"); } } process.on("SIGINT", gracefulShutdown); process.on("SIGTERM", gracefulShutdown); async function bootstrap() { logger.info("🚀 Iniciando bot..."); await server.listen(process.env.PORT || 3000, () => { logger.info( `📘 Amayo Docs disponible en http://localhost:${process.env.PORT || 3000}` ); }); // Cargar recursos locales (no deberían tirar el proceso si fallan) try { loadCommands(); } catch (e) { logger.error({ err: e }, "Error cargando comandos"); } try { loadComponents(); } catch (e) { logger.error({ err: e }, "Error cargando componentes"); } try { loadEvents(); } catch (e) { logger.error({ err: e }, "Error cargando eventos"); } try { await loadFeatureFlags(); } catch (e) { logger.error({ err: e }, "Error cargando feature flags"); } /* // Inicializar repositorio de mobs (intenta cargar mobs desde DB si existe) try { // import dinamico para evitar ciclos en startup const { initializeMobRepository } = await import("./game/mobs/mobData.js"); await initializeMobRepository(); } catch (e) { logger.warn({ err: e }, "No se pudo inicializar el repositorio de mobs"); } */ // Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot withRetry("Registrar slash commands", async () => { await registeringCommands(); }).catch((e) => logger.error({ err: e }, "Registro de comandos agotó reintentos") ); // 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); }, } ); // Asegurar esquema de Appwrite para recordatorios (colección + atributos + índice) try { await ensureRemindersSchema(); } catch (e) { logger.warn({ err: e }, "No se pudo asegurar el esquema de recordatorios"); } // Iniciar poller de recordatorios si Appwrite está configurado startReminderPoller(bot); // Iniciar limpieza periódica de caché de guilds (cada 10 minutos) setInterval(async () => { try { await cleanExpiredGuildCache(); } catch (error) { logger.error({ error }, "❌ Error en limpieza periódica de caché"); } }, 10 * 60 * 1000); // 10 minutos logger.info("✅ Bot conectado a Discord"); } // 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); }, }); })();