npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

discord-cluster

v1.1.2

Published

Transparent cross-cluster operations for discord.js bots. Cache first, REST fallback, type-safe IPC, shared state, and more.

Downloads

465

Readme

discord-cluster

Stop using broadcastEval. It serializes functions as strings, eval()s them remotely, loses all TypeScript types, and broadcasts to every cluster even when only one has the data you need.

discord-cluster makes cross-cluster operations feel like normal discord.js code — cache first, targeted IPC, REST fallback, full types.

npm version License: MIT Node.js Docs


Quick Start

npm install discord-cluster

Manager (spawns clusters)

import { ClusterManager, ProcessGuard } from 'discord-cluster';

const manager = new ClusterManager('./bot.js', {
  mode: 'worker',
  token: process.env.DISCORD_TOKEN,
  totalShards: -1,
  totalClusters: -1,
  logging: { enabled: true },
});

const guard = new ProcessGuard(manager);

guard.addCleanupTask('killClusters', async () => {
  for (const cluster of manager.clusters.values()) {
    await cluster.kill({ reason: 'Manager shutting down' });
  }
}, 10000);

manager.spawn();

Bot file (runs in each cluster)

import { ClusterClient, ClusterProcessGuard } from 'discord-cluster';
import { Client, GatewayIntentBits } from 'discord.js';

const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const cluster = new ClusterClient(client);
const guard = new ClusterProcessGuard();

client.login(process.env.DISCORD_TOKEN);

ClusterClient automatically patches shard options and triggers ready. ClusterProcessGuard self-terminates the cluster if the manager dies.

Full documentation →


Real-World Example

A slash command that fetches a guild from any cluster — no broadcastEval, no eval, no string serialization:

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'server-info') {
    const guildId = interaction.options.getString('guild', true);

    const guild = await cluster.guilds.fetch(guildId);
    // Checks local cache → IPC to correct cluster → REST fallback

    if (!guild) return interaction.reply('Guild not found.');

    await interaction.reply(`**${guild.name}** — ${guild.memberCount} members`);
  }
});

One line. The library handles cache lookup, shard math routing, IPC, and REST fallback automatically.


Features

| Feature | Description | |---------|-------------| | Transparent API | cluster.guilds.fetch(), cluster.channels.send(), cluster.members.fetch() work across clusters automatically | | Type-Safe IPC | Named request/response handlers with full TypeScript types | | Shared Store | Cross-cluster key-value store with TTL, sub-millisecond latency | | Cross-Cluster Events | Pub/sub between clusters with optional targeting | | Structured Results | ResultCollection with .values(), .errors(), .sum(), .allOk() | | Process Guard | Orphan detection, stale cleanup, graceful shutdown with cleanup tasks | | Rolling Restarts | Zero-downtime restarts with manager.rollingRestart() | | Logger | Built-in colored logging with configurable levels |


Transparent API

Access data from any cluster like you would with discord.js normally:

const guild = await cluster.guilds.fetch('guildId');
const guildCount = await cluster.guilds.count();

const channel = await cluster.channels.fetch('channelId');
await cluster.channels.send('channelId', { content: 'Hello' });

const member = await cluster.members.fetch('guildId', 'userId');
await cluster.members.addRole('guildId', 'userId', 'roleId');
await cluster.members.removeRole('guildId', 'userId', 'roleId');
await cluster.members.ban('guildId', 'userId', { reason: 'bad' });

const user = await cluster.users.fetch('userId');
await cluster.users.send('userId', { content: 'DM' });

Type-Safe IPC

Named handlers replace broadcastEval. Normal code, full types, no eval:

cluster.ipc.handle('getGuildInfo', async (data) => {
  const guild = client.guilds.cache.get(data.guildId);
  if (!guild) return null;
  return { id: guild.id, name: guild.name, memberCount: guild.memberCount };
});

const info = await cluster.ipc.request('getGuildInfo', { guildId: '123' });

const info = await cluster.ipc.requestTo(2, 'getGuildInfo', { guildId: '123' });

const results = await cluster.ipc.requestAll('getGuildCount');
results.values()    // [1200, 1350, 1280]
results.errors()    // [{ clusterId: 2, error: '...' }]
results.sum()       // 3830
results.allOk()     // true

Shared Store

Cross-cluster key-value store. Manager holds the data, clusters read/write via IPC:

await cluster.store.set('cooldown:userId', Date.now(), { ttl: 30000 });
const val = await cluster.store.get('cooldown:userId');
const exists = await cluster.store.has('cooldown:userId');
await cluster.store.delete('cooldown:userId');

Cross-Cluster Events

cluster.events.broadcast('settingsUpdated', { guildId: '123' });

cluster.events.emitTo(3, 'reloadConfig', {});

cluster.events.on('settingsUpdated', (data) => {
  settingsCache.delete(data.guildId);
});

Stats & Utilities

const stats = await cluster.stats();
// { totalGuilds, totalUsers, totalClusters, totalShards, clusters: [...] }

cluster.findGuild('guildId')  // → clusterId
cluster.findShard('guildId')  // → shardId

cluster.id          // current cluster id
cluster.shards      // [0, 1, 2, 3]
cluster.isPrimary   // cluster.id === 0
cluster.totalShards
cluster.totalClusters

Process Guard

Manager side

Graceful shutdown with cleanup tasks, signal handling, stale process cleanup:

const guard = new ProcessGuard(manager);

guard.addCleanupTask('saveState', async () => {
  await db.flush();
}, 5000);

guard.addCleanupTask('killClusters', async () => {
  for (const cluster of manager.clusters.values()) {
    await cluster.kill({ reason: 'Shutting down' });
  }
}, 10000);

Cluster side

Monitors parent PID. If the manager dies, the cluster self-terminates:

const guard = new ClusterProcessGuard();

Logging

Built-in colored console output. Disabled by default:

const manager = new ClusterManager('./bot.js', {
  logging: {
    enabled: true,
    colors: true,
    timestamps: true,
    level: 'info',       // 'debug' | 'info' | 'warn' | 'error'
  },
});

manager.logger.info('[MyApp] Custom message');
manager.logger.warn('[MyApp] Warning');
manager.logger.error('[MyApp] Error');

Why Not broadcastEval?

| | discord-cluster | broadcastEval | |--|:-:|:-:| | TypeScript types | Full | Lost | | Targeted requests | Math routing | Broadcast to all | | Return types | Typed | unknown | | Error handling | Per-cluster | All or nothing | | Shared state | Built-in store | DIY | | Events | Pub/sub | None | | Code | Normal functions | Serialized strings |


const manager = new ClusterManager('./bot.js', {
  mode: 'worker',              // 'worker' | 'process'
  token: process.env.DISCORD_TOKEN,

  totalShards: -1,             // -1 = auto from Discord API
  totalClusters: -1,           // -1 = auto (based on CPU cores)
  shardsPerClusters: -1,       // -1 = auto (totalShards / totalClusters)

  spawnOptions: {
    timeout: -1,               // Spawn timeout (-1 = none)
    delay: 7000,               // Delay between cluster spawns
  },

  heartbeat: {
    enabled: true,
    interval: 2000,
    timeout: 8000,
    maxMissedHeartbeats: 2,
    maxRestarts: -1,           // -1 = unlimited
  },

  respawn: true,

  logging: {
    enabled: false,
    colors: true,
    timestamps: true,
    level: 'info',
  },

  queueOptions: {
    mode: 'auto',              // 'auto' | 'manual'
  },

  shardArgs: [],
  execArgv: [],
});

Requirements

  • Node.js 18.4.0+
  • discord.js 14.14.1+

License

MIT © LucasCzechia