Por qué mantener la lógica de runtime en servicios
PrefixConfig gestiona la política global y la hidratación en el arranque, pero las reglas de dominio para leer, escribir y auditar prefijos por guild pertenecen a servicios dedicados. Mezclar lógica de runtime en config crea un cuello de botella de mantenimiento que se vuelve más difícil de testear a medida que el sistema escala. Mantenerlo separado también facilita cambiar backends de almacenamiento o añadir trazas de auditoría sin tocar la capa de configuración.
El punto de entrada para todas las operaciones de prefijos en runtime es PrefixDispatcher, que expone guildPrefixes como una instancia de PrefixGuildManager. Inyectar PrefixDispatcher en un servicio te da control total sobre el estado por guild sin depender directamente de los detalles internos de config. Todo lo siguiente opera sobre ese único punto de inyección.
- PrefixConfig gestiona la política global de prefijos, flags de funcionalidades y el provider en arranque. Mantén consultas, lógica de autorización y mutaciones de runtime fuera de config.
- Los servicios son dueños de las operaciones CRUD por guild, la coordinación de persistencia y la lógica de auditoría. Evita reimplementar bootstrap o comportamiento del parser dentro de un servicio.
- Los repositorios leen y escriben estado de prefijos en el almacenamiento. Mantén la lógica de respuesta de Discord y la orquestación de handlers separadas de la capa de repositorio.
getGuildPrefix
getGuildPrefix devuelve el arreglo de strings de prefijo activos para una guild dada, o undefined si no existe override. Este es el método principal de lectura que usas antes de mostrar el prefijo actual a un usuario o construir un embed de ayuda. Como el resultado proviene del estado en memoria, la llamada es síncrona y no tiene costo de I/O.
Úsalo en métodos de servicio de solo lectura y en constructores de embeds, no directamente en handlers. Encapsular la lectura en un método de servicio te permite añadir lógica de fallback, wrappers de caché o telemetría sin cambiar los call sites. El patrón de abajo devuelve un arreglo vacío en lugar de undefined para simplificar a los consumidores.
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
@Injectable()
export class GuildPrefixService {
constructor(private readonly dispatcher: PrefixDispatcher) {}
getForGuild(guildId: string): string[] {
return this.dispatcher.guildPrefixes.getGuildPrefix(guildId) ?? [];
}
}hasGuildPrefix
hasGuildPrefix devuelve un boolean que indica si una guild tiene un override de prefijo activo en memoria. Usa este método cuando solo necesites bifurcar por existencia, sin inspeccionar el valor del prefijo en sí. Es más barato que llamar getGuildPrefix y comprobar undefined porque la intención es explícita y puede optimizarse de forma independiente.
Un caso de uso común está en guards que deben aplicar comportamiento distinto para guilds con prefijo personalizado frente a guilds que usan el valor global por defecto. El guard puede inyectar el servicio, llamar a hasGuildPrefix y hacer short-circuit según corresponda. Evita usar este método como atajo para saltarte operaciones CRUD normales.
import { CanActivate, ExecutionContext, Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
@Injectable()
export class CustomPrefixGuard implements CanActivate {
constructor(private readonly dispatcher: PrefixDispatcher) {}
canActivate(context: ExecutionContext): boolean {
const guildId = context.getMessage().guildId;
if (!guildId) return false;
return this.dispatcher.guildPrefixes.hasGuildPrefix(guildId);
}
}setGuildPrefix
setGuildPrefix crea o reemplaza por completo el override de prefijo para una guild. El tercer argumento acepta un objeto opcional de metadatos para guardar campos de auditoría como el ID del actor y la marca de tiempo. Llámalo cuando un usuario o administrador actualiza el prefijo mediante un comando o endpoint de API.
Persiste siempre el nuevo valor en tu base de datos antes de llamar a setGuildPrefix en memoria. Si la escritura en base de datos falla después de la mutación en memoria, el estado en memoria diverge del estado duradero y se pierde la sincronización hasta la siguiente recarga. El patrón persist-first de abajo mantiene ambas capas consistentes ante fallos parciales.
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import { GuildPrefixRepository } from '../repositories/guild-prefix.repository';
@Injectable()
export class GuildPrefixService {
constructor(
private readonly dispatcher: PrefixDispatcher,
private readonly repository: GuildPrefixRepository,
) {}
async setForGuild(
guildId: string,
prefix: string,
actorId: string,
): Promise<void> {
await this.repository.upsert(guildId, [prefix], actorId);
this.dispatcher.guildPrefixes.setGuildPrefix(guildId, prefix, {
updatedBy: actorId,
updatedAt: new Date().toISOString(),
});
}
}updateGuildPrefix
updateGuildPrefix parchea la entrada existente con una fusión parcial en lugar de un reemplazo completo. Úsalo cuando quieras cambiar solo el valor del prefijo y conservar los campos de metadatos existentes, o cuando quieras actualizar metadatos sin cambiar el prefijo. Se comporta igual que setGuildPrefix si no existe ninguna entrada para la guild.
Este método es útil para escenarios como un panel de administración que envía actualizaciones por campo en lugar de reenviar todo el registro. Si tu capa de almacenamiento también soporta actualizaciones parciales, empareja ambas llamadas para mantener la base de datos y el estado en memoria consistentes con la misma semántica.
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import { GuildPrefixRepository } from '../repositories/guild-prefix.repository';
@Injectable()
export class GuildPrefixService {
constructor(
private readonly dispatcher: PrefixDispatcher,
private readonly repository: GuildPrefixRepository,
) {}
async updateForGuild(
guildId: string,
prefix: string,
actorId: string,
): Promise<void> {
await this.repository.patch(guildId, {
prefixes: [prefix],
updatedBy: actorId,
});
this.dispatcher.guildPrefixes.updateGuildPrefix(guildId, prefix, {
updatedBy: actorId,
updatedAt: new Date().toISOString(),
});
}
}removeGuildPrefix
removeGuildPrefix elimina el override en memoria para una guild y devuelve true si existía una entrada o false si la guild no tenía prefijo personalizado. Tras eliminarlo, el parser vuelve al prefijo global definido en PrefixConfig para esa guild. Úsalo cuando un administrador de guild restablece su servidor al comportamiento por defecto.
Sigue aquí también el patrón persist-first: elimina en la base de datos antes de llamar a removeGuildPrefix en memoria. Esto garantiza que un reinicio del proceso no vuelva a hidratar la entrada eliminada desde el provider. El ejemplo de abajo devuelve un booleano al caller para que el handler pueda confirmar si realmente ocurrió una eliminación.
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import { GuildPrefixRepository } from '../repositories/guild-prefix.repository';
@Injectable()
export class GuildPrefixService {
constructor(
private readonly dispatcher: PrefixDispatcher,
private readonly repository: GuildPrefixRepository,
) {}
async removeForGuild(guildId: string): Promise<boolean> {
await this.repository.remove(guildId);
return this.dispatcher.guildPrefixes.removeGuildPrefix(guildId);
}
}getAllGuildIds
getAllGuildIds devuelve un arreglo con cada ID de guild que actualmente tiene un override de prefijo en memoria. Úsalo en paneles administrativos, jobs de sincronización o endpoints de diagnóstico que necesiten enumerar o iterar sobre guilds configuradas. El resultado refleja el estado vivo en memoria, por lo que puede diferir de la base de datos durante una recarga parcialmente completada.
Un patrón común es llamar getAllGuildIds en un job programado que cruce memoria contra registros de base de datos y repare cualquier drift. También puedes usarlo para construir una lista paginada para una API de gestión sin ejecutar escaneos amplios en base de datos. Mantén la llamada dentro de la capa de servicio para que los consumidores no dependan directamente del dispatcher.
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
@Injectable()
export class GuildPrefixService {
constructor(private readonly dispatcher: PrefixDispatcher) {}
listConfiguredGuilds(): string[] {
return this.dispatcher.guildPrefixes.getAllGuildIds();
}
}loadFromProvider: hook de ciclo de vida onReady
loadFromProvider acepta una función provider asíncrona, la espera y reemplaza todas las entradas de prefijo por guild en memoria con el resultado. Como realiza I/O masivo y está intrínsecamente ligado a disponibilidad de datos, pertenece a un hook de ciclo de vida en lugar de un método de servicio de propósito general. Ejecutarlo desde un servicio requeriría que un caller supiera cuándo invocarlo o arriesgar múltiples recargas completas concurrentes.
El hook onReady de SpraxiumOnReady se ejecuta una vez después de que el cliente de Discord inicia sesión y está listo, que es exactamente cuando los datos iniciales deberían cargarse en memoria. Implementa el hook en una clase de servicio dedicada, inyecta PrefixDispatcher junto a tu repositorio y llama ahí a loadFromProvider. También puedes llamar loadFromProvider desde un job programado cuando necesites una resincronización controlada después de cambios masivos en base de datos.
import { Injectable } from '@spraxium/common';
import type { SpraxiumOnReady } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import type { Client } from 'discord.js';
import { GuildPrefixRepository } from './repositories/guild-prefix.repository';
@Injectable()
export class PrefixPrefetchService implements SpraxiumOnReady {
constructor(
private readonly dispatcher: PrefixDispatcher,
private readonly repository: GuildPrefixRepository,
) {}
async onReady(_client: Client<true>): Promise<void> {
await this.dispatcher.guildPrefixes.loadFromProvider(async () => {
const rows = await this.repository.findAll();
return rows.map((row) => ({
guildId: row.guildId,
prefix: row.prefixes,
metadata: {
updatedBy: row.updatedBy,
updatedAt: row.updatedAt,
},
}));
});
}
}Registra PrefixPrefetchService en un módulo para que SpraxiumOnReady se llame automáticamente. El framework invoca onReady en todo inyectable que lo implemente, así que no se requiere cableado extra más allá de la declaración del módulo.
Checklist de producción
Una implementación robusta debe mantener alineados el estado en memoria y el estado persistido ante condiciones de fallo. También necesita suficiente contexto de auditoría para flujos de soporte y moderación. Esta checklist ayuda a validar esas garantías antes del release.
- Persist before mutating memory: escribe en tu base de datos antes de llamar a cualquier método de mutación en memoria para evitar divergencia de estado ante fallos parciales.
- Wrap dispatcher writes in a service: centraliza reglas de negocio y validación en una única clase de servicio para simplificar pruebas y cambios futuros.
- Store audit metadata on every write: registra el ID del actor y la marca de tiempo para que los flujos de moderación y soporte puedan rastrear cuándo cambió un prefijo.
- Use onReady for initial load: llama a loadFromProvider desde un hook de ciclo de vida después de que el cliente de Discord esté listo para garantizar que los datos estén disponibles antes de ejecutar comandos.
- Schedule safe reload routines: reconcilia el estado en memoria con la base de datos después de migraciones o importaciones masivas para cerrar cualquier ventana de drift.