mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-11 09:03:29 +01:00
src: sharding cleanup and checkReady rewrite (#3393)
* src: Step 1 of who knows how many * src: Remove accidentally committed test file * src: Remove useless added property in package.json * docs: Trailing spaces, come back >.> * src: Buhbye uws, we will miss you..not! * src: Move 'auto' shard selection from totalShardCount to shards * src: tweak * src: Filter out floats from shard IDs You want half of a shard or what? * src: Misc cleanup and bugfix for GUILD_BAN_ADD * src: Rewrite checkReady * src: Misse this while merging master into my branch * typings: Bring these up to date * typings: Forgot allReady event * src: Don't checkReady if the shard isn't waiting for guilds * src: Fix a possible bug for when the ws dies and the session becomes -1 * src: Hopefully fix last edge case that could case a shard to infinitely boot loop * src: Rename totalShardCount to shardCount * src: Small bugfix * src: Correct error message for shardCount being imvalid Co-Authored-By: bdistin <bdistin@gmail.com> * src: Small tweaks * src: If this doesn't fix the issues I'm gonna throw a brick at my PC * src: I swear, STOP BREAKING * src: *groans at a certain snake* * src: Use undefined instead of null on destroy in close event Setting it to null sets the close code to null, which causes a WebSocket error to be thrown. The error is thrown from WebSocket, although there is no connection alive. Fun times! * src: @SpaceEEC's requested changes * src: Remove zucc from discord.js Discord is removing support for it, sooo... Bye bye * src: Missed this * src: Apply @kyranet's suggestions Co-Authored-By: Antonio Román <kyradiscord@gmail.com> * src: @kyranet's suggestions * src: Remove pako, update debug messages - Pako is officially gone from both enviroments Install zlib-sync on node.js if you want it - Improve a few debug messages some more - Discover that internal sharding works in browsers but please don't do that
This commit is contained in:
@@ -19,6 +19,7 @@ const BeforeReadyWhitelist = [
|
||||
];
|
||||
|
||||
const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011];
|
||||
const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007];
|
||||
|
||||
/**
|
||||
* The WebSocket manager for this client.
|
||||
@@ -47,9 +48,9 @@ class WebSocketManager extends EventEmitter {
|
||||
/**
|
||||
* The amount of shards this manager handles
|
||||
* @private
|
||||
* @type {number|string}
|
||||
* @type {number}
|
||||
*/
|
||||
this.totalShards = this.client.options.shardCount;
|
||||
this.totalShards = this.client.options.shards.length;
|
||||
|
||||
/**
|
||||
* A collection of all shards this manager handles
|
||||
@@ -143,25 +144,25 @@ class WebSocketManager extends EventEmitter {
|
||||
const { total, remaining, reset_after } = sessionStartLimit;
|
||||
|
||||
this.debug(`Fetched Gateway Information
|
||||
URL: ${gatewayURL}
|
||||
Recommended Shards: ${recommendedShards}`);
|
||||
URL: ${gatewayURL}
|
||||
Recommended Shards: ${recommendedShards}`);
|
||||
|
||||
this.debug(`Session Limit Information
|
||||
Total: ${total}
|
||||
Remaining: ${remaining}`);
|
||||
Total: ${total}
|
||||
Remaining: ${remaining}`);
|
||||
|
||||
this.gateway = `${gatewayURL}/`;
|
||||
|
||||
if (this.totalShards === 'auto') {
|
||||
const { shards } = this.client.options;
|
||||
|
||||
if (shards === 'auto') {
|
||||
this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`);
|
||||
this.totalShards = this.client.options.shardCount = this.client.options.totalShardCount = recommendedShards;
|
||||
if (typeof this.client.options.shards === 'undefined' || !this.client.options.shards.length) {
|
||||
this.totalShards = this.client.options.shardCount = recommendedShards;
|
||||
if (shards === 'auto' || !this.client.options.shards.length) {
|
||||
this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i);
|
||||
}
|
||||
}
|
||||
|
||||
const { shards } = this.client.options;
|
||||
|
||||
if (Array.isArray(shards)) {
|
||||
this.totalShards = shards.length;
|
||||
this.debug(`Spawning shards: ${shards.join(', ')}`);
|
||||
@@ -190,15 +191,17 @@ class WebSocketManager extends EventEmitter {
|
||||
this.shardQueue.delete(shard);
|
||||
|
||||
if (!shard.eventsAttached) {
|
||||
shard.on(ShardEvents.READY, () => {
|
||||
shard.on(ShardEvents.ALL_READY, unavailableGuilds => {
|
||||
/**
|
||||
* Emitted when a shard turns ready.
|
||||
* @event Client#shardReady
|
||||
* @param {number} id The shard ID that turned ready
|
||||
* @param {?Set<string>} unavailableGuilds Set of unavailable guild IDs, if any
|
||||
*/
|
||||
this.client.emit(Events.SHARD_READY, shard.id);
|
||||
this.client.emit(Events.SHARD_READY, shard.id, unavailableGuilds);
|
||||
|
||||
if (!this.shardQueue.size) this.reconnecting = false;
|
||||
this.checkShardsReady();
|
||||
});
|
||||
|
||||
shard.on(ShardEvents.CLOSE, event => {
|
||||
@@ -214,8 +217,8 @@ class WebSocketManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 1000 || event.code === 4006) {
|
||||
// Any event code in this range cannot be resumed.
|
||||
if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
|
||||
// These event codes cannot be resumed
|
||||
shard.sessionID = undefined;
|
||||
}
|
||||
|
||||
@@ -226,27 +229,23 @@ class WebSocketManager extends EventEmitter {
|
||||
*/
|
||||
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||
|
||||
this.shardQueue.add(shard);
|
||||
|
||||
if (shard.sessionID) {
|
||||
this.debug(`Session ID is present, attempting an immediate reconnect...`, shard);
|
||||
shard.connect().catch(() => null);
|
||||
return;
|
||||
this.reconnect(true);
|
||||
} else {
|
||||
shard.destroy(undefined, true);
|
||||
this.reconnect();
|
||||
}
|
||||
|
||||
shard.destroy();
|
||||
|
||||
this.shardQueue.add(shard);
|
||||
this.reconnect();
|
||||
});
|
||||
|
||||
shard.on(ShardEvents.INVALID_SESSION, () => {
|
||||
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||
|
||||
this.shardQueue.add(shard);
|
||||
this.reconnect();
|
||||
});
|
||||
|
||||
shard.on(ShardEvents.DESTROYED, () => {
|
||||
this.debug('Shard was destroyed but no WebSocket connection existed... Reconnecting...', shard);
|
||||
this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard);
|
||||
|
||||
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||
|
||||
@@ -264,7 +263,7 @@ class WebSocketManager extends EventEmitter {
|
||||
} catch (error) {
|
||||
if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) {
|
||||
throw new DJSError(WSCodes[error.code]);
|
||||
// Undefined if session is invalid, error event (or uws' event mimicking it) for regular closes
|
||||
// Undefined if session is invalid, error event for regular closes
|
||||
} else if (!error || error.code) {
|
||||
this.debug('Failed to connect to the gateway, requeueing...', shard);
|
||||
this.shardQueue.add(shard);
|
||||
@@ -285,14 +284,15 @@ class WebSocketManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Handles reconnects for this manager.
|
||||
* @param {boolean} [skipLimit=false] IF this reconnect should skip checking the session limit
|
||||
* @private
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async reconnect() {
|
||||
async reconnect(skipLimit = false) {
|
||||
if (this.reconnecting || this.status !== Status.READY) return false;
|
||||
this.reconnecting = true;
|
||||
try {
|
||||
await this._handleSessionLimit();
|
||||
if (!skipLimit) await this._handleSessionLimit();
|
||||
await this.createShards();
|
||||
} catch (error) {
|
||||
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
|
||||
@@ -340,7 +340,7 @@ class WebSocketManager extends EventEmitter {
|
||||
this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`);
|
||||
this.destroyed = true;
|
||||
this.shardQueue.clear();
|
||||
for (const shard of this.shards.values()) shard.destroy();
|
||||
for (const shard of this.shards.values()) shard.destroy(1000, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,8 +356,8 @@ class WebSocketManager extends EventEmitter {
|
||||
remaining = session_start_limit.remaining;
|
||||
resetAfter = session_start_limit.reset_after;
|
||||
this.debug(`Session Limit Information
|
||||
Total: ${session_start_limit.total}
|
||||
Remaining: ${remaining}`);
|
||||
Total: ${session_start_limit.total}
|
||||
Remaining: ${remaining}`);
|
||||
}
|
||||
if (!remaining) {
|
||||
this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`);
|
||||
@@ -396,45 +396,37 @@ class WebSocketManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Checks whether the client is ready to be marked as ready.
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
checkReady() {
|
||||
async checkShardsReady() {
|
||||
if (this.status === Status.READY) return;
|
||||
if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
const unavailableGuilds = this.client.guilds.reduce((acc, guild) => guild.available ? acc : acc + 1, 0);
|
||||
this.status = Status.NEARLY;
|
||||
|
||||
// TODO: Rethink implementation for this
|
||||
if (unavailableGuilds === 0) {
|
||||
this.status = Status.NEARLY;
|
||||
if (!this.client.options.fetchAllMembers) return this.triggerReady();
|
||||
// Fetch all members before marking self as ready
|
||||
const promises = this.client.guilds.map(g => g.members.fetch());
|
||||
Promise.all(promises)
|
||||
.then(() => this.triggerReady())
|
||||
.catch(e => {
|
||||
this.debug(`Failed to fetch all members before ready! ${e}\n${e.stack}`);
|
||||
this.triggerReady();
|
||||
if (this.client.options.fetchAllMembers) {
|
||||
try {
|
||||
const promises = this.client.guilds.map(guild => {
|
||||
if (guild.available) return guild.members.fetch();
|
||||
// Return empty promise if guild is unavailable
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else {
|
||||
this.debug(`There are ${unavailableGuilds} unavailable guilds. Waiting for their GUILD_CREATE packets`);
|
||||
await Promise.all(promises);
|
||||
} catch (err) {
|
||||
this.debug(`Failed to fetch all members before ready! ${err}\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
this.triggerClientReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the client to be marked as ready and emits the ready event.
|
||||
* @private
|
||||
*/
|
||||
triggerReady() {
|
||||
if (this.status === Status.READY) {
|
||||
this.debug('Tried to mark self as ready, but already ready');
|
||||
return;
|
||||
}
|
||||
|
||||
triggerClientReady() {
|
||||
this.status = Status.READY;
|
||||
|
||||
this.client.readyAt = new Date();
|
||||
|
||||
Reference in New Issue
Block a user