feat: Agregar ejemplos de uso de feature flags en comandos y mejorar la configuración del proyecto
This commit is contained in:
311
README/FEATURE_FLAGS_COMANDOS.md
Normal file
311
README/FEATURE_FLAGS_COMANDOS.md
Normal file
@@ -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. 🎮
|
||||||
218
src/core/lib/featureFlagCommandWrapper.ts
Normal file
218
src/core/lib/featureFlagCommandWrapper.ts
Normal file
@@ -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<void>,
|
||||||
|
options?: {
|
||||||
|
fallbackMessage?: string;
|
||||||
|
silent?: boolean;
|
||||||
|
}
|
||||||
|
): (interaction: ChatInputCommandInteraction, client: Amayo) => Promise<void>;
|
||||||
|
|
||||||
|
// Overload para comandos de mensaje
|
||||||
|
export function withFeatureFlag(
|
||||||
|
flagName: string,
|
||||||
|
handler: (message: Message, args: string[], client: Amayo) => Promise<void>,
|
||||||
|
options?: {
|
||||||
|
fallbackMessage?: string;
|
||||||
|
silent?: boolean;
|
||||||
|
}
|
||||||
|
): (message: Message, args: string[], client: Amayo) => Promise<void>;
|
||||||
|
|
||||||
|
// 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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<T = void>(
|
||||||
|
flagName: string,
|
||||||
|
source: ChatInputCommandInteraction | Message,
|
||||||
|
variants: {
|
||||||
|
variant: () => Promise<T>;
|
||||||
|
control: () => Promise<T>;
|
||||||
|
}
|
||||||
|
): Promise<T> {
|
||||||
|
const context = extractContext(source);
|
||||||
|
const enabled = await featureFlagService.isEnabled(flagName, context);
|
||||||
|
return enabled ? variants.variant() : variants.control();
|
||||||
|
}
|
||||||
361
test/examples/featureFlagsCommands.ts
Normal file
361
test/examples/featureFlagsCommands.ts
Normal file
@@ -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
|
||||||
|
*/
|
||||||
@@ -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
|
|
||||||
*/
|
|
||||||
@@ -11,5 +11,5 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": ["src", "test/examples"]
|
"include": ["src", "test/examples", "test/examples"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user