Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class Client extends EventEmitter {
* @param {Object} [options.gateway.disableEvents] If disableEvents[eventName] is true, the WS event will not be processed. This can cause significant performance increase on large bots. [A full list of the WS event names in Discord's documentation](https://discord.com/developers/docs/topics/gateway-events#receive-events)
* @param {Number} [options.gateway.firstShardID=0] The ID of the first shard to run for this client
* @param {Boolean} [options.gateway.getAllUsers=false] Get all the users in every guild. Ready time will be severely delayed
* @param {Number} [options.gateway.guildCreateTimeout=2000] How long in milliseconds to wait for a GUILD_CREATE before "ready" is fired. Increase this value if you notice missing guilds
* @param {Number} [options.gateway.guildCreateTimeout=500] How long in milliseconds to wait for a GUILD_CREATE before "ready" is fired. Increase this value if you notice missing guilds
* @param {Number | Array<String | Number>} [options.gateway.intents] A list of [intent names](https://github.com/projectdysnomia/dysnomia/blob/dev/lib/Constants.js#L311), pre-shifted intent numbers to add, or a raw bitmask value describing the intents to subscribe to. Some intents, like `guildPresences` and `guildMembers`, must be enabled on your application's page to be used. By default, all non-privileged intents are enabled.
* @param {Number} [options.gateway.largeThreshold=250] The maximum number of offline users per guild during initial guild data transmission
* @param {Number} [options.gateway.lastShardID=options.maxShards - 1] The ID of the last shard to run for this client
Expand Down Expand Up @@ -416,7 +416,7 @@ class Client extends EventEmitter {
throw new Error(`Invalid token "${this._token}"`);
}
try {
const data = await (this.shards.options.maxShards === "auto" || (this.shards.options.shardConcurrency === "auto" && this.bot) ? this.getBotGateway() : this.getGateway());
const data = await (this.shards.options.maxShards === "auto" || (this.shards.options.maxConcurrency === "auto" && this.bot) ? this.getBotGateway() : this.getGateway());
if(!data.url || (this.shards.options.maxShards === "auto" && !data.shards)) {
throw new Error("Invalid response from gateway REST call");
}
Expand Down Expand Up @@ -444,7 +444,7 @@ class Client extends EventEmitter {
this.shards.options.lastShardID ??= data.shards - 1;
}

if(this.shards.options.shardConcurrency === "auto" && typeof data.session_start_limit?.max_concurrency === "number") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlikely to be a huge deal as you're expected to prefix your bot token, but I'd keep the check for the field there. Otherwise

const client = new Client("<unprefixed TOKEN>", {
    gateway: {
        maxConcurrency: "auto"
    }
});

client.connect();

will render the bot unable to connect at all (even though it's the user's fault for doing so)

if(this.shards.options.maxConcurrency === "auto") {
this.shards.options.maxConcurrency = data.session_start_limit.max_concurrency;
}

Expand Down
38 changes: 29 additions & 9 deletions lib/gateway/Shard.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ try {
* @extends EventEmitter
*/
class Shard extends EventEmitter {
#awaitedGuildIDs = new Set();
#onWSClose;
#onWSError;
#onWSMessage;
#onWSOpen;
#token;
#zlibSync;

constructor(id, client) {
super();

Expand Down Expand Up @@ -93,6 +95,7 @@ class Shard extends EventEmitter {
* @event Shard#ready
*/
super.emit("ready");
this.#awaitedGuildIDs.clear();
}
}
}
Expand Down Expand Up @@ -263,7 +266,10 @@ class Shard extends EventEmitter {
this.connectAttempts = 0;
this.ws = null;
this.heartbeatInterval = null;
this.guildCreateTimeout = null;
if(this.guildCreateTimeout) {
clearTimeout(this.guildCreateTimeout);
this.guildCreateTimeout = null;
}
this.globalBucket = new Bucket(120, 60000, {reservedTokens: 5});
this.presenceUpdateBucket = new Bucket(5, 20000);
this.presence = JSON.parse(JSON.stringify(this.client.presence)); // Fast copy
Expand Down Expand Up @@ -333,6 +339,7 @@ class Shard extends EventEmitter {
identify.presence = this.presence;
}
this.sendWS(Constants.GatewayOPCodes.IDENTIFY, identify);
this.client.shards._identifyPacketCB(this.id);
}

initializeWS() {
Expand Down Expand Up @@ -405,11 +412,15 @@ class Shard extends EventEmitter {
break;
}
case Constants.GatewayOPCodes.INVALID_SESSION: {
this.seq = 0;
this.sessionID = null;
this.resumeURL = null;
this.emit("warn", "Invalid session, reidentifying!", this.id);
this.identify();
if(!packet.d || !this.sessionID) {
this.seq = 0;
this.sessionID = null;
this.resumeURL = null;
}
this.emit("warn", `Invalid session, ${this.sessionID ? "resuming" : "reidentifying"}!`, this.id);
this.disconnect({
reconnect: "auto"
});
break;
}
case Constants.GatewayOPCodes.RECONNECT: {
Expand Down Expand Up @@ -551,6 +562,7 @@ class Shard extends EventEmitter {
this.connecting = false;
this.ready = false;
this.preReady = false;
this.#awaitedGuildIDs.clear();
if(this.requestMembersPromise !== undefined) {
for(const guildID in this.requestMembersPromise) {
if(!Object.hasOwn(this.requestMembersPromise, guildID)) {
Expand Down Expand Up @@ -581,10 +593,11 @@ class Shard extends EventEmitter {
this.guildCreateTimeout = null;
}
if(!this.ready) {
if(this.client.unavailableGuilds.size === 0) {
if(this.#awaitedGuildIDs.size === 0) {
return this.checkReady();
}
this.guildCreateTimeout = setTimeout(() => {
this.emit("warn", `Not all guilds were found from the READY event (${this.#awaitedGuildIDs.size} missing), continuing...`);
this.checkReady();
}, this.client.shards.options.guildCreateTimeout);
}
Expand Down Expand Up @@ -1375,6 +1388,7 @@ class Shard extends EventEmitter {
}
} else {
this.client.unavailableGuilds.remove(packet.d);
this.#awaitedGuildIDs.delete(packet.d.id);
this.restartGuildCreateTimeout();
}
} else {
Expand Down Expand Up @@ -1492,6 +1506,11 @@ class Shard extends EventEmitter {
* @prop {Guild} guild The guild
*/
this.emit("guildUnavailable", this.client.unavailableGuilds.add(packet.d, this.client));

if(!this.ready) {
this.#awaitedGuildIDs.delete(packet.d.id);
this.restartGuildCreateTimeout();
}
} else {
/**
* Fired when a guild is deleted. This happens when:
Expand Down Expand Up @@ -1861,7 +1880,6 @@ class Shard extends EventEmitter {
this.connectTimeout = null;
this.status = "ready";
this.presence.status = "online";
this.client.shards._readyPacketCB(this.id);

if(packet.t === "RESUMED") {
// Can only heartbeat after resume succeeds, discord/discord-api-docs#1619
Expand Down Expand Up @@ -1895,10 +1913,12 @@ class Shard extends EventEmitter {

this.sessionID = packet.d.session_id;

this.#awaitedGuildIDs.clear();
packet.d.guilds.forEach((guild) => {
if(guild.unavailable) {
this.client.guilds.remove(guild);
this.client.unavailableGuilds.add(guild, this.client, true);
this.#awaitedGuildIDs.add(guild.id);
} else {
this.client.unavailableGuilds.remove(this.createGuild(guild));
}
Expand All @@ -1914,7 +1934,7 @@ class Shard extends EventEmitter {
*/
this.emit("shardPreReady", this.id);

if(this.client.unavailableGuilds.size > 0 && packet.d.guilds.length > 0) {
if(this.#awaitedGuildIDs.size > 0 && packet.d.guilds.length > 0) {
this.restartGuildCreateTimeout();
} else {
this.checkReady();
Expand Down
40 changes: 23 additions & 17 deletions lib/gateway/ShardManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ShardManager extends Collection {
disableEvents: {},
firstShardID: 0,
getAllUsers: false,
guildCreateTimeout: 2000,
guildCreateTimeout: 500,
intents: Constants.Intents.allNonPrivileged,
largeThreshold: 250,
maxReconnectAttempts: Infinity,
Expand Down Expand Up @@ -160,26 +160,32 @@ class ShardManager extends Collection {
return;
}

let nextTry = Infinity;

// loop over the connectQueue
for(const shard of this.connectQueue) {
// find the bucket for our shard
const rateLimitKey = (shard.id % this.options.maxConcurrency) || 0;
const lastConnect = this.buckets.get(rateLimitKey) || 0;

// has enough time passed since the last connect for this bucket (5s/bucket)?
// alternatively if we have a sessionID, we can skip this check
if(!shard.sessionID && Date.now() - lastConnect < 5000) {
continue;
}
for(const shard of [...this.connectQueue]) {
// We are only rate-limiting shards that can't be resumed
if(!shard.sessionID) {
// find the bucket for our shard
const rateLimitKey = (shard.id % this.options.maxConcurrency) || 0;
const lastIdentify = this.buckets.get(rateLimitKey) || 0;
const wait = 5000 - (Date.now() - lastIdentify);

// Has enough time passed since the last identify for this bucket (5s/bucket)?
if(wait > 0) {
nextTry = Math.min(nextTry, wait);
continue;
}

// Are there any connecting shards in the same bucket we should wait on?
if(this.some((s) => s.connecting && ((s.id % this.options.maxConcurrency) || 0) === rateLimitKey)) {
continue;
// Are there any connecting shards in the same bucket we should wait on?
if(this.some((s) => s.connecting && ((s.id % this.options.maxConcurrency) || 0) === rateLimitKey)) {
nextTry = Math.min(nextTry, 250);
continue;
}
}

// connect the shard
shard.connect();
this.buckets.set(rateLimitKey, Date.now());

// remove the shard from the queue
const index = this.connectQueue.findIndex((s) => s.id === shard.id);
Expand All @@ -191,11 +197,11 @@ class ShardManager extends Collection {
this.connectTimeout = setTimeout(() => {
this.connectTimeout = null;
this.tryConnect();
}, 500);
}, Number.isFinite(nextTry) ? Math.max(1, nextTry) : 500);
}
}

_readyPacketCB(shardID) {
_identifyPacketCB(shardID) {
const rateLimitKey = (shardID % this.options.maxConcurrency) || 0;
this.buckets.set(rateLimitKey, Date.now());

Expand Down