npm package: @hylandia/strata-framework
Description: A powerful TypeScript framework for building Discord bots with built-in sharding, caching, and IPC
Author: Hylandia
Latest version: 1.0.4

# Strata Discord Bot Framework

A powerful TypeScript framework for building Discord bots with built-in sharding support, strongly-typed caching, IPC communication, and automatic command handling.

## Features

- **Automatic Sharding**: Multi-cluster architecture for horizontal scaling
- **Single Instance Mode**: Simple development mode without sharding overhead
- **Command Router**: Automatic command loading from directory with guild/global registration
- **Cross-Shard Resource Fetching**: Fetch Discord resources (guilds, users, channels) across shards
- **Strongly Typed Cache**: Type-safe Redis-backed caching with generic schemas
- **IPC System**: Redis pub/sub for type-safe inter-process communication between shards
- **Global Logger**: Pino logger automatically available without imports
- **Console Blocking**: TypeScript and ESLint enforcement to prevent console usage

## Setup

### Prerequisites

- Node.js v18+
- Bun (for development)
- Redis server
- Discord bot token

### Installation

```bash
bun install
```

### Environment Configuration

Create a `.env` file in the project root:

```env
DISCORD_TOKEN=your_discord_bot_token_here
NODE_ENV=development
```

## Sharding Configuration

### Single Instance Mode (Development)

For local development or small bots, use single instance mode:

```typescript
import { Strata, BotInstance } from "./framework/strata";
import { IntentsBitField } from "discord.js";

const strata = (await new Strata({
  token: process.env.DISCORD_TOKEN!,
  intents: [
    IntentsBitField.Flags.Guilds,
    IntentsBitField.Flags.GuildMessages,
    IntentsBitField.Flags.MessageContent,
  ],
  commandsDir: "./dist/commands", // Optional: Auto-load commands
  guilds: ["123456789"], // Optional: Guild IDs for guild-only commands
  sharding: {
    enabled: false,
  },
}).start()) as BotInstance;

while (!strata.client.isReady()) {
  await new Promise((resolve) => setTimeout(resolve, 100));
}

logger.info(`Logged in as ${strata.client.user.tag}`);
```

### Sharding Mode (Production)

For production deployments with multiple clusters and shards:

**Main Entry Point (`src/bot.ts`):**

```typescript
import "dotenv/config";
import { Strata } from "./framework/strata";
import { IntentsBitField } from "discord.js";

const strata = new Strata({
  token: process.env.DISCORD_TOKEN!,
  intents: [
    IntentsBitField.Flags.Guilds,
    IntentsBitField.Flags.GuildMessages,
    IntentsBitField.Flags.MessageContent,
  ],
  sharding: {
    enabled: true,
    clusters: 2, // Number of Node.js cluster processes
    shards: 4, // Total number of Discord shards
    jsFile: "./dist/shard.js", // Path to worker file
  },
});

strata.start();
```

**Worker File (`src/shard.ts`):**

```typescript
import { BotInstance, Strata } from "../src/framework/strata";
import { IntentsBitField } from "discord.js";

const strata = (await new Strata({
  token: process.env.DISCORD_TOKEN!,
  intents: [
    IntentsBitField.Flags.Guilds,
    IntentsBitField.Flags.GuildMessages,
    IntentsBitField.Flags.MessageContent,
  ],
  sharding: {
    enabled: false, // Disable sharding in worker
  },
  guilds: ["123456789"], // Optional: Guild IDs for guild-only commands
  commandsDir: "./dist/commands", // Optional: Auto-load commands
}).start()) as BotInstance;

while (!strata.client.isReady()) {
  await new Promise((resolve) => setTimeout(resolve, 100));
}

logger.info(
  `Logged in as ${strata.client.user.tag} (Shard ${strata.client.shard?.ids})`
);
```

**Note**: In sharding mode, `commandsDir` and `guilds` are configured in the worker file (`shard.ts`), not in the main cluster manager (`bot.ts`).

### How Sharding Works

1. **Cluster Manager**: The main process spawns multiple Node.js cluster workers
2. **Shard Manager**: Each cluster spawns Discord.js shards using `jsFile`
3. **Distribution**: Shards are evenly distributed across clusters
4. **Auto-restart**: Crashed clusters automatically restart

Example with 2 clusters and 4 shards:

- **Cluster 0**: Shards 0, 1
- **Cluster 1**: Shards 2, 3

## Building & Running

### Development

```bash
bun run dev
```

### Production Build

```bash
bun run build
```

This creates two bundled files:

- `dist/bot.js` - Cluster manager entry point
- `dist/shard.js` - Worker file for shards

### Start Production

```bash
bun run start
# or
node dist/bot.js
```

## Using the Framework

### Commands

Create commands by extending the `Command` abstract class:

```typescript
import { SlashCommandBuilder, EmbedBuilder } from "discord.js";
import { Command } from "../framework/commands/Command";
import type { ExecuteData } from "../types/command";

export default class PingCommand extends Command {
  guildOnly = true; // Optional: Only available in guilds
  description = "Check bot latency"; // Optional: For help menus
  usage = "/ping"; // Optional: Custom usage (auto-generated from options if not provided)

  public data = new SlashCommandBuilder()
    .setName("ping")
    .setDescription("Check bot latency");

  public async execute({ interaction, client, strata, commands }: ExecuteData) {
    // Return a string
    return "Pong!";

    // Or return an EmbedBuilder
    return new EmbedBuilder()
      .setTitle("Pong!")
      .setDescription(`Latency: ${client.ws.ping}ms`);

    // Or return full reply options
    return {
      content: "Pong!",
      ephemeral: true,
      withResponse: true as const,
    };

    // Or handle the reply yourself (return void/nothing)
    await interaction.reply({ content: "Pong!" });
  }
}
```

**ExecuteData Properties:**

- `interaction` - The ChatInputCommandInteraction
- `client` - Discord.js Client instance (ready state)
- `strata` - Full BotInstance with cache, IPC, and resource fetching
- `commands` - CommandRouter to access all loaded commands

**Command Properties:**

- `data` - SlashCommandBuilder (required)
- `execute()` - Command handler (required)
- `description` - Description for help menus (optional)
- `usage` - Usage string (optional, auto-generated from slash command options)
- `guildOnly` - Only register in specified guilds (optional)
- `category` - Command category (optional)
- `cooldown` - Cooldown in seconds (optional)
- `permission` - Required permission(s) (optional)

**Advanced Example - Paginated Help Menu:**

```typescript
import {
  SlashCommandBuilder,
  EmbedBuilder,
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  ComponentType,
} from "discord.js";

export default class HelpCommand extends Command {
  public data = new SlashCommandBuilder()
    .setName("help")
    .setDescription("Shows all available commands");

  public async execute({ interaction, commands }: ExecuteData) {
    const allCommands = commands.getAllCommands();
    let currentPage = 0;
    const totalPages = Math.ceil(allCommands.length / 5);

    const generateEmbed = (page: number) => {
      const pageCommands = allCommands.slice(page * 5, (page + 1) * 5);
      const embed = new EmbedBuilder()
        .setTitle("📚 Command Help")
        .setFooter({ text: `Page ${page + 1} of ${totalPages}` });

      for (const cmd of pageCommands) {
        embed.addFields({
          name: `/${cmd.data.name}`,
          value: cmd.description || "No description",
        });
      }
      return embed;
    };

    const generateButtons = (page: number) => {
      return new ActionRowBuilder<ButtonBuilder>().addComponents(
        new ButtonBuilder()
          .setCustomId("prev")
          .setLabel("Previous")
          .setStyle(ButtonStyle.Primary)
          .setDisabled(page === 0),
        new ButtonBuilder()
          .setCustomId("next")
          .setLabel("Next")
          .setStyle(ButtonStyle.Primary)
          .setDisabled(page === totalPages - 1)
      );
    };

    await interaction.reply({
      embeds: [generateEmbed(currentPage)],
      components: [generateButtons(currentPage)],
    });

    const message = await interaction.fetchReply();
    const collector = message.createMessageComponentCollector({
      componentType: ComponentType.Button,
      time: 300_000,
    });

    collector.on("collect", async (i) => {
      if (i.user.id !== interaction.user.id) {
        await i.reply({ content: "Not for you!", ephemeral: true });
        return;
      }

      currentPage = i.customId === "next" ? currentPage + 1 : currentPage - 1;
      await i.update({
        embeds: [generateEmbed(currentPage)],
        components: [generateButtons(currentPage)],
      });
    });

    collector.on("end", () => interaction.editReply({ components: [] }));
  }
}
```

Commands are automatically:

- Loaded from the `commandsDir` directory (recursive scan)
- Registered with Discord API (global or guild-specific)
- Routed to the correct handler on interaction
- Given access to strata instance and all other commands

**Guild-Only Commands**: If a command has `guildOnly = true`, it will only be registered to guilds specified in the `guilds` array. You must provide the `guilds` array in `StrataOptions` or the command will be skipped.

### Cross-Shard Resource Fetching

Fetch Discord resources from any shard with automatic cross-shard communication:

```typescript
// Fetch a specific guild (cross-shard if needed)
const guild = await strata.get("guilds", "1234567890");
if (guild) {
  logger.info(`Guild: ${guild.name}, Members: ${guild.memberCount}`);
}

// Fetch all guilds across all shards (deduplicated)
const allGuilds = await strata.get("guilds");
logger.info(`Total guilds: ${allGuilds.length}`);

// Fetch users
const user = await strata.get("users", "9876543210");
const allUsers = await strata.get("users");

// Fetch channels
const channel = await strata.get("channels", "1111111111");
const allChannels = await strata.get("channels");
```

Features:

- Automatic shard routing using Discord's shard formula
- Cross-shard IPC via Redis pub/sub
- Deduplication of results across shards
- Fallback to Discord API if not in cache
- Type-safe with method overloads

### Global Logger

The logger is automatically available globally without imports:

```typescript
logger.info("Bot started");
logger.warn("Something went wrong");
logger.error("Critical error", error);
```

### Type-Safe Cache

Define your cache schema and use it with full type safety:

```typescript
type MyCacheSchema = {
  'user:123': { id: string; name: string; level: number };
  'config:theme': 'light' | 'dark';
  'guild:456': { name: string; memberCount: number };
};

const strata = await new Strata<MyCacheSchema>({ ... }).start();

// Fully typed
await strata.cache.set('user:123', { id: '123', name: 'Alice', level: 5 });
const user = await strata.cache.get('user:123'); // Type: { id: string; name: string; level: number } | null
```

### Type-Safe IPC

Define IPC operations with typed data and responses using Redis pub/sub:

```typescript
type MyIPCSchema = {
  'user:fetch': {
    data: { userId: string };
    response: { name: string; level: number }
  };
  'stats:update': {
    data: { metric: string; value: number };
    response: void
  };
};

const strata = await new Strata<Record<string, unknown>, MyIPCSchema>({ ... }).start();

// Register handler on each shard
strata.IPC.register('user:fetch', async (data) => {
  const user = await fetchUser(data.userId);
  return { name: user.name, level: user.level };
});

// Request from any shard (with 5-second timeout)
const user = await strata.IPC.request('user:fetch', { userId: '123' });
logger.info(`User: ${user.name}, Level: ${user.level}`);

// Broadcast to all shards
strata.IPC.broadcast('stats:update', { metric: 'commands', value: 1 });
```

Features:

- Redis pub/sub for shard-to-shard communication
- Unique response channels per request
- 5-second timeout with automatic cleanup
- Broadcast messages to all shards
- Type-safe data and responses

## Scripts

- `bun run dev` - Run in development mode with SINGLETON environment variable
- `bun run build` - Build all test files to dist/
- `bun run start` - Start production build (runs dist/bot.js)
- `bun run lint` - Run ESLint on src/
- `bun run lint:fix` - Run ESLint with auto-fix
- `bun run build` - Build both bot.js and shard.js
- `bun run build:bot` - Build only bot.js
- `bun run build:shard` - Build only shard.js
- `bun run start` - Start production build
- `bun run lint` - Run ESLint

## Architecture

```
┌─────────────────────────────────────┐
│     Primary Process (bot.js)        │
│    ClusterManager spawns workers    │
└─────────────┬───────────────────────┘
              │
    ┌─────────┴──────────┐
    │                    │
┌───▼────────┐    ┌──────▼────┐
│ Cluster 0  │    │ Cluster 1 │
│ Shards 0-1 │    │ Shards 2-3│
└────────────┘    └───────────┘
     │                  │
     │  Each spawns     │
     │  shard.js        │
     │  workers         │
     └──────┬───────────┘
            │
    ┌───────▼────────┐
    │  Discord API   │
    └────────────────┘
```

## License

MIT