Open KieronWiltshire opened 2 years ago
Hi! Can you provide minimal reproduction repository? NestJS and discord.js are not directly related in any way. How would you like to see sharding out of the box?
Yeah I understand that NestJS and discord aren't directly related. To be honest, I'd prefer something like this...
|- src
main.ts
app.module.ts
bot
- bot.module.ts
- bot.gateway.ts
then in app.module.ts
it would be awesome to do something like
const manager = new ShardManager(path.join(__dirname, 'bot', 'bot.module.ts'), { token: configService.get<string>('discord.bot.token')});
manager.spawn();
Yeah I understand that NestJS and discord aren't directly related. To be honest, I'd prefer something like this...
|- src main.ts app.module.ts bot - bot.module.ts - bot.gateway.ts
then in
app.module.ts
it would be awesome to do something likeconst manager = new ShardManager(path.join(__dirname, 'bot', 'bot.module.ts'), { token: configService.get<string>('discord.bot.token')}); manager.spawn();
app.module.ts
is not a bootstrap. You must specify path to the main.ts
file.
/* spawn-shards.ts */
async function createShardingManager() {
const appContext = await NestFactory.createApplicationContext(ConfigModule);
const configService = appContext.get(ConfigService);
const manager = new ShardManager(/* path to main.ts/js */, { token: configService.get<string>('discord.bot.token')});
appContext.close();
return manager;
}
createShardingManager().then((manager) => {
manager.spawn();
});
For anyone coming across this, I have found a solution using @fjodor-rybakov help, follow the steps below. Hopefully the documentation will be updated to include this.
In your src
directory, create the following files:
You may also need to add a webpack.config.js
file to your root directory which exports the bot.ts
as it's not automatically exported with the application due to how the bot.ts
file is used within another process that webpack is unable to detect. You can use the following snippet:
const Path = require('path');
module.exports = function (options) {
return {
...options,
entry: {
server: options.entry,
bot: Path.join(__dirname, 'src', 'bot.ts')
},
output: {
filename: '[name].js'
}
};
};
Secondly, change your entryPoint
in your nest-cli.json
file to server
. The server.ts
file will become our entry point for instantiating the HTTP server and the Bot. If you do not need a HTTP server, then you can make the adjustments yourself, this example assumes you want both.
Steps:
main.ts
and export the function instead. You can keep everything in your main.ts
as it was aside from these changes. This will be your HTTP server.shared.module.ts
, I have provided an example below.bot.module.ts
file, you're going to want to load up the DiscordModule.forRootAsync
and DiscordModule.forFeature
which should exist from when you created it following this package's readme file. You'll also want to include the SharedModule
for anything you wish to use within the bot. I have provided an example below.bot.ts
file, you'll want to copy the code below.server.ts
file you'll also want to copy the code below.npm run start
exactly how you did before, except now it will start the server.ts
file which in turn launches the HTTP server and creates a ShardingManager
instance which in turn loads your bot.ts
as shard processes.import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import appConfig from './config/app.config';
import discordConfig from './config/discord.config';
@Module({
imports: [
ConfigModule.forRoot({
cache: true,
isGlobal: true,
load: [
appConfig,
discordConfig,
]
}),
],
})
export class SharedModule {}
import { Module } from '@nestjs/common';
import { SharedModule } from "./shared.module";
@Module({
imports: [
SharedModule
],
})
export class AppModule {}
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
export async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
const config = app.get(ConfigService);
if (config.get<boolean>('app.debug')) {
app.getHttpAdapter().getInstance().set('json spaces', 2);
}
const port = config.get<string>('app.port');
await app.listen(port);
}
import { NestFactory } from '@nestjs/core';
import { BotModule } from "./bot/bot.module";
async function bootstrap() {
const app = await NestFactory.createApplicationContext(BotModule);
}
bootstrap();
import { NestFactory } from "@nestjs/core";
import { ConfigService } from "@nestjs/config";
import * as Path from "path";
import { ShardingManager } from 'discord.js';
import { SharedModule } from "./shared.module";
import { bootstrap } from './main';
async function createShardingManager() {
const appContext = await NestFactory.createApplicationContext(SharedModule);
const config = appContext.get(ConfigService);
const manager = new ShardingManager(Path.join(__dirname, 'bot.js'), {
token: config.get<string>('discord.bot.token')
});
return manager;
}
bootstrap().then(() => createShardingManager()).then((manager) => {
manager.spawn();
manager.on("shardCreate", shard => {
shard.on('reconnecting', () => {
console.log(`Reconnecting shard: [${shard.id}]`);
});
shard.on('spawn', () => {
console.log(`Spawned shard: [${shard.id}]`);
});
shard.on('ready', () => {
console.log(` Shard [${shard.id}] is ready`);
});
shard.on('death', () => {
console.log(`Died shard: [${shard.id}]`);
});
shard.on('error', (err)=>{
console.log(`Error in [${shard.id}] with : ${err} `)
shard.respawn()
})
});
});
import { Module } from '@nestjs/common';
import { DiscordModule } from '@discord-nestjs/core';
import { BotGateway } from './bot.gateway'
import { GatewayIntentBits } from "discord.js";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { SharedModule } from "../shared.module";
@Module({
imports: [
SharedModule,
DiscordModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
token: config.get<string>('discord.bot.token'),
discordClientOptions: {
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildWebhooks,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent
],
},
}),
inject: [ConfigService],
}),
DiscordModule.forFeature()
],
providers: [BotGateway]
})
export class BotModule {}
My directory structure looks something as shown below. My advice would be to keep all your bot related code within the bot
sub directory.
|- [src]
|- [bot]
|- bot.gateway.ts
|- bot.module.ts
|- [config]
|- app.config.ts
|- discord.config.ts
|- app.module.ts
|- bot.ts
|- main.ts
|- server.ts
|- shared.module.ts
Note, if you're using the discord-hybrid-sharding
package found here, then just change your server.ts
to use that shard manager instead of the built in discord.js
. That will allow you to scale using discord-cross-hosting
package found here.
@KieronWiltshire Thank you for your comment, however I'm wondering if there is anything to prevent each shard from registering the commands. My understanding is that by default the library will register all commands as application commands, and if there are multiple shards each of them is going to try to perform that registration
@DJDavid98 not sure I understand your issue
The library register the commands by default, as per the docs:
registerCommandOptions
- Specific registration of slash commands(If option is not set, global commands will be registered)
So each time a shard starts up, it will register the global commands, that is my understanding at least. My existing bot did not use this and there all I did was only let shard 0 update the commands, but I'm not sure such filtering is possible here.
I'm still not sure I understand your problem. Link me to the docs in question please.
Here are the docs: https://github.com/fjodor-rybakov/discord-nestjs/tree/master/packages/core#%E2%84%B9%EF%B8%8F-automatic-registration-of-slash-commands-
Without sharding, the BotModule
exists in a single instance, registers the commands globally upon registration, no issue there.
As soon as you introduce sharding, the BotModule
module will be instantiated for each shard and will execute the command registration logic independently. This is my primary concern, that if you start 10 shards, the bot will register its commands 10 times.
@DJDavid98 why does that bother you? in theory thats how it works... thats how it's suggested in the Discordjs docs or at least implied anyway... https://discordjs.guide/sharding/#when-to-shard
What is the exact problem you're having?
Command registration needs to happen only once during application startup. By running the command registration multiple times there is a possibility for race conditions as multiple processes simultaneously start registering commands, and in case the removeCommandsBefore
option is provided it will potentially try to delete and re-register commands multiple times during a single application start. I manually started 50 shards for my bot and each of them is running this registration process, which is quite wasteful.
I see, but other than it being "wasteful" it works as intended right?
Technically speaking, for my essentially "hello world" bot at this stage, yes, it currently does. However for a larger bot with multiple commands where registering them might take longer, I feel like this can cause issues later down the line. I specifically wanted to ask if there is some way you are aware of to prevent this. I tried to look into using trigger
-based registration based on the options shown in the docs, but when it comes to that option they are a bit lacking.
To be fair, I've never created a full featured discord bot. I've always just loved poking around at things, the intentions are there to build a bot at some point, but not in my immediate scope for the project I work on. So for full transparency, I have no idea if sharding in this manner actually works at scale. I personally just struggled to get this working, and I thought if I ever was going to make a bot using NestJS, I'd want to make sure it can absolutely be done. So I went out and did some research on this and came back with the method above, but again other than getting it to just "work," I have no idea.
Now with all that being said, if you'd want to try something else instead of discord-nestjs, I would highly recommend Necord. I wrote up the sharding part into the docs on there and the main dev is extremely active in taking feature requests etc... so if this is still an issue over on the necord package, bring it up to the dev on the necord discord. He will gladly patch any issues you're facing I'm almost sure!
I'm sorry I can't be of more help to you :(
Alright, thanks for the suggestion, I will check it out
Is your feature request related to a problem? Please describe. I tried creating 2 apps within nestjs and using the shard manager to instantiate the other, but I keep getting an error,
SHARDING_READY_TIMEOUT Shard 0's Client took too long to become ready.
.Describe the solution you'd like I'd like to be able to specify within the config the sharding requirements and have it all handled behind the scenes.
Describe alternatives you've considered I've tried creating a single file I could specify in the sharding manager and a separate nestjs app under a monorepo with no success.