From 76ce4e4e4d71248e5c76d26067d1292905c64d7d Mon Sep 17 00:00:00 2001 From: Shni Date: Fri, 31 Oct 2025 21:18:46 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20Agregar=20ejemplos=20de=20uso=20de=20fe?= =?UTF-8?q?ature=20flags=20en=20comandos=20y=20mejorar=20la=20configuraci?= =?UTF-8?q?=C3=B3n=20del=20proyecto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README/FEATURE_FLAGS_COMANDOS.md | 311 +++++++++++++++++++ src/core/lib/featureFlagCommandWrapper.ts | 218 +++++++++++++ test/examples/featureFlagsCommands.ts | 361 ++++++++++++++++++++++ test/examples/featureFlagsUsage.ts | 296 ------------------ tsconfig.json | 2 +- 5 files changed, 891 insertions(+), 297 deletions(-) create mode 100644 README/FEATURE_FLAGS_COMANDOS.md create mode 100644 src/core/lib/featureFlagCommandWrapper.ts create mode 100644 test/examples/featureFlagsCommands.ts delete mode 100644 test/examples/featureFlagsUsage.ts diff --git a/README/FEATURE_FLAGS_COMANDOS.md b/README/FEATURE_FLAGS_COMANDOS.md new file mode 100644 index 0000000..f7a5d8f --- /dev/null +++ b/README/FEATURE_FLAGS_COMANDOS.md @@ -0,0 +1,311 @@ +# 🎮 Feature Flags - Uso en Comandos Slash y Mensajes + +## 🚀 Uso Universal + +El sistema funciona **idénticamente** para comandos slash y comandos de mensaje. + +--- + +## 📦 3 Formas de Usar + +### 1️⃣ Wrapper (Recomendado - Más Limpio) + +```typescript +import { withFeatureFlag } from "@/core/lib/featureFlagCommandWrapper"; +import { CommandSlash } from "@/core/types/commands"; + +// COMANDO SLASH +export const command: CommandSlash = { + name: 'shop', + description: 'Abre la tienda', + type: 'slash', + cooldown: 10, + run: withFeatureFlag('new_shop_system', async (interaction, client) => { + // Tu código aquí - solo se ejecuta si el flag está enabled + await interaction.reply('🛒 Tienda!'); + }, { + fallbackMessage: '🔧 Tienda en mantenimiento' + }) +}; + +// COMANDO DE MENSAJE +export const command: CommandMessage = { + name: 'shop', + type: 'message', + cooldown: 10, + run: withFeatureFlag('new_shop_system', async (message, args, client) => { + // Mismo código, funciona igual + await message.reply('🛒 Tienda!'); + }, { + fallbackMessage: '🔧 Tienda en mantenimiento' + }) +}; +``` + +### 2️⃣ Guard (Respuesta Automática) + +```typescript +import { guardFeatureFlag } from "@/core/lib/featureFlagCommandWrapper"; + +// COMANDO SLASH +export const command: CommandSlash = { + name: 'mine', + description: 'Minea recursos', + type: 'slash', + cooldown: 10, + run: async (interaction, client) => { + // Guard responde automáticamente si está disabled + if (!await guardFeatureFlag('new_mining', interaction)) { + return; // Ya respondió al usuario + } + + // Tu código aquí + await interaction.reply('⛏️ Minando...'); + } +}; + +// COMANDO DE MENSAJE - EXACTAMENTE IGUAL +export const command: CommandMessage = { + name: 'mine', + type: 'message', + cooldown: 10, + run: async (message, args, client) => { + // Mismo guard funciona para mensajes + if (!await guardFeatureFlag('new_mining', message)) { + return; + } + + await message.reply('⛏️ Minando...'); + } +}; +``` + +### 3️⃣ Check Manual (Más Control) + +```typescript +import { checkFeatureFlag } from "@/core/lib/featureFlagCommandWrapper"; + +// COMANDO SLASH +export const command: CommandSlash = { + name: 'inventory', + description: 'Tu inventario', + type: 'slash', + cooldown: 5, + run: async (interaction, client) => { + const useNewUI = await checkFeatureFlag('inventory_v2', interaction); + + if (useNewUI) { + await interaction.reply('📦 Inventario v2'); + } else { + await interaction.reply('📦 Inventario v1'); + } + } +}; + +// COMANDO DE MENSAJE - IGUAL +export const command: CommandMessage = { + name: 'inventory', + type: 'message', + cooldown: 5, + run: async (message, args, client) => { + const useNewUI = await checkFeatureFlag('inventory_v2', message); + + if (useNewUI) { + await message.reply('📦 Inventario v2'); + } else { + await message.reply('📦 Inventario v1'); + } + } +}; +``` + +--- + +## 🔥 A/B Testing + +```typescript +import { abTestCommand } from "@/core/lib/featureFlagCommandWrapper"; + +// COMANDO SLASH +export const command: CommandSlash = { + name: 'attack', + description: 'Ataca', + type: 'slash', + cooldown: 10, + run: async (interaction, client) => { + await abTestCommand('new_combat', interaction, { + variant: async () => { + // Nueva versión (50% usuarios) + await interaction.reply('⚔️ Daño nuevo: 100'); + }, + control: async () => { + // Versión antigua (50% usuarios) + await interaction.reply('⚔️ Daño viejo: 50'); + } + }); + } +}; + +// COMANDO DE MENSAJE - IGUAL +export const command: CommandMessage = { + name: 'attack', + type: 'message', + cooldown: 10, + run: async (message, args, client) => { + await abTestCommand('new_combat', message, { + variant: async () => { + await message.reply('⚔️ Daño nuevo: 100'); + }, + control: async () => { + await message.reply('⚔️ Daño viejo: 50'); + } + }); + } +}; +``` + +--- + +## 💡 Ejemplo Real: Comando Universal + +```typescript +import { checkFeatureFlag } from "@/core/lib/featureFlagCommandWrapper"; +import { CommandSlash, CommandMessage } from "@/core/types/commands"; + +// Función de negocio (reutilizable) +async function executeShop(source: any) { + const useNewShop = await checkFeatureFlag('new_shop_system', source); + + const items = useNewShop + ? ['⚔️ Espada Legendaria', '🛡️ Escudo Épico'] + : ['Espada', 'Escudo']; + + const response = `🛒 **Tienda**\n${items.join('\n')}`; + + // Detectar tipo y responder + if ('options' in source) { + await source.reply(response); + } else { + await source.reply(response); + } +} + +// COMANDO SLASH +export const shopSlash: CommandSlash = { + name: 'shop', + description: 'Tienda', + type: 'slash', + cooldown: 10, + run: async (interaction, client) => { + await executeShop(interaction); + } +}; + +// COMANDO DE MENSAJE +export const shopMessage: CommandMessage = { + name: 'shop', + type: 'message', + cooldown: 10, + run: async (message, args, client) => { + await executeShop(message); + } +}; +``` + +--- + +## 📊 Configurar Flags + +```bash +# Crear flag +/featureflags create name:new_shop_system status:disabled target:global + +# Habilitar +/featureflags update flag:new_shop_system status:enabled + +# Rollout 25% de usuarios +/featureflags rollout flag:new_shop_system strategy:percentage percentage:25 + +# A/B testing (50/50) +/featureflags rollout flag:new_combat strategy:percentage percentage:50 + +# Ver estadísticas +/featureflags stats flag:new_shop_system +``` + +--- + +## ✨ Ventajas del Sistema + +✅ **Un solo código** para ambos tipos de comandos +✅ **No rompe** comandos existentes +✅ **Rollouts progresivos** sin redeploys +✅ **Kill switches** instantáneos +✅ **A/B testing** automático +✅ **Estadísticas** de uso en tiempo real + +--- + +## 🎯 Casos de Uso + +### Migración Gradual +```typescript +run: async (interaction, client) => { + const useNew = await checkFeatureFlag('new_system', interaction); + + if (useNew) { + await newSystem(interaction); + } else { + await oldSystem(interaction); + } +} +``` + +### Kill Switch +```bash +# Si hay un bug crítico +/featureflags update flag:problematic_feature status:maintenance +# Inmediatamente deshabilitado sin redeploy +``` + +### Beta Testing +```bash +# Solo para guilds específicos +/featureflags create name:beta_features status:rollout target:guild +/featureflags rollout flag:beta_features strategy:whitelist +# Luego añadir IDs de guilds en el config +``` + +### Eventos Temporales +```typescript +// Crear con fechas +await featureFlagService.setFlag({ + name: 'halloween_event', + status: 'enabled', + startDate: new Date('2025-10-25'), + endDate: new Date('2025-11-01') +}); +// Se auto-desactiva el 1 de noviembre +``` + +--- + +## 🔧 Integración en tu Bot + +Simplemente usa los helpers en cualquier comando: + +```typescript +import { withFeatureFlag } from '@/core/lib/featureFlagCommandWrapper'; + +export const command: CommandSlash = { + name: 'tu_comando', + description: 'Descripción', + type: 'slash', + cooldown: 10, + run: withFeatureFlag('tu_flag', async (interaction, client) => { + // Tu código existente aquí + }) +}; +``` + +**Eso es todo.** El sistema funciona transparentemente para ambos tipos de comandos. 🎮 diff --git a/src/core/lib/featureFlagCommandWrapper.ts b/src/core/lib/featureFlagCommandWrapper.ts new file mode 100644 index 0000000..dbc5a79 --- /dev/null +++ b/src/core/lib/featureFlagCommandWrapper.ts @@ -0,0 +1,218 @@ +/** + * Feature Flag Wrapper para Comandos + * Wrapper universal que funciona tanto para comandos slash como mensajes + */ + +import { ChatInputCommandInteraction, Message, MessageFlags } from "discord.js"; +import { featureFlagService } from "../services/FeatureFlagService"; +import { extractContext } from "../lib/featureFlagHelpers"; +import logger from "../lib/logger"; +import type Amayo from "../client"; + +/** + * Wrapper para proteger comandos con feature flags + * Funciona tanto para comandos slash como comandos de mensaje + * + * @example + * ```ts + * export const command: CommandSlash = { + * name: 'shop', + * run: withFeatureFlag('new_shop_system', async (interaction, client) => { + * // Tu código aquí + * }) + * }; + * + * export const command: CommandMessage = { + * name: 'shop', + * run: withFeatureFlag('new_shop_system', async (message, args, client) => { + * // Tu código aquí + * }) + * }; + * ``` + */ + +// Overload para comandos slash +export function withFeatureFlag( + flagName: string, + handler: ( + interaction: ChatInputCommandInteraction, + client: Amayo + ) => Promise, + options?: { + fallbackMessage?: string; + silent?: boolean; + } +): (interaction: ChatInputCommandInteraction, client: Amayo) => Promise; + +// Overload para comandos de mensaje +export function withFeatureFlag( + flagName: string, + handler: (message: Message, args: string[], client: Amayo) => Promise, + options?: { + fallbackMessage?: string; + silent?: boolean; + } +): (message: Message, args: string[], client: Amayo) => Promise; + +// Implementación +export function withFeatureFlag( + flagName: string, + handler: any, + options: { + fallbackMessage?: string; + silent?: boolean; + } = {} +): any { + return async function (...args: any[]) { + const firstArg = args[0]; + + // Determinar si es comando slash o mensaje + const isSlashCommand = + "options" in firstArg && "reply" in firstArg && "user" in firstArg; + const isMessageCommand = "content" in firstArg && "author" in firstArg; + + if (!isSlashCommand && !isMessageCommand) { + logger.error("[FeatureFlag] Tipo de comando no soportado"); + return; + } + + const context = extractContext(firstArg); + const enabled = await featureFlagService.isEnabled(flagName, context); + + if (!enabled) { + if (!options.silent) { + const message = + options.fallbackMessage || + "⚠️ Esta funcionalidad no está disponible en este momento."; + + if (isSlashCommand) { + const interaction = firstArg as ChatInputCommandInteraction; + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ + content: message, + flags: MessageFlags.Ephemeral, + }); + } else { + await interaction.reply({ + content: message, + flags: MessageFlags.Ephemeral, + }); + } + } else { + const msg = firstArg as Message; + await msg.reply(message); + } + } + + logger.debug(`[FeatureFlag] Comando bloqueado por flag "${flagName}"`); + return; + } + + // Ejecutar el handler original + return handler(...args); + }; +} + +/** + * Check rápido para usar dentro de comandos + * Devuelve true/false sin responder automáticamente + * + * @example + * ```ts + * run: async (interaction, client) => { + * if (!await checkFeatureFlag('new_system', interaction)) { + * await interaction.reply('❌ No disponible'); + * return; + * } + * // código... + * } + * ``` + */ +export async function checkFeatureFlag( + flagName: string, + source: ChatInputCommandInteraction | Message +): Promise { + const context = extractContext(source); + return await featureFlagService.isEnabled(flagName, context); +} + +/** + * Guard que responde automáticamente si el flag está disabled + * + * @example + * ```ts + * run: async (interaction, client) => { + * if (!await guardFeatureFlag('new_system', interaction)) { + * return; // Ya respondió automáticamente + * } + * // código... + * } + * ``` + */ +export async function guardFeatureFlag( + flagName: string, + source: ChatInputCommandInteraction | Message, + customMessage?: string +): Promise { + const context = extractContext(source); + const enabled = await featureFlagService.isEnabled(flagName, context); + + if (!enabled) { + const message = + customMessage || + "⚠️ Esta funcionalidad está deshabilitada temporalmente."; + + if ("options" in source && "reply" in source && "user" in source) { + // Es un comando slash + const interaction = source as ChatInputCommandInteraction; + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ + content: message, + flags: MessageFlags.Ephemeral, + }); + } else { + await interaction.reply({ + content: message, + flags: MessageFlags.Ephemeral, + }); + } + } else { + // Es un mensaje + await source.reply(message); + } + } + + return enabled; +} + +/** + * Helper para A/B testing en comandos + * + * @example + * ```ts + * run: async (interaction, client) => { + * await abTestCommand('new_algorithm', interaction, { + * variant: async () => { + * // Nueva versión + * await interaction.reply('Usando algoritmo nuevo'); + * }, + * control: async () => { + * // Versión antigua + * await interaction.reply('Usando algoritmo antiguo'); + * } + * }); + * } + * ``` + */ +export async function abTestCommand( + flagName: string, + source: ChatInputCommandInteraction | Message, + variants: { + variant: () => Promise; + control: () => Promise; + } +): Promise { + const context = extractContext(source); + const enabled = await featureFlagService.isEnabled(flagName, context); + return enabled ? variants.variant() : variants.control(); +} diff --git a/test/examples/featureFlagsCommands.ts b/test/examples/featureFlagsCommands.ts new file mode 100644 index 0000000..cff3e35 --- /dev/null +++ b/test/examples/featureFlagsCommands.ts @@ -0,0 +1,361 @@ +/** + * Ejemplos de uso de Feature Flags en comandos + * Funciona para comandos slash Y comandos de mensaje + */ + +import { ChatInputCommandInteraction, Message } from "discord.js"; +import { CommandSlash, CommandMessage } from "../../types/commands"; +import { + withFeatureFlag, + checkFeatureFlag, + guardFeatureFlag, + abTestCommand, +} from "../lib/featureFlagCommandWrapper"; +import type Amayo from "../client"; + +// ============================================================================ +// PATRÓN 1: Usando withFeatureFlag (wrapper) - RECOMENDADO +// ============================================================================ + +/** + * Comando Slash con Feature Flag + * El wrapper bloquea automáticamente si el flag está disabled + */ +export const shopSlashCommand: CommandSlash = { + name: "shop", + description: "Abre la tienda", + type: "slash", + cooldown: 10, + // Envuelve el handler con el wrapper + run: withFeatureFlag( + "new_shop_system", + async (interaction: ChatInputCommandInteraction, client: Amayo) => { + // Este código solo se ejecuta si el flag está enabled + await interaction.reply("🛒 Bienvenido a la tienda!"); + }, + { + fallbackMessage: "🔧 La tienda está en mantenimiento.", + } + ), +}; + +/** + * Comando de Mensaje con Feature Flag + * El mismo wrapper funciona para comandos de mensaje + */ +export const shopMessageCommand: CommandMessage = { + name: "shop", + type: "message", + cooldown: 10, + description: "Abre la tienda", + // El mismo wrapper funciona aquí + run: withFeatureFlag( + "new_shop_system", + async (message: Message, args: string[], client: Amayo) => { + // Este código solo se ejecuta si el flag está enabled + await message.reply("🛒 Bienvenido a la tienda!"); + }, + { + fallbackMessage: "🔧 La tienda está en mantenimiento.", + } + ), +}; + +// ============================================================================ +// PATRÓN 2: Usando guardFeatureFlag (check con respuesta automática) +// ============================================================================ + +/** + * Comando Slash con guard + */ +export const mineSlashCommand: CommandSlash = { + name: "mine", + description: "Minea recursos", + type: "slash", + cooldown: 10, + run: async (interaction, client) => { + // Guard que responde automáticamente si está disabled + if (!(await guardFeatureFlag("new_mining_system", interaction))) { + return; // Ya respondió automáticamente + } + + // Código del comando + await interaction.reply("⛏️ Minando..."); + }, +}; + +/** + * Comando de Mensaje con guard + */ +export const mineMessageCommand: CommandMessage = { + name: "mine", + type: "message", + cooldown: 10, + run: async (message, args, client) => { + // El mismo guard funciona para mensajes + if (!(await guardFeatureFlag("new_mining_system", message))) { + return; + } + + await message.reply("⛏️ Minando..."); + }, +}; + +// ============================================================================ +// PATRÓN 3: Usando checkFeatureFlag (check manual) +// ============================================================================ + +/** + * Comando Slash con check manual + * Útil cuando necesitas lógica custom + */ +export const inventorySlashCommand: CommandSlash = { + name: "inventory", + description: "Muestra tu inventario", + type: "slash", + cooldown: 5, + run: async (interaction, client) => { + const useNewUI = await checkFeatureFlag("inventory_ui_v2", interaction); + + if (useNewUI) { + // Nueva UI + await interaction.reply({ + content: "📦 **Inventario v2**\n- Item 1\n- Item 2", + ephemeral: true, + }); + } else { + // UI antigua + await interaction.reply({ + content: "📦 Inventario: Item 1, Item 2", + ephemeral: true, + }); + } + }, +}; + +/** + * Comando de Mensaje con check manual + */ +export const inventoryMessageCommand: CommandMessage = { + name: "inventory", + type: "message", + cooldown: 5, + aliases: ["inv", "items"], + run: async (message, args, client) => { + const useNewUI = await checkFeatureFlag("inventory_ui_v2", message); + + if (useNewUI) { + await message.reply("📦 **Inventario v2**\n- Item 1\n- Item 2"); + } else { + await message.reply("📦 Inventario: Item 1, Item 2"); + } + }, +}; + +// ============================================================================ +// PATRÓN 4: A/B Testing +// ============================================================================ + +/** + * Comando Slash con A/B testing + */ +export const combatSlashCommand: CommandSlash = { + name: "attack", + description: "Ataca a un enemigo", + type: "slash", + cooldown: 10, + run: async (interaction, client) => { + await abTestCommand("improved_combat_algorithm", interaction, { + variant: async () => { + // 50% de usuarios ven el nuevo algoritmo + const damage = Math.floor(Math.random() * 100) + 50; + await interaction.reply(`⚔️ Daño (nuevo): ${damage}`); + }, + control: async () => { + // 50% ven el algoritmo antiguo + const damage = Math.floor(Math.random() * 50) + 25; + await interaction.reply(`⚔️ Daño (antiguo): ${damage}`); + }, + }); + }, +}; + +/** + * Comando de Mensaje con A/B testing + */ +export const combatMessageCommand: CommandMessage = { + name: "attack", + type: "message", + cooldown: 10, + run: async (message, args, client) => { + await abTestCommand("improved_combat_algorithm", message, { + variant: async () => { + const damage = Math.floor(Math.random() * 100) + 50; + await message.reply(`⚔️ Daño (nuevo): ${damage}`); + }, + control: async () => { + const damage = Math.floor(Math.random() * 50) + 25; + await message.reply(`⚔️ Daño (antiguo): ${damage}`); + }, + }); + }, +}; + +// ============================================================================ +// PATRÓN 5: Múltiples flags (migrando sistema antiguo a nuevo) +// ============================================================================ + +/** + * Comando que migra gradualmente de un sistema a otro + */ +export const economySlashCommand: CommandSlash = { + name: "balance", + description: "Muestra tu balance", + type: "slash", + cooldown: 5, + run: async (interaction, client) => { + const useNewEconomy = await checkFeatureFlag( + "economy_system_v2", + interaction + ); + const usePremiumFeatures = await checkFeatureFlag( + "premium_features", + interaction + ); + + if (useNewEconomy) { + // Sistema nuevo de economía + const balance = 5000; + const streak = usePremiumFeatures ? "🔥 Racha: 7 días" : ""; + + await interaction.reply( + `💰 Balance: ${balance} monedas\n${streak}`.trim() + ); + } else { + // Sistema antiguo + const balance = 5000; + await interaction.reply(`💰 Tienes ${balance} monedas`); + } + }, +}; + +/** + * Lo mismo pero para comando de mensaje + */ +export const economyMessageCommand: CommandMessage = { + name: "balance", + type: "message", + cooldown: 5, + aliases: ["bal", "money"], + run: async (message, args, client) => { + const useNewEconomy = await checkFeatureFlag("economy_system_v2", message); + const usePremiumFeatures = await checkFeatureFlag( + "premium_features", + message + ); + + if (useNewEconomy) { + const balance = 5000; + const streak = usePremiumFeatures ? "🔥 Racha: 7 días" : ""; + await message.reply(`💰 Balance: ${balance} monedas\n${streak}`.trim()); + } else { + const balance = 5000; + await message.reply(`💰 Tienes ${balance} monedas`); + } + }, +}; + +// ============================================================================ +// PATRÓN 6: Comando universal (un solo run para ambos) +// ============================================================================ + +/** + * Helper para detectar tipo de comando + */ +function isSlashCommand( + source: ChatInputCommandInteraction | Message +): source is ChatInputCommandInteraction { + return "options" in source && "user" in source; +} + +/** + * Función de negocio universal + */ +async function showProfile( + source: ChatInputCommandInteraction | Message, + userId: string +) { + const useNewProfile = await checkFeatureFlag("profile_v2", source); + + const profileText = useNewProfile + ? `👤 **Perfil v2**\nUsuario: <@${userId}>\nNivel: 10` + : `👤 Perfil: <@${userId}> - Nivel 10`; + + if (isSlashCommand(source)) { + await source.reply(profileText); + } else { + await source.reply(profileText); + } +} + +/** + * Comando Slash que usa la función universal + */ +export const profileSlashCommand: CommandSlash = { + name: "profile", + description: "Muestra tu perfil", + type: "slash", + cooldown: 5, + run: async (interaction, client) => { + await showProfile(interaction, interaction.user.id); + }, +}; + +/** + * Comando de Mensaje que usa la misma función universal + */ +export const profileMessageCommand: CommandMessage = { + name: "profile", + type: "message", + cooldown: 5, + aliases: ["perfil", "me"], + run: async (message, args, client) => { + await showProfile(message, message.author.id); + }, +}; + +// ============================================================================ +// RESUMEN DE PATRONES +// ============================================================================ + +/* + * PATRÓN 1: withFeatureFlag() + * - Más limpio y declarativo + * - Bloquea automáticamente si disabled + * - Recomendado para comandos simples + * + * PATRÓN 2: guardFeatureFlag() + * - Check con respuesta automática + * - Control total del flujo + * - Bueno para lógica compleja + * + * PATRÓN 3: checkFeatureFlag() + * - Check manual sin respuesta + * - Para if/else personalizados + * - Migración gradual de sistemas + * + * PATRÓN 4: abTestCommand() + * - A/B testing directo + * - Ejecuta función u otra según flag + * - Ideal para comparar versiones + * + * PATRÓN 5: Múltiples flags + * - Combina varios checks + * - Features progresivas + * - Sistemas modulares + * + * PATRÓN 6: Función universal + * - Un solo código para ambos tipos + * - Reutilización máxima + * - Mantenimiento simplificado + */ diff --git a/test/examples/featureFlagsUsage.ts b/test/examples/featureFlagsUsage.ts deleted file mode 100644 index 4c6a8bb..0000000 --- a/test/examples/featureFlagsUsage.ts +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Ejemplo de Uso de Feature Flags en Comandos - * - * Este archivo muestra varios patrones de uso del sistema de feature flags - */ - -import { CommandInteraction, Message } from "discord.js"; -import { - RequireFeature, - featureGuard, - isFeatureEnabledForInteraction, - abTest, - extractContext, - requireAllFeatures, -} from "../core/lib/featureFlagHelpers"; - -// ============================================================================ -// Ejemplo 1: Usar decorador @RequireFeature -// ============================================================================ -export class ShopCommand { - /** - * El decorador RequireFeature bloquea automáticamente la ejecución - * si el flag no está habilitado y responde al usuario - */ - @RequireFeature("new_shop_system", { - fallbackMessage: "🔧 El nuevo sistema de tienda estará disponible pronto.", - }) - async execute(interaction: CommandInteraction) { - // Este código solo se ejecuta si el flag está habilitado - await interaction.reply("¡Bienvenido a la nueva tienda!"); - } -} - -// ============================================================================ -// Ejemplo 2: Usar featureGuard (más control) -// ============================================================================ -export async function handleMiningCommand(interaction: CommandInteraction) { - // featureGuard devuelve true/false y opcionalmente responde al usuario - if ( - !(await featureGuard("new_mining_system", interaction, { - replyIfDisabled: true, - customMessage: "⛏️ El nuevo sistema de minería está en mantenimiento.", - })) - ) { - return; // Sale si el flag está deshabilitado - } - - // Código del nuevo sistema de minería - await interaction.reply("⛏️ Iniciando minería con el nuevo sistema..."); -} - -// ============================================================================ -// Ejemplo 3: Check manual (para lógica condicional) -// ============================================================================ -export async function handleInventoryCommand(interaction: CommandInteraction) { - const useNewUI = await isFeatureEnabledForInteraction( - "inventory_ui_v2", - interaction - ); - - if (useNewUI) { - // Muestra el inventario con la nueva UI - await showInventoryV2(interaction); - } else { - // Muestra el inventario con la UI antigua - await showInventoryV1(interaction); - } -} - -async function showInventoryV2(interaction: CommandInteraction) { - await interaction.reply("📦 Inventario (UI v2)"); -} - -async function showInventoryV1(interaction: CommandInteraction) { - await interaction.reply("📦 Inventario (UI v1)"); -} - -// ============================================================================ -// Ejemplo 4: A/B Testing -// ============================================================================ -export async function handleCombatCommand(interaction: CommandInteraction) { - const context = extractContext(interaction); - - // A/B testing: mitad de usuarios usa el algoritmo nuevo, mitad el viejo - const result = await abTest("improved_combat_algorithm", context, { - variant: async () => { - // Nueva versión del algoritmo - return calculateDamageV2(); - }, - control: async () => { - // Versión antigua del algoritmo - return calculateDamageV1(); - }, - }); - - await interaction.reply(`⚔️ Daño calculado: ${result}`); -} - -function calculateDamageV2(): number { - // Lógica nueva - return Math.floor(Math.random() * 100) + 50; -} - -function calculateDamageV1(): number { - // Lógica antigua - return Math.floor(Math.random() * 50) + 25; -} - -// ============================================================================ -// Ejemplo 5: Múltiples flags (acceso premium) -// ============================================================================ -export async function handlePremiumFeature(interaction: CommandInteraction) { - const context = extractContext(interaction); - - // Requiere que TODOS los flags estén habilitados - const hasAccess = await requireAllFeatures( - ["premium_features", "beta_access", "advanced_commands"], - context - ); - - if (!hasAccess) { - await interaction.reply({ - content: "❌ No tienes acceso a esta funcionalidad premium.", - flags: ["Ephemeral"], - }); - return; - } - - await interaction.reply("✨ Funcionalidad premium activada!"); -} - -// ============================================================================ -// Ejemplo 6: Migrando de sistema antiguo a nuevo gradualmente -// ============================================================================ -export async function handleEconomyCommand(interaction: CommandInteraction) { - const useNewSystem = await isFeatureEnabledForInteraction( - "economy_system_v2", - interaction - ); - - if (useNewSystem) { - // Nuevo sistema de economía - await newEconomySystem.processTransaction(interaction); - } else { - // Sistema antiguo (mantener por compatibilidad durante el rollout) - await oldEconomySystem.processTransaction(interaction); - } -} - -// Simulación de sistemas -const newEconomySystem = { - async processTransaction(interaction: CommandInteraction) { - await interaction.reply("💰 Transacción procesada (Sistema v2)"); - }, -}; - -const oldEconomySystem = { - async processTransaction(interaction: CommandInteraction) { - await interaction.reply("💰 Transacción procesada (Sistema v1)"); - }, -}; - -// ============================================================================ -// Ejemplo 7: Eventos temporales con fechas -// ============================================================================ -export async function handleHalloweenEvent(interaction: CommandInteraction) { - // El flag 'halloween_2025' tiene startDate y endDate configurados - // Se habilitará automáticamente durante el período del evento - if ( - !(await featureGuard("halloween_2025", interaction, { - replyIfDisabled: true, - customMessage: - "🎃 El evento de Halloween no está activo en este momento.", - })) - ) { - return; - } - - await interaction.reply("🎃 ¡Bienvenido al evento de Halloween 2025!"); -} - -// ============================================================================ -// Ejemplo 8: Kill Switch para emergencias -// ============================================================================ -export async function handleProblematicFeature( - interaction: CommandInteraction -) { - // Si hay un bug crítico, el administrador puede cambiar el flag a 'maintenance' - // inmediatamente sin necesidad de redeploy - if ( - !(await featureGuard("experimental_feature", interaction, { - replyIfDisabled: true, - customMessage: - "🔧 Esta funcionalidad está en mantenimiento temporalmente.", - })) - ) { - return; - } - - // Código que podría tener bugs - await experimentalLogic(interaction); -} - -async function experimentalLogic(interaction: CommandInteraction) { - await interaction.reply("🧪 Funcionalidad experimental activada"); -} - -// ============================================================================ -// Ejemplo 9: Beta Testing por Guild (servidor) -// ============================================================================ -export async function handleBetaCommand(interaction: CommandInteraction) { - // El flag 'beta_features' está configurado con: - // - target: 'guild' - // - rolloutStrategy: 'whitelist' - // - rolloutConfig: { targetIds: ['guild_id_1', 'guild_id_2'] } - - const context = extractContext(interaction); - - if ( - !(await featureGuard("beta_features", interaction, { - replyIfDisabled: true, - customMessage: - "🔒 Tu servidor no tiene acceso a las funcionalidades beta.", - })) - ) { - return; - } - - await interaction.reply( - "🧪 Funcionalidades beta activadas para este servidor!" - ); -} - -// ============================================================================ -// Ejemplo 10: Rollout progresivo por porcentaje -// ============================================================================ -export async function handleNewGameMode(interaction: CommandInteraction) { - // El flag 'new_game_mode' está configurado con: - // - status: 'rollout' - // - rolloutStrategy: 'percentage' - // - rolloutConfig: { percentage: 25 } - // - // Esto significa que el 25% de usuarios verán el nuevo modo de juego - // de forma determinista (el mismo usuario siempre verá lo mismo) - - if (!(await featureGuard("new_game_mode", interaction))) { - return; - } - - await interaction.reply("🎮 ¡Nuevo modo de juego desbloqueado!"); -} - -// ============================================================================ -// Ejemplo 11: Usando en Message Commands (comandos de texto) -// ============================================================================ -export async function handleTextCommand(message: Message, args: string[]) { - // También funciona con comandos de texto tradicionales - const context = extractContext(message); - - const useNewParser = await isFeatureEnabledForInteraction( - "new_command_parser", - message - ); - - if (useNewParser) { - await parseCommandV2(message, args); - } else { - await parseCommandV1(message, args); - } -} - -async function parseCommandV2(message: Message, args: string[]) { - await message.reply("Comando parseado con parser v2"); -} - -async function parseCommandV1(message: Message, args: string[]) { - await message.reply("Comando parseado con parser v1"); -} - -// ============================================================================ -// RESUMEN DE PATRONES -// ============================================================================ -/* - * 1. @RequireFeature - Para bloquear métodos enteros fácilmente - * 2. featureGuard - Para checks con respuesta automática al usuario - * 3. isFeatureEnabled - Para lógica condicional if/else - * 4. abTest - Para A/B testing - * 5. requireAllFeatures - Para requerir múltiples flags (AND) - * 6. requireAnyFeature - Para requerir al menos uno (OR) - * 7. withFeature - Para ejecutar código con fallback opcional - * - * Configuración de flags vía comando: - * /featureflags create name:flag_name status:disabled target:global - * /featureflags update flag:flag_name status:enabled - * /featureflags rollout flag:flag_name strategy:percentage percentage:25 - */ diff --git a/tsconfig.json b/tsconfig.json index bec4ed8..112f686 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "skipLibCheck": true, "resolveJsonModule": true }, - "include": ["src", "test/examples"] + "include": ["src", "test/examples", "test/examples"] } \ No newline at end of file