diff --git a/Procfile b/Procfile index 9ebe8e8..de4acc0 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ -worker: npm start +worker: npm start:prod +dev: npm dev:mem diff --git a/package.json b/package.json index 9dc6987..130609f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,11 @@ "main": "src/main.ts", "scripts": { "start": "npx tsx watch src/main.ts", - "dev": "npx tsx watch src/main.ts" + "dev": "npx tsx watch src/main.ts", + "dev:light": "CACHE_MESSAGES_LIMIT=25 CACHE_MEMBERS_LIMIT=50 SWEEP_MESSAGES_LIFETIME_SECONDS=600 SWEEP_MESSAGES_INTERVAL_SECONDS=240 npx tsx watch --clear-screen=false src/main.ts", + "dev:mem": "MEMORY_LOG_INTERVAL_SECONDS=120 npx tsx watch src/main.ts", + "start:prod": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=384 npx tsx src/main.ts", + "typecheck": "tsc --noEmit" }, "keywords": [ ], "author": "", diff --git a/src/core/client.ts b/src/core/client.ts index 68e5ed6..a0332c6 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -1,15 +1,15 @@ // @ts-ignore -import { Client, GatewayIntentBits } from 'discord.js'; -// 1. Importa PrismaClient +import { Client, GatewayIntentBits, Options, Partials } from 'discord.js'; +// 1. Importa PrismaClient (singleton) // @ts-ignore -import { PrismaClient } from '@prisma/client'; +import { prisma, ensurePrismaConnection } from './prisma'; process.loadEnvFile(); class Amayo extends Client { public key: string; - // 2. Declara la propiedad prisma - public prisma: PrismaClient; + // 2. Propiedad prisma apuntando al singleton + public prisma = prisma; constructor() { super({ @@ -17,17 +17,37 @@ class Amayo extends Client { GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMessageTyping + GatewayIntentBits.MessageContent + // Eliminado GuildMessageTyping para reducir tráfico/memoria si no se usa ], + partials: [Partials.Channel, Partials.Message], // Permite recibir eventos sin cachear todo + makeCache: Options.cacheWithLimits({ + // Limitar el tamaño de los managers más pesados + MessageManager: parseInt(process.env.CACHE_MESSAGES_LIMIT || '50', 10), + GuildMemberManager: parseInt(process.env.CACHE_MEMBERS_LIMIT || '100', 10), + ThreadManager: 10, + ReactionManager: 0, + GuildInviteManager: 0, + StageInstanceManager: 0, + PresenceManager: 0 + }), + sweepers: { + messages: { + // Cada 5 min barrer mensajes más antiguos que 15 min (ajustable por env) + interval: parseInt(process.env.SWEEP_MESSAGES_INTERVAL_SECONDS || '300', 10), + lifetime: parseInt(process.env.SWEEP_MESSAGES_LIFETIME_SECONDS || '900', 10) + }, + users: { + interval: 60 * 30, // cada 30 minutos + filter: () => (user) => user.bot && user.id !== this.user?.id + } + }, rest: { - retries: 10 + retries: 5 // bajar un poco para evitar colas largas en memoria } }); this.key = process.env.TOKEN ?? ''; - // 3. Instancia PrismaClient en el constructor - this.prisma = new PrismaClient(); } async play () { @@ -35,10 +55,9 @@ class Amayo extends Client { 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 { - await this.prisma.$connect(); - console.log('Successfully connected to the database.'); + await ensurePrismaConnection(); + console.log('Successfully connected to the database (singleton).'); await this.login(this.key); } catch (error) { console.error('Failed to connect to DB or login to Discord:', error); diff --git a/src/core/memoryMonitor.ts b/src/core/memoryMonitor.ts new file mode 100644 index 0000000..9ddeb76 --- /dev/null +++ b/src/core/memoryMonitor.ts @@ -0,0 +1,50 @@ +// Monitor ligero de memoria y event loop. +// Se activa si defines MEMORY_LOG_INTERVAL_SECONDS. +import { monitorEventLoopDelay } from 'node:perf_hooks'; + +export interface MemoryMonitorOptions { + intervalSeconds: number; + warnHeapRatio?: number; // 0-1 fracción del máximo estimado antes de warning +} + +export function startMemoryMonitor(opts: MemoryMonitorOptions) { + const { intervalSeconds, warnHeapRatio = 0.8 } = opts; + if (intervalSeconds <= 0) return; + + const h = monitorEventLoopDelay({ resolution: 20 }); + h.enable(); + + const formatMB = (n: number) => (n / 1024 / 1024).toFixed(1) + 'MB'; + + // Intento de obtener el límite de heap (puede variar según flags) + let heapLimit = 0; + try { + // @ts-ignore + const v8 = require('v8'); + const stats = v8.getHeapStatistics(); + heapLimit = stats.heap_size_limit || 0; + } catch {} + + setInterval(() => { + const m = process.memoryUsage(); + const rss = formatMB(m.rss); + const heapUsed = formatMB(m.heapUsed); + const heapTotal = formatMB(m.heapTotal); + const external = formatMB(m.external); + const elDelay = h.mean / 1e6; // ms + + let warn = ''; + if (heapLimit) { + const ratio = m.heapUsed / heapLimit; + if (ratio >= warnHeapRatio) { + warn = ` ⚠ heap ${(ratio * 100).toFixed(1)}% del límite (~${formatMB(heapLimit)})`; + } + } + + console.log(`[MEM] rss=${rss} heapUsed=${heapUsed} heapTotal=${heapTotal} ext=${external} evLoopDelay=${elDelay.toFixed(2)}ms${warn}`); + + // Resetear métricas de event loop delay para la siguiente ventana + h.reset(); + }, intervalSeconds * 1000).unref(); +} + diff --git a/src/core/prisma.ts b/src/core/prisma.ts new file mode 100644 index 0000000..162f4d2 --- /dev/null +++ b/src/core/prisma.ts @@ -0,0 +1,24 @@ +// Prisma singleton para evitar múltiples instancias (especialmente en modo watch) +// y reducir consumo de memoria. +import { PrismaClient } from '@prisma/client'; + +// Contenedor global seguro para hot-reload / watch sin duplicar instancias +const globalForPrisma = globalThis as unknown as { __amayo_prisma?: PrismaClient }; + +export const prisma: PrismaClient = globalForPrisma.__amayo_prisma ?? new PrismaClient({ + log: process.env.PRISMA_LOG_QUERIES === '1' ? ['query', 'error', 'warn'] : ['error', 'warn'] +}); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.__amayo_prisma = prisma; +} + +export async function ensurePrismaConnection() { + // Evita múltiples $connect si ya está conectada (no hay API directa, usamos heurística) + try { + await prisma.$queryRaw`SELECT 1`; + } catch { + await prisma.$connect(); + } + return prisma; +} diff --git a/src/events/extras/alliace.ts b/src/events/extras/alliace.ts index 3bd4526..54d3762 100644 --- a/src/events/extras/alliace.ts +++ b/src/events/extras/alliace.ts @@ -1,12 +1,10 @@ import { Message } from "discord.js"; -// Se agrega ts -//@ts-ignore -import { PrismaClient } from "@prisma/client"; +// Reemplaza instancia local -> usa singleton +import { prisma } from "../../core/prisma"; import { replaceVars } from "../../core/lib/vars"; -const prisma = new PrismaClient(); // Regex para detectar URLs válidas (corregido) const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/gi; @@ -389,8 +387,8 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an } // Función helper para validar URLs -function isValidUrl(url: string): boolean { - if (!url || typeof url !== 'string') return false; +function isValidUrl(url: unknown): url is string { + if (typeof url !== 'string' || !url) return false; try { new URL(url); return url.startsWith('http://') || url.startsWith('https://'); diff --git a/src/main.ts b/src/main.ts index fc720d5..b074648 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,13 @@ import { loadEvents } from "./core/loaderEvents"; import { redis, redisConnect } from "./core/redis"; import { registeringCommands } from "./core/api/discordAPI"; import {loadComponents} from "./core/components"; +import { startMemoryMonitor } from "./core/memoryMonitor"; // añadido + +// 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 }); +} export const bot = new Amayo();