From a65c76295065df303f42b90d874630248a91cb2a Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:41:25 +0200 Subject: [PATCH] refactor!: fully integrate /ws into mainlib (#10420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: `Client#ws` is now a `@discordjs/ws#WebSocketManager` BREAKING CHANGE: `WebSocketManager` and `WebSocketShard` are now re-exports from `@discordjs/ws` BREAKING CHANGE: Removed the `WebSocketShardEvents` enum BREAKING CHANGE: Renamed the `Client#ready` event to `Client#clientReady` event to not confuse it with the gateway `READY` event BREAKING CHANGE: Added `Client#ping` to replace the old `WebSocketManager#ping` BREAKING CHANGE: Removed the `Shard#reconnecting` event which wasn’t emitted anymore since 14.8.0 anyway BREAKING CHANGE: Removed `ShardClientUtil#ids` and `ShardClientUtil#count` in favor of `Client#ws#getShardIds()` and `Client#ws#getShardCount()` BREAKING CHANGE: `ClientUser#setPresence()` and `ClientPresence#set()` now return a Promise which resolves when the gateway call was sent successfully BREAKING CHANGE: Removed `Guild#shard` as `WebSocketShard`s are now handled by `@discordjs/ws` BREAKING CHANGE: Removed the following deprecated `Client` events: `raw`, `shardDisconnect`, `shardError`, `shardReady`, `shardReconnecting`, `shardResume` in favor of events from `@discordjs/ws#WebSocketManager` BREAKING CHANGE: Removed `ClientOptions#shards` and `ClientOptions#shardCount` in favor of `ClientOptions#ws#shardIds` and `ClientOptions#ws#shardCount` --- packages/discord.js/package.json | 2 +- packages/discord.js/src/client/Client.js | 306 +++++++++++--- .../src/client/actions/GuildMemberRemove.js | 5 +- .../src/client/actions/GuildMemberUpdate.js | 5 +- .../src/client/voice/ClientVoiceManager.js | 12 +- .../src/client/websocket/WebSocketManager.js | 387 ------------------ .../src/client/websocket/WebSocketShard.js | 234 ----------- .../client/websocket/handlers/GUILD_CREATE.js | 6 +- .../websocket/handlers/GUILD_MEMBER_ADD.js | 17 +- .../websocket/handlers/GUILD_MEMBER_REMOVE.js | 4 +- .../websocket/handlers/GUILD_MEMBER_UPDATE.js | 4 +- .../src/client/websocket/handlers/READY.js | 6 +- .../src/client/websocket/handlers/RESUMED.js | 14 - .../src/client/websocket/handlers/index.js | 1 - packages/discord.js/src/errors/ErrorCodes.js | 83 +++- packages/discord.js/src/index.js | 3 - .../discord.js/src/managers/GuildManager.js | 2 +- .../src/managers/GuildMemberManager.js | 2 +- packages/discord.js/src/sharding/Shard.js | 17 +- .../src/sharding/ShardClientUtil.js | 37 +- .../src/structures/ClientPresence.js | 14 +- .../discord.js/src/structures/ClientUser.js | 8 +- packages/discord.js/src/structures/Guild.js | 13 +- packages/discord.js/src/util/APITypes.js | 4 + packages/discord.js/src/util/Events.js | 13 +- packages/discord.js/src/util/Options.js | 51 +-- packages/discord.js/src/util/Status.js | 18 +- .../src/util/WebSocketShardEvents.js | 25 -- packages/discord.js/typings/index.d.ts | 124 +----- packages/discord.js/typings/index.test-d.ts | 16 +- pnpm-lock.yaml | 62 +-- 31 files changed, 409 insertions(+), 1086 deletions(-) delete mode 100644 packages/discord.js/src/client/websocket/WebSocketManager.js delete mode 100644 packages/discord.js/src/client/websocket/WebSocketShard.js delete mode 100644 packages/discord.js/src/client/websocket/handlers/RESUMED.js delete mode 100644 packages/discord.js/src/util/WebSocketShardEvents.js diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 5aac93e77..e64d85856 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -70,7 +70,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/rest": "workspace:^", "@discordjs/util": "workspace:^", - "@discordjs/ws": "1.1.1", + "@discordjs/ws": "workspace:^", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.37.101", "fast-deep-equal": "3.1.3", diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 3a72b8414..048cfb32d 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -1,14 +1,16 @@ 'use strict'; const process = require('node:process'); +const { clearTimeout, setImmediate, setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); -const { OAuth2Scopes, Routes } = require('discord-api-types/v10'); +const { WebSocketManager, WebSocketShardEvents, WebSocketShardStatus } = require('@discordjs/ws'); +const { GatewayDispatchEvents, GatewayIntentBits, OAuth2Scopes, Routes } = require('discord-api-types/v10'); const BaseClient = require('./BaseClient'); const ActionsManager = require('./actions/ActionsManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); -const WebSocketManager = require('./websocket/WebSocketManager'); -const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); +const PacketHandlers = require('./websocket/handlers'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const ChannelManager = require('../managers/ChannelManager'); const GuildManager = require('../managers/GuildManager'); @@ -31,6 +33,17 @@ const PermissionsBitField = require('../util/PermissionsBitField'); const Status = require('../util/Status'); const Sweepers = require('../util/Sweepers'); +const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; +const BeforeReadyWhitelist = [ + GatewayDispatchEvents.Ready, + GatewayDispatchEvents.Resumed, + GatewayDispatchEvents.GuildCreate, + GatewayDispatchEvents.GuildDelete, + GatewayDispatchEvents.GuildMembersChunk, + GatewayDispatchEvents.GuildMemberAdd, + GatewayDispatchEvents.GuildMemberRemove, +]; + /** * The main hub for interacting with the Discord API, and the starting point for any bot. * @extends {BaseClient} @@ -45,43 +58,45 @@ class Client extends BaseClient { const data = require('node:worker_threads').workerData ?? process.env; const defaults = Options.createDefault(); - if (this.options.shards === defaults.shards) { - if ('SHARDS' in data) { - this.options.shards = JSON.parse(data.SHARDS); - } + if (this.options.ws.shardIds === defaults.ws.shardIds && 'SHARDS' in data) { + this.options.ws.shardIds = JSON.parse(data.SHARDS); } - if (this.options.shardCount === defaults.shardCount) { - if ('SHARD_COUNT' in data) { - this.options.shardCount = Number(data.SHARD_COUNT); - } else if (Array.isArray(this.options.shards)) { - this.options.shardCount = this.options.shards.length; - } + if (this.options.ws.shardCount === defaults.ws.shardCount && 'SHARD_COUNT' in data) { + this.options.ws.shardCount = Number(data.SHARD_COUNT); } - const typeofShards = typeof this.options.shards; - - if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { - this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); - } - - if (typeofShards === 'number') this.options.shards = [this.options.shards]; - - if (Array.isArray(this.options.shards)) { - this.options.shards = [ - ...new Set( - this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)), - ), - ]; - } + /** + * The presence of the Client + * @private + * @type {ClientPresence} + */ + this.presence = new ClientPresence(this, this.options.ws.initialPresence ?? this.options.presence); this._validateOptions(); /** - * The WebSocket manager of the client - * @type {WebSocketManager} + * The current status of this Client + * @type {Status} + * @private */ - this.ws = new WebSocketManager(this); + this.status = Status.Idle; + + /** + * A set of guild ids this Client expects to receive + * @name Client#expectedGuilds + * @type {Set} + * @private + */ + Object.defineProperty(this, 'expectedGuilds', { value: new Set(), writable: true }); + + /** + * The ready timeout + * @name Client#readyTimeout + * @type {?NodeJS.Timeout} + * @private + */ + Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); /** * The action manager of the client @@ -90,12 +105,6 @@ class Client extends BaseClient { */ this.actions = new ActionsManager(this); - /** - * The voice manager of the client - * @type {ClientVoiceManager} - */ - this.voice = new ClientVoiceManager(this); - /** * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * @type {?ShardClientUtil} @@ -119,7 +128,7 @@ class Client extends BaseClient { /** * All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids - - * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot + * as long as no sharding manager is being used, this will be *every* channel in *every* guild the bot * is a member of. Note that DM channels will not be initially cached, and thus not be present * in the Manager without their explicit fetching or use. * @type {ChannelManager} @@ -132,13 +141,6 @@ class Client extends BaseClient { */ this.sweepers = new Sweepers(this, this.options.sweepers); - /** - * The presence of the Client - * @private - * @type {ClientPresence} - */ - this.presence = new ClientPresence(this, this.options.presence); - Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'DISCORD_TOKEN' in process.env) { /** @@ -148,10 +150,31 @@ class Client extends BaseClient { * @type {?string} */ this.token = process.env.DISCORD_TOKEN; + } else if (this.options.ws.token) { + this.token = this.options.ws.token; } else { this.token = null; } + const wsOptions = { + ...this.options.ws, + intents: this.options.intents.bitfield, + rest: this.rest, + token: this.token, + }; + + /** + * The WebSocket manager of the client + * @type {WebSocketManager} + */ + this.ws = new WebSocketManager(wsOptions); + + /** + * The voice manager of the client + * @type {ClientVoiceManager} + */ + this.voice = new ClientVoiceManager(this); + /** * User that the client is logged in as * @type {?ClientUser} @@ -164,11 +187,33 @@ class Client extends BaseClient { */ this.application = null; + /** + * The latencies of the WebSocketShard connections + * @type {Collection} + */ + this.pings = new Collection(); + + /** + * The last time a ping was sent (a timestamp) for each WebSocketShard connection + * @type {Collection} + */ + this.lastPingTimestamps = new Collection(); + /** * Timestamp of the time the client was last {@link Status.Ready} at * @type {?number} */ this.readyTimestamp = null; + + /** + * An array of queued events before this Client became ready + * @type {Object[]} + * @private + * @name Client#incomingPacketQueue + */ + Object.defineProperty(this, 'incomingPacketQueue', { value: [] }); + + this._attachEvents(); } /** @@ -215,13 +260,10 @@ class Client extends BaseClient { this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); this.rest.setToken(token); this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); - - if (this.options.presence) { - this.options.ws.presence = this.presence._parse(this.options.presence); - } - this.emit(Events.Debug, 'Preparing to connect to the gateway...'); + this.ws.setToken(this.token); + try { await this.ws.connect(); return this.token; @@ -231,13 +273,150 @@ class Client extends BaseClient { } } + /** + * Checks if the client can be marked as ready + * @private + */ + async _checkReady() { + // Step 0. Clear the ready timeout, if it exists + if (this.readyTimeout) { + clearTimeout(this.readyTimeout); + this.readyTimeout = null; + } + // Step 1. If we don't have any other guilds pending, we are ready + if ( + !this.expectedGuilds.size && + (await this.ws.fetchStatus()).every(status => status === WebSocketShardStatus.Ready) + ) { + this.emit(Events.Debug, 'Client received all its guilds. Marking as fully ready.'); + this.status = Status.Ready; + + this._triggerClientReady(); + return; + } + const hasGuildsIntent = this.options.intents.has(GatewayIntentBits.Guilds); + // Step 2. Create a timeout that will mark the client as ready if there are still unavailable guilds + // * The timeout is 15 seconds by default + // * This can be optionally changed in the client options via the `waitGuildTimeout` option + // * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds. + + this.readyTimeout = setTimeout( + () => { + this.emit( + Events.Debug, + `${ + hasGuildsIntent + ? `Client did not receive any guild packets in ${this.options.waitGuildTimeout} ms.` + : 'Client will not receive anymore guild packets.' + }\nUnavailable guild count: ${this.expectedGuilds.size}`, + ); + + this.readyTimeout = null; + this.status = Status.Ready; + + this._triggerClientReady(); + }, + hasGuildsIntent ? this.options.waitGuildTimeout : 0, + ).unref(); + } + + /** + * Attaches event handlers to the WebSocketShardManager from `@discordjs/ws`. + * @private + */ + _attachEvents() { + this.ws.on(WebSocketShardEvents.Debug, (message, shardId) => + this.emit(Events.Debug, `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`), + ); + this.ws.on(WebSocketShardEvents.Dispatch, this._handlePacket.bind(this)); + + this.ws.on(WebSocketShardEvents.Ready, data => { + for (const guild of data.guilds) { + this.expectedGuilds.add(guild.id); + } + this.status = Status.WaitingForGuilds; + this._checkReady(); + }); + + this.ws.on(WebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency }, shardId) => { + this.emit(Events.Debug, `[WS => Shard ${shardId}] Heartbeat acknowledged, latency of ${latency}ms.`); + this.lastPingTimestamps.set(shardId, heartbeatAt); + this.pings.set(shardId, latency); + }); + } + + /** + * Processes a packet and queues it if this WebSocketManager is not ready. + * @param {GatewayDispatchPayload} packet The packet to be handled + * @param {number} shardId The shardId that received this packet + * @private + */ + _handlePacket(packet, shardId) { + if (this.status !== Status.Ready && !BeforeReadyWhitelist.includes(packet.t)) { + this.incomingPacketQueue.push({ packet, shardId }); + } else { + if (this.incomingPacketQueue.length) { + const item = this.incomingPacketQueue.shift(); + setImmediate(() => { + this._handlePacket(item.packet, item.shardId); + }).unref(); + } + + if (PacketHandlers[packet.t]) { + PacketHandlers[packet.t](this, packet, shardId); + } + + if (this.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(packet.t)) { + this.expectedGuilds.delete(packet.d.id); + this._checkReady(); + } + } + } + + /** + * Broadcasts a packet to every shard of this client handles. + * @param {Object} packet The packet to send + * @private + */ + async _broadcast(packet) { + const shardIds = await this.ws.getShardIds(); + return Promise.all(shardIds.map(shardId => this.ws.send(shardId, packet))); + } + + /** + * Causes the client to be marked as ready and emits the ready event. + * @private + */ + _triggerClientReady() { + this.status = Status.Ready; + + this.readyTimestamp = Date.now(); + + /** + * Emitted when the client becomes ready to start working. + * @event Client#clientReady + * @param {Client} client The client + */ + this.emit(Events.ClientReady, this); + } + /** * Returns whether the client has logged in, indicative of being able to access * properties such as `user` and `application`. * @returns {boolean} */ isReady() { - return !this.ws.destroyed && this.ws.status === Status.Ready; + return this.status === Status.Ready; + } + + /** + * The average ping of all WebSocketShards + * @type {number} + * @readonly + */ + get ping() { + const sum = this.pings.reduce((a, b) => a + b, 0); + return sum / this.pings.size; } /** @@ -505,20 +684,10 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { - if (options.intents === undefined) { + if (options.intents === undefined && options.ws?.intents === undefined) { throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents); } else { - options.intents = new IntentsBitField(options.intents).freeze(); - } - if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shardCount', 'a number greater than or equal to 1'); - } - if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shards', "'auto', a number or array of numbers"); - } - if (options.shards && !options.shards.length) throw new DiscordjsRangeError(ErrorCodes.ClientInvalidProvidedShards); - if (typeof options.makeCache !== 'function') { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'makeCache', 'a function'); + options.intents = new IntentsBitField(options.intents ?? options.ws.intents).freeze(); } if (typeof options.sweepers !== 'object' || options.sweepers === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'sweepers', 'an object'); @@ -541,12 +710,17 @@ class Client extends BaseClient { ) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'allowedMentions', 'an object'); } - if (typeof options.presence !== 'object' || options.presence === null) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); - } if (typeof options.ws !== 'object' || options.ws === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'ws', 'an object'); } + if ( + (typeof options.presence !== 'object' || options.presence === null) && + options.ws.initialPresence === undefined + ) { + throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); + } else { + options.ws.initialPresence = options.ws.initialPresence ?? this.presence._parse(this.options.presence); + } if (typeof options.rest !== 'object' || options.rest === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'rest', 'an object'); } diff --git a/packages/discord.js/src/client/actions/GuildMemberRemove.js b/packages/discord.js/src/client/actions/GuildMemberRemove.js index 45eb6c419..e7cefee67 100644 --- a/packages/discord.js/src/client/actions/GuildMemberRemove.js +++ b/packages/discord.js/src/client/actions/GuildMemberRemove.js @@ -2,10 +2,9 @@ const Action = require('./Action'); const Events = require('../../util/Events'); -const Status = require('../../util/Status'); class GuildMemberRemoveAction extends Action { - handle(data, shard) { + handle(data) { const client = this.client; const guild = client.guilds.cache.get(data.guild_id); let member = null; @@ -19,7 +18,7 @@ class GuildMemberRemoveAction extends Action { * @event Client#guildMemberRemove * @param {GuildMember} member The member that has left/been kicked from the guild */ - if (shard.status === Status.Ready) client.emit(Events.GuildMemberRemove, member); + client.emit(Events.GuildMemberRemove, member); } guild.presences.cache.delete(data.user.id); guild.voiceStates.cache.delete(data.user.id); diff --git a/packages/discord.js/src/client/actions/GuildMemberUpdate.js b/packages/discord.js/src/client/actions/GuildMemberUpdate.js index 491b36181..9561ab82a 100644 --- a/packages/discord.js/src/client/actions/GuildMemberUpdate.js +++ b/packages/discord.js/src/client/actions/GuildMemberUpdate.js @@ -2,10 +2,9 @@ const Action = require('./Action'); const Events = require('../../util/Events'); -const Status = require('../../util/Status'); class GuildMemberUpdateAction extends Action { - handle(data, shard) { + handle(data) { const { client } = this; if (data.user.username) { const user = client.users.cache.get(data.user.id); @@ -27,7 +26,7 @@ class GuildMemberUpdateAction extends Action { * @param {GuildMember} oldMember The member before the update * @param {GuildMember} newMember The member after the update */ - if (shard.status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); + if (!member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); } else { const newMember = guild.members._add(data); /** diff --git a/packages/discord.js/src/client/voice/ClientVoiceManager.js b/packages/discord.js/src/client/voice/ClientVoiceManager.js index 55b0d830e..520dd40cf 100644 --- a/packages/discord.js/src/client/voice/ClientVoiceManager.js +++ b/packages/discord.js/src/client/voice/ClientVoiceManager.js @@ -1,6 +1,6 @@ 'use strict'; -const Events = require('../../util/Events'); +const { WebSocketShardEvents, CloseCodes } = require('@discordjs/ws'); /** * Manages voice connections for the client @@ -21,10 +21,12 @@ class ClientVoiceManager { */ this.adapters = new Map(); - client.on(Events.ShardDisconnect, (_, shardId) => { - for (const [guildId, adapter] of this.adapters.entries()) { - if (client.guilds.cache.get(guildId)?.shardId === shardId) { - adapter.destroy(); + client.ws.on(WebSocketShardEvents.Closed, (code, shardId) => { + if (code === CloseCodes.Normal) { + for (const [guildId, adapter] of this.adapters.entries()) { + if (client.guilds.cache.get(guildId)?.shardId === shardId) { + adapter.destroy(); + } } } }); diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js deleted file mode 100644 index 7a1831ad6..000000000 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ /dev/null @@ -1,387 +0,0 @@ -'use strict'; - -const EventEmitter = require('node:events'); -const process = require('node:process'); -const { setImmediate } = require('node:timers'); -const { Collection } = require('@discordjs/collection'); -const { - WebSocketManager: WSWebSocketManager, - WebSocketShardEvents: WSWebSocketShardEvents, - CompressionMethod, - CloseCodes, -} = require('@discordjs/ws'); -const { GatewayCloseCodes, GatewayDispatchEvents } = require('discord-api-types/v10'); -const WebSocketShard = require('./WebSocketShard'); -const PacketHandlers = require('./handlers'); -const { DiscordjsError, ErrorCodes } = require('../../errors'); -const Events = require('../../util/Events'); -const Status = require('../../util/Status'); -const WebSocketShardEvents = require('../../util/WebSocketShardEvents'); - -let zlib; - -try { - zlib = require('zlib-sync'); -} catch {} // eslint-disable-line no-empty - -const BeforeReadyWhitelist = [ - GatewayDispatchEvents.Ready, - GatewayDispatchEvents.Resumed, - GatewayDispatchEvents.GuildCreate, - GatewayDispatchEvents.GuildDelete, - GatewayDispatchEvents.GuildMembersChunk, - GatewayDispatchEvents.GuildMemberAdd, - GatewayDispatchEvents.GuildMemberRemove, -]; - -const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; - -const UNRESUMABLE_CLOSE_CODES = [ - CloseCodes.Normal, - GatewayCloseCodes.AlreadyAuthenticated, - GatewayCloseCodes.InvalidSeq, -]; - -const reasonIsDeprecated = 'the reason property is deprecated, use the code property to determine the reason'; -let deprecationEmittedForInvalidSessionEvent = false; -let deprecationEmittedForDestroyedEvent = false; - -/** - * The WebSocket manager for this client. - * This class forwards raw dispatch events, - * read more about it here {@link https://discord.com/developers/docs/topics/gateway} - * @extends {EventEmitter} - */ -class WebSocketManager extends EventEmitter { - constructor(client) { - super(); - - /** - * The client that instantiated this WebSocketManager - * @type {Client} - * @readonly - * @name WebSocketManager#client - */ - Object.defineProperty(this, 'client', { value: client }); - - /** - * The gateway this manager uses - * @type {?string} - */ - this.gateway = null; - - /** - * A collection of all shards this manager handles - * @type {Collection} - */ - this.shards = new Collection(); - - /** - * An array of queued events before this WebSocketManager became ready - * @type {Object[]} - * @private - * @name WebSocketManager#packetQueue - */ - Object.defineProperty(this, 'packetQueue', { value: [] }); - - /** - * The current status of this WebSocketManager - * @type {Status} - */ - this.status = Status.Idle; - - /** - * If this manager was destroyed. It will prevent shards from reconnecting - * @type {boolean} - * @private - */ - this.destroyed = false; - - /** - * The internal WebSocketManager from `@discordjs/ws`. - * @type {WSWebSocketManager} - * @private - */ - this._ws = null; - } - - /** - * The average ping of all WebSocketShards - * @type {number} - * @readonly - */ - get ping() { - const sum = this.shards.reduce((a, b) => a + b.ping, 0); - return sum / this.shards.size; - } - - /** - * Emits a debug message. - * @param {string[]} messages The debug message - * @param {?number} [shardId] The id of the shard that emitted this message, if any - * @private - */ - debug(messages, shardId) { - this.client.emit( - Events.Debug, - `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${messages.join('\n\t')}`, - ); - } - - /** - * Connects this manager to the gateway. - * @private - */ - async connect() { - const invalidToken = new DiscordjsError(ErrorCodes.TokenInvalid); - const { shards, shardCount, intents, ws } = this.client.options; - if (this._ws && this._ws.options.token !== this.client.token) { - await this._ws.destroy({ code: CloseCodes.Normal, reason: 'Login with differing token requested' }); - this._ws = null; - } - if (!this._ws) { - const wsOptions = { - intents: intents.bitfield, - rest: this.client.rest, - token: this.client.token, - largeThreshold: ws.large_threshold, - version: ws.version, - shardIds: shards === 'auto' ? null : shards, - shardCount: shards === 'auto' ? null : shardCount, - initialPresence: ws.presence, - retrieveSessionInfo: shardId => this.shards.get(shardId).sessionInfo, - updateSessionInfo: (shardId, sessionInfo) => { - this.shards.get(shardId).sessionInfo = sessionInfo; - }, - compression: zlib ? CompressionMethod.ZlibStream : null, - }; - if (ws.buildIdentifyThrottler) wsOptions.buildIdentifyThrottler = ws.buildIdentifyThrottler; - if (ws.buildStrategy) wsOptions.buildStrategy = ws.buildStrategy; - this._ws = new WSWebSocketManager(wsOptions); - this.attachEvents(); - } - - const { - url: gatewayURL, - shards: recommendedShards, - session_start_limit: sessionStartLimit, - } = await this._ws.fetchGatewayInformation().catch(error => { - throw error.status === 401 ? invalidToken : error; - }); - - const { total, remaining } = sessionStartLimit; - this.debug(['Fetched Gateway Information', `URL: ${gatewayURL}`, `Recommended Shards: ${recommendedShards}`]); - this.debug(['Session Limit Information', `Total: ${total}`, `Remaining: ${remaining}`]); - this.gateway = `${gatewayURL}/`; - - this.client.options.shardCount = await this._ws.getShardCount(); - this.client.options.shards = await this._ws.getShardIds(); - this.totalShards = this.client.options.shards.length; - for (const id of this.client.options.shards) { - if (!this.shards.has(id)) { - const shard = new WebSocketShard(this, id); - this.shards.set(id, shard); - - shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => { - /** - * Emitted when a shard turns ready. - * @event Client#shardReady - * @param {number} id The shard id that turned ready - * @param {?Set} unavailableGuilds Set of unavailable guild ids, if any - */ - this.client.emit(Events.ShardReady, shard.id, unavailableGuilds); - - this.checkShardsReady(); - }); - shard.status = Status.Connecting; - } - } - - await this._ws.connect(); - - this.shards.forEach(shard => { - if (shard.listenerCount(WebSocketShardEvents.InvalidSession) > 0 && !deprecationEmittedForInvalidSessionEvent) { - process.emitWarning( - 'The WebSocketShard#invalidSession event is deprecated and will never emit.', - 'DeprecationWarning', - ); - - deprecationEmittedForInvalidSessionEvent = true; - } - if (shard.listenerCount(WebSocketShardEvents.Destroyed) > 0 && !deprecationEmittedForDestroyedEvent) { - process.emitWarning( - 'The WebSocketShard#destroyed event is deprecated and will never emit.', - 'DeprecationWarning', - ); - - deprecationEmittedForDestroyedEvent = true; - } - }); - } - - /** - * Attaches event handlers to the internal WebSocketShardManager from `@discordjs/ws`. - * @private - */ - attachEvents() { - this._ws.on(WSWebSocketShardEvents.Debug, ({ message, shardId }) => this.debug([message], shardId)); - this._ws.on(WSWebSocketShardEvents.Dispatch, ({ data, shardId }) => { - this.client.emit(Events.Raw, data, shardId); - this.emit(data.t, data.d, shardId); - const shard = this.shards.get(shardId); - this.handlePacket(data, shard); - if (shard.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(data.t)) { - shard.gotGuild(data.d.id); - } - }); - - this._ws.on(WSWebSocketShardEvents.Ready, ({ data, shardId }) => { - this.shards.get(shardId).onReadyPacket(data); - }); - - this._ws.on(WSWebSocketShardEvents.Closed, ({ code, shardId }) => { - const shard = this.shards.get(shardId); - shard.emit(WebSocketShardEvents.Close, { code, reason: reasonIsDeprecated, wasClean: true }); - if (UNRESUMABLE_CLOSE_CODES.includes(code) && this.destroyed) { - shard.status = Status.Disconnected; - /** - * Emitted when a shard's WebSocket disconnects and will no longer reconnect. - * @event Client#shardDisconnect - * @param {CloseEvent} event The WebSocket close event - * @param {number} id The shard id that disconnected - */ - this.client.emit(Events.ShardDisconnect, { code, reason: reasonIsDeprecated, wasClean: true }, shardId); - this.debug([`Shard not resumable: ${code} (${GatewayCloseCodes[code] ?? CloseCodes[code]})`], shardId); - return; - } - - this.shards.get(shardId).status = Status.Connecting; - /** - * Emitted when a shard is attempting to reconnect or re-identify. - * @event Client#shardReconnecting - * @param {number} id The shard id that is attempting to reconnect - */ - this.client.emit(Events.ShardReconnecting, shardId); - }); - this._ws.on(WSWebSocketShardEvents.Hello, ({ shardId }) => { - const shard = this.shards.get(shardId); - if (shard.sessionInfo) { - shard.closeSequence = shard.sessionInfo.sequence; - shard.status = Status.Resuming; - } else { - shard.status = Status.Identifying; - } - }); - - this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => { - const shard = this.shards.get(shardId); - shard.status = Status.Ready; - /** - * Emitted when the shard resumes successfully - * @event WebSocketShard#resumed - */ - shard.emit(WebSocketShardEvents.Resumed); - }); - - this._ws.on(WSWebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency, shardId }) => { - this.debug([`Heartbeat acknowledged, latency of ${latency}ms.`], shardId); - const shard = this.shards.get(shardId); - shard.lastPingTimestamp = heartbeatAt; - shard.ping = latency; - }); - - this._ws.on(WSWebSocketShardEvents.Error, ({ error, shardId }) => { - /** - * Emitted whenever a shard's WebSocket encounters a connection error. - * @event Client#shardError - * @param {Error} error The encountered error - * @param {number} shardId The shard that encountered this error - */ - this.client.emit(Events.ShardError, error, shardId); - }); - } - - /** - * Broadcasts a packet to every shard this manager handles. - * @param {Object} packet The packet to send - * @private - */ - broadcast(packet) { - for (const shardId of this.shards.keys()) this._ws.send(shardId, packet); - } - - /** - * Destroys this manager and all its shards. - * @private - */ - async destroy() { - if (this.destroyed) return; - // TODO: Make a util for getting a stack - this.debug([Object.assign(new Error(), { name: 'Manager was destroyed:' }).stack]); - this.destroyed = true; - await this._ws?.destroy({ code: CloseCodes.Normal, reason: 'Manager was destroyed' }); - } - - /** - * Processes a packet and queues it if this WebSocketManager is not ready. - * @param {Object} [packet] The packet to be handled - * @param {WebSocketShard} [shard] The shard that will handle this packet - * @returns {boolean} - * @private - */ - handlePacket(packet, shard) { - if (packet && this.status !== Status.Ready) { - if (!BeforeReadyWhitelist.includes(packet.t)) { - this.packetQueue.push({ packet, shard }); - return false; - } - } - - if (this.packetQueue.length) { - const item = this.packetQueue.shift(); - setImmediate(() => { - this.handlePacket(item.packet, item.shard); - }).unref(); - } - - if (packet && PacketHandlers[packet.t]) { - PacketHandlers[packet.t](this.client, packet, shard); - } - - return true; - } - - /** - * Checks whether the client is ready to be marked as ready. - * @private - */ - checkShardsReady() { - if (this.status === Status.Ready) return; - if (this.shards.size !== this.totalShards || this.shards.some(shard => shard.status !== Status.Ready)) { - return; - } - - this.triggerClientReady(); - } - - /** - * Causes the client to be marked as ready and emits the ready event. - * @private - */ - triggerClientReady() { - this.status = Status.Ready; - - this.client.readyTimestamp = Date.now(); - - /** - * Emitted when the client becomes ready to start working. - * @event Client#ready - * @param {Client} client The client - */ - this.client.emit(Events.ClientReady, this.client); - - this.handlePacket(); - } -} - -module.exports = WebSocketManager; diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js deleted file mode 100644 index d3d8167f9..000000000 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ /dev/null @@ -1,234 +0,0 @@ -'use strict'; - -const EventEmitter = require('node:events'); -const process = require('node:process'); -const { setTimeout, clearTimeout } = require('node:timers'); -const { GatewayIntentBits } = require('discord-api-types/v10'); -const Status = require('../../util/Status'); -const WebSocketShardEvents = require('../../util/WebSocketShardEvents'); - -let deprecationEmittedForImportant = false; -/** - * Represents a Shard's WebSocket connection - * @extends {EventEmitter} - */ -class WebSocketShard extends EventEmitter { - constructor(manager, id) { - super(); - - /** - * The WebSocketManager of the shard - * @type {WebSocketManager} - */ - this.manager = manager; - - /** - * The shard's id - * @type {number} - */ - this.id = id; - - /** - * The current status of the shard - * @type {Status} - */ - this.status = Status.Idle; - - /** - * The sequence of the shard after close - * @type {number} - * @private - */ - this.closeSequence = 0; - - /** - * The previous heartbeat ping of the shard - * @type {number} - */ - this.ping = -1; - - /** - * The last time a ping was sent (a timestamp) - * @type {number} - */ - this.lastPingTimestamp = -1; - - /** - * A set of guild ids this shard expects to receive - * @name WebSocketShard#expectedGuilds - * @type {?Set} - * @private - */ - Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true }); - - /** - * The ready timeout - * @name WebSocketShard#readyTimeout - * @type {?NodeJS.Timeout} - * @private - */ - Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); - - /** - * @external SessionInfo - * @see {@link https://discord.js.org/docs/packages/ws/stable/SessionInfo:Interface} - */ - - /** - * The session info used by `@discordjs/ws` package. - * @name WebSocketShard#sessionInfo - * @type {?SessionInfo} - * @private - */ - Object.defineProperty(this, 'sessionInfo', { value: null, writable: true }); - } - - /** - * Emits a debug event. - * @param {string[]} messages The debug message - * @private - */ - debug(messages) { - this.manager.debug(messages, this.id); - } - - /** - * @external CloseEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} - */ - - /** - * This method is responsible to emit close event for this shard. - * This method helps the shard reconnect. - * @param {CloseEvent} [event] Close event that was received - * @deprecated - */ - emitClose( - event = { - code: 1011, - reason: 'INTERNAL_ERROR', - wasClean: false, - }, - ) { - this.debug([ - '[CLOSE]', - `Event Code: ${event.code}`, - `Clean : ${event.wasClean}`, - `Reason : ${event.reason ?? 'No reason received'}`, - ]); - - /** - * Emitted when a shard's WebSocket closes. - * @private - * @event WebSocketShard#close - * @param {CloseEvent} event The received event - */ - this.emit(WebSocketShardEvents.Close, event); - } - - /** - * Called when the shard receives the READY payload. - * @param {Object} packet The received packet - * @private - */ - onReadyPacket(packet) { - if (!packet) { - this.debug([`Received broken packet: '${packet}'.`]); - return; - } - - /** - * Emitted when the shard receives the READY payload and is now waiting for guilds - * @event WebSocketShard#ready - */ - this.emit(WebSocketShardEvents.Ready); - - this.expectedGuilds = new Set(packet.guilds.map(guild => guild.id)); - this.status = Status.WaitingForGuilds; - } - - /** - * Called when a GuildCreate or GuildDelete for this shard was sent after READY payload was received, - * but before we emitted the READY event. - * @param {Snowflake} guildId the id of the Guild sent in the payload - * @private - */ - gotGuild(guildId) { - this.expectedGuilds.delete(guildId); - this.checkReady(); - } - - /** - * Checks if the shard can be marked as ready - * @private - */ - checkReady() { - // Step 0. Clear the ready timeout, if it exists - if (this.readyTimeout) { - clearTimeout(this.readyTimeout); - this.readyTimeout = null; - } - // Step 1. If we don't have any other guilds pending, we are ready - if (!this.expectedGuilds.size) { - this.debug(['Shard received all its guilds. Marking as fully ready.']); - this.status = Status.Ready; - - /** - * Emitted when the shard is fully ready. - * This event is emitted if: - * * all guilds were received by this shard - * * the ready timeout expired, and some guilds are unavailable - * @event WebSocketShard#allReady - * @param {?Set} unavailableGuilds Set of unavailable guilds, if any - */ - this.emit(WebSocketShardEvents.AllReady); - return; - } - const hasGuildsIntent = this.manager.client.options.intents.has(GatewayIntentBits.Guilds); - // Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds - // * The timeout is 15 seconds by default - // * This can be optionally changed in the client options via the `waitGuildTimeout` option - // * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds. - - const { waitGuildTimeout } = this.manager.client.options; - - this.readyTimeout = setTimeout( - () => { - this.debug([ - hasGuildsIntent - ? `Shard did not receive any guild packets in ${waitGuildTimeout} ms.` - : 'Shard will not receive anymore guild packets.', - `Unavailable guild count: ${this.expectedGuilds.size}`, - ]); - - this.readyTimeout = null; - this.status = Status.Ready; - - this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds); - }, - hasGuildsIntent ? waitGuildTimeout : 0, - ).unref(); - } - - /** - * Adds a packet to the queue to be sent to the gateway. - * If you use this method, make sure you understand that you need to provide - * a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands). - * Do not use this method if you don't know what you're doing. - * @param {Object} data The full packet to send - * @param {boolean} [important=false] If this packet should be added first in queue - * This parameter is **deprecated**. Important payloads are determined by their opcode instead. - */ - send(data, important = false) { - if (important && !deprecationEmittedForImportant) { - process.emitWarning( - 'Sending important payloads explicitly is deprecated. They are determined by their opcode implicitly now.', - 'DeprecationWarning', - ); - deprecationEmittedForImportant = true; - } - this.manager._ws.send(this.id, data); - } -} - -module.exports = WebSocketShard; diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js b/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js index 141f0abe9..87d724b58 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js @@ -3,7 +3,7 @@ const Events = require('../../../util/Events'); const Status = require('../../../util/Status'); -module.exports = (client, { d: data }, shard) => { +module.exports = (client, { d: data }, shardId) => { let guild = client.guilds.cache.get(data.id); if (guild) { if (!guild.available && !data.unavailable) { @@ -19,9 +19,9 @@ module.exports = (client, { d: data }, shard) => { } } else { // A new guild - data.shardId = shard.id; + data.shardId = shardId; guild = client.guilds._add(data); - if (client.ws.status === Status.Ready) { + if (client.status === Status.Ready) { /** * Emitted whenever the client joins a guild. * @event Client#guildCreate diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js index fece5d76f..53faae51f 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -1,20 +1,17 @@ 'use strict'; const Events = require('../../../util/Events'); -const Status = require('../../../util/Status'); -module.exports = (client, { d: data }, shard) => { +module.exports = (client, { d: data }) => { const guild = client.guilds.cache.get(data.guild_id); if (guild) { guild.memberCount++; const member = guild.members._add(data); - if (shard.status === Status.Ready) { - /** - * Emitted whenever a user joins a guild. - * @event Client#guildMemberAdd - * @param {GuildMember} member The member that has joined a guild - */ - client.emit(Events.GuildMemberAdd, member); - } + /** + * Emitted whenever a user joins a guild. + * @event Client#guildMemberAdd + * @param {GuildMember} member The member that has joined a guild + */ + client.emit(Events.GuildMemberAdd, member); } }; diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js index 72432af11..81f672016 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = (client, packet, shard) => { - client.actions.GuildMemberRemove.handle(packet.d, shard); +module.exports = (client, packet) => { + client.actions.GuildMemberRemove.handle(packet.d); }; diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js index cafc6bd59..5dab27e8d 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = (client, packet, shard) => { - client.actions.GuildMemberUpdate.handle(packet.d, shard); +module.exports = (client, packet) => { + client.actions.GuildMemberUpdate.handle(packet.d); }; diff --git a/packages/discord.js/src/client/websocket/handlers/READY.js b/packages/discord.js/src/client/websocket/handlers/READY.js index 82da01cf7..5ff890248 100644 --- a/packages/discord.js/src/client/websocket/handlers/READY.js +++ b/packages/discord.js/src/client/websocket/handlers/READY.js @@ -3,7 +3,7 @@ const ClientApplication = require('../../../structures/ClientApplication'); let ClientUser; -module.exports = (client, { d: data }, shard) => { +module.exports = (client, { d: data }, shardId) => { if (client.user) { client.user._patch(data.user); } else { @@ -13,7 +13,7 @@ module.exports = (client, { d: data }, shard) => { } for (const guild of data.guilds) { - guild.shardId = shard.id; + guild.shardId = shardId; client.guilds._add(guild); } @@ -22,6 +22,4 @@ module.exports = (client, { d: data }, shard) => { } else { client.application = new ClientApplication(client, data.application); } - - shard.checkReady(); }; diff --git a/packages/discord.js/src/client/websocket/handlers/RESUMED.js b/packages/discord.js/src/client/websocket/handlers/RESUMED.js deleted file mode 100644 index 27ed7ddc5..000000000 --- a/packages/discord.js/src/client/websocket/handlers/RESUMED.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -const Events = require('../../../util/Events'); - -module.exports = (client, packet, shard) => { - const replayed = shard.sessionInfo.sequence - shard.closeSequence; - /** - * Emitted when a shard resumes successfully. - * @event Client#shardResume - * @param {number} id The shard id that resumed - * @param {number} replayedEvents The amount of replayed events - */ - client.emit(Events.ShardResume, shard.id, replayed); -}; diff --git a/packages/discord.js/src/client/websocket/handlers/index.js b/packages/discord.js/src/client/websocket/handlers/index.js index e18b0196e..4af0714a1 100644 --- a/packages/discord.js/src/client/websocket/handlers/index.js +++ b/packages/discord.js/src/client/websocket/handlers/index.js @@ -49,7 +49,6 @@ const handlers = Object.fromEntries([ ['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')], ['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')], ['READY', require('./READY')], - ['RESUMED', require('./RESUMED')], ['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')], ['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')], ['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')], diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index 7d7b0b99e..c1552392a 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -12,8 +12,25 @@ * @property {'TokenMissing'} TokenMissing * @property {'ApplicationCommandPermissionsTokenMissing'} ApplicationCommandPermissionsTokenMissing -* @property {'BitFieldInvalid'} BitFieldInvalid + * @property {'WSCloseRequested'} WSCloseRequested + * This property is deprecated. + * @property {'WSConnectionExists'} WSConnectionExists + * This property is deprecated. + * @property {'WSNotOpen'} WSNotOpen + * This property is deprecated. + * @property {'ManagerDestroyed'} ManagerDestroyed + * This property is deprecated. + * @property {'BitFieldInvalid'} BitFieldInvalid + + * @property {'ShardingInvalid'} ShardingInvalid + * This property is deprecated. + * @property {'ShardingRequired'} ShardingRequired + * This property is deprecated. + * @property {'InvalidIntents'} InvalidIntents + * This property is deprecated. + * @property {'DisallowedIntents'} DisallowedIntents + * This property is deprecated. * @property {'ShardingNoShards'} ShardingNoShards * @property {'ShardingInProcess'} ShardingInProcess * @property {'ShardingInvalidEvalBroadcast'} ShardingInvalidEvalBroadcast @@ -32,10 +49,30 @@ * @property {'InviteOptionsMissingChannel'} InviteOptionsMissingChannel + * @property {'ButtonLabel'} ButtonLabel + * This property is deprecated. + * @property {'ButtonURL'} ButtonURL + * This property is deprecated. + * @property {'ButtonCustomId'} ButtonCustomId + * This property is deprecated. + + * @property {'SelectMenuCustomId'} SelectMenuCustomId + * This property is deprecated. + * @property {'SelectMenuPlaceholder'} SelectMenuPlaceholder + * This property is deprecated. + * @property {'SelectOptionLabel'} SelectOptionLabel + * This property is deprecated. + * @property {'SelectOptionValue'} SelectOptionValue + * This property is deprecated. + * @property {'SelectOptionDescription'} SelectOptionDescription + * This property is deprecated. + * @property {'InteractionCollectorError'} InteractionCollectorError * @property {'FileNotFound'} FileNotFound + * @property {'UserBannerNotFetched'} UserBannerNotFetched + * This property is deprecated. * @property {'UserNoDMChannel'} UserNoDMChannel * @property {'VoiceNotStageChannel'} VoiceNotStageChannel @@ -45,11 +82,19 @@ * @property {'ReqResourceType'} ReqResourceType + * @property {'ImageFormat'} ImageFormat + * This property is deprecated. + * @property {'ImageSize'} ImageSize + * This property is deprecated. + * @property {'MessageBulkDeleteType'} MessageBulkDeleteType * @property {'MessageContentType'} MessageContentType * @property {'MessageNonceRequired'} MessageNonceRequired * @property {'MessageNonceType'} MessageNonceType + * @property {'SplitMaxLen'} SplitMaxLen + * This property is deprecated. + * @property {'BanResolveId'} BanResolveId * @property {'FetchBanResolveId'} FetchBanResolveId @@ -83,11 +128,16 @@ * @property {'EmojiType'} EmojiType * @property {'EmojiManaged'} EmojiManaged * @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission + * @property {'MissingManageEmojisAndStickersPermission'} MissingManageEmojisAndStickersPermission + * This property is deprecated. Use `MissingManageGuildExpressionsPermission` instead. * * @property {'NotGuildSticker'} NotGuildSticker * @property {'ReactionResolveUser'} ReactionResolveUser + * @property {'VanityURL'} VanityURL + * This property is deprecated. + * @property {'InviteResolveCode'} InviteResolveCode * @property {'InviteNotFound'} InviteNotFound @@ -102,6 +152,8 @@ * @property {'InteractionAlreadyReplied'} InteractionAlreadyReplied * @property {'InteractionNotReplied'} InteractionNotReplied + * @property {'InteractionEphemeralReplied'} InteractionEphemeralReplied + * This property is deprecated. * @property {'CommandInteractionOptionNotFound'} CommandInteractionOptionNotFound * @property {'CommandInteractionOptionType'} CommandInteractionOptionType @@ -140,8 +192,17 @@ const keys = [ 'TokenMissing', 'ApplicationCommandPermissionsTokenMissing', + 'WSCloseRequested', + 'WSConnectionExists', + 'WSNotOpen', + 'ManagerDestroyed', + 'BitFieldInvalid', + 'ShardingInvalid', + 'ShardingRequired', + 'InvalidIntents', + 'DisallowedIntents', 'ShardingNoShards', 'ShardingInProcess', 'ShardingInvalidEvalBroadcast', @@ -160,10 +221,21 @@ const keys = [ 'InviteOptionsMissingChannel', + 'ButtonLabel', + 'ButtonURL', + 'ButtonCustomId', + + 'SelectMenuCustomId', + 'SelectMenuPlaceholder', + 'SelectOptionLabel', + 'SelectOptionValue', + 'SelectOptionDescription', + 'InteractionCollectorError', 'FileNotFound', + 'UserBannerNotFetched', 'UserNoDMChannel', 'VoiceNotStageChannel', @@ -173,11 +245,16 @@ const keys = [ 'ReqResourceType', + 'ImageFormat', + 'ImageSize', + 'MessageBulkDeleteType', 'MessageContentType', 'MessageNonceRequired', 'MessageNonceType', + 'SplitMaxLen', + 'BanResolveId', 'FetchBanResolveId', @@ -211,11 +288,14 @@ const keys = [ 'EmojiType', 'EmojiManaged', 'MissingManageGuildExpressionsPermission', + 'MissingManageEmojisAndStickersPermission', 'NotGuildSticker', 'ReactionResolveUser', + 'VanityURL', + 'InviteResolveCode', 'InviteNotFound', @@ -230,6 +310,7 @@ const keys = [ 'InteractionAlreadyReplied', 'InteractionNotReplied', + 'InteractionEphemeralReplied', 'CommandInteractionOptionNotFound', 'CommandInteractionOptionType', diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 45e8ac4f7..4297f221b 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -48,7 +48,6 @@ exports.SystemChannelFlagsBitField = require('./util/SystemChannelFlagsBitField' exports.ThreadMemberFlagsBitField = require('./util/ThreadMemberFlagsBitField'); exports.UserFlagsBitField = require('./util/UserFlagsBitField'); __exportStar(require('./util/Util.js'), exports); -exports.WebSocketShardEvents = require('./util/WebSocketShardEvents'); exports.version = require('../package.json').version; // Managers @@ -88,8 +87,6 @@ exports.ThreadManager = require('./managers/ThreadManager'); exports.ThreadMemberManager = require('./managers/ThreadMemberManager'); exports.UserManager = require('./managers/UserManager'); exports.VoiceStateManager = require('./managers/VoiceStateManager'); -exports.WebSocketManager = require('./client/websocket/WebSocketManager'); -exports.WebSocketShard = require('./client/websocket/WebSocketShard'); // Structures exports.ActionRow = require('./structures/ActionRow'); diff --git a/packages/discord.js/src/managers/GuildManager.js b/packages/discord.js/src/managers/GuildManager.js index 1ab9090bd..e02cd5e82 100644 --- a/packages/discord.js/src/managers/GuildManager.js +++ b/packages/discord.js/src/managers/GuildManager.js @@ -273,7 +273,7 @@ class GuildManager extends CachedManager { const data = await this.client.rest.get(Routes.guild(id), { query: makeURLSearchParams({ with_counts: options.withCounts ?? true }), }); - data.shardId = ShardClientUtil.shardIdForGuildId(id, this.client.options.shardCount); + data.shardId = ShardClientUtil.shardIdForGuildId(id, await this.client.ws.fetchShardCount()); return this._add(data, options.cache); } diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index 4b1b48e62..fb4713284 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -235,7 +235,7 @@ class GuildMemberManager extends CachedManager { return new Promise((resolve, reject) => { if (!query && !users) query = ''; - this.guild.shard.send({ + this.guild.client.ws.send(this.guild.shardId, { op: GatewayOpcodes.RequestGuildMembers, d: { guild_id: this.guild.id, diff --git a/packages/discord.js/src/sharding/Shard.js b/packages/discord.js/src/sharding/Shard.js index 9d9da67d5..da1d57b59 100644 --- a/packages/discord.js/src/sharding/Shard.js +++ b/packages/discord.js/src/sharding/Shard.js @@ -352,7 +352,7 @@ class Shard extends EventEmitter { if (message._ready) { this.ready = true; /** - * Emitted upon the shard's {@link Client#event:shardReady} event. + * Emitted upon the shard's {@link Client#event:clientReady} event. * @event Shard#ready */ this.emit(ShardEvents.Ready); @@ -363,29 +363,18 @@ class Shard extends EventEmitter { if (message._disconnect) { this.ready = false; /** - * Emitted upon the shard's {@link Client#event:shardDisconnect} event. + * Emitted upon the shard's {@link WebSocketShardEvents#Closed} event. * @event Shard#disconnect */ this.emit(ShardEvents.Disconnect); return; } - // Shard is attempting to reconnect - if (message._reconnecting) { - this.ready = false; - /** - * Emitted upon the shard's {@link Client#event:shardReconnecting} event. - * @event Shard#reconnecting - */ - this.emit(ShardEvents.Reconnecting); - return; - } - // Shard has resumed if (message._resume) { this.ready = true; /** - * Emitted upon the shard's {@link Client#event:shardResume} event. + * Emitted upon the shard's {@link WebSocketShardEvents#Resumed} event. * @event Shard#resume */ this.emit(ShardEvents.Resume); diff --git a/packages/discord.js/src/sharding/ShardClientUtil.js b/packages/discord.js/src/sharding/ShardClientUtil.js index c1bd4a800..bec0aba52 100644 --- a/packages/discord.js/src/sharding/ShardClientUtil.js +++ b/packages/discord.js/src/sharding/ShardClientUtil.js @@ -2,6 +2,7 @@ const process = require('node:process'); const { calculateShardId } = require('@discordjs/util'); +const { WebSocketShardEvents } = require('@discordjs/ws'); const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); const Events = require('../util/Events'); const { makeError, makePlainError } = require('../util/Util'); @@ -33,56 +34,32 @@ class ShardClientUtil { switch (mode) { case 'process': process.on('message', this._handleMessage.bind(this)); - client.on(Events.ShardReady, () => { + client.on(Events.ClientReady, () => { process.send({ _ready: true }); }); - client.on(Events.ShardDisconnect, () => { + client.ws.on(WebSocketShardEvents.Closed, () => { process.send({ _disconnect: true }); }); - client.on(Events.ShardReconnecting, () => { - process.send({ _reconnecting: true }); - }); - client.on(Events.ShardResume, () => { + client.ws.on(WebSocketShardEvents.Resumed, () => { process.send({ _resume: true }); }); break; case 'worker': this.parentPort = require('node:worker_threads').parentPort; this.parentPort.on('message', this._handleMessage.bind(this)); - client.on(Events.ShardReady, () => { + client.on(Events.ClientReady, () => { this.parentPort.postMessage({ _ready: true }); }); - client.on(Events.ShardDisconnect, () => { + client.ws.on(WebSocketShardEvents.Closed, () => { this.parentPort.postMessage({ _disconnect: true }); }); - client.on(Events.ShardReconnecting, () => { - this.parentPort.postMessage({ _reconnecting: true }); - }); - client.on(Events.ShardResume, () => { + client.ws.on(WebSocketShardEvents.Resumed, () => { this.parentPort.postMessage({ _resume: true }); }); break; } } - /** - * Array of shard ids of this client - * @type {number[]} - * @readonly - */ - get ids() { - return this.client.options.shards; - } - - /** - * Total number of shards - * @type {number} - * @readonly - */ - get count() { - return this.client.options.shardCount; - } - /** * Sends a message to the master process. * @param {*} message Message to send diff --git a/packages/discord.js/src/structures/ClientPresence.js b/packages/discord.js/src/structures/ClientPresence.js index f8e45af91..bec6ab169 100644 --- a/packages/discord.js/src/structures/ClientPresence.js +++ b/packages/discord.js/src/structures/ClientPresence.js @@ -16,19 +16,19 @@ class ClientPresence extends Presence { /** * Sets the client's presence * @param {PresenceData} presence The data to set the presence to - * @returns {ClientPresence} + * @returns {Promise} */ - set(presence) { + async set(presence) { const packet = this._parse(presence); this._patch(packet); if (presence.shardId === undefined) { - this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + await this.client._broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet }); } else if (Array.isArray(presence.shardId)) { - for (const shardId of presence.shardId) { - this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); - } + await Promise.all( + presence.shardId.map(shardId => this.client.ws.send(shardId, { op: GatewayOpcodes.PresenceUpdate, d: packet })), + ); } else { - this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + await this.client.ws.send(presence.shardId, { op: GatewayOpcodes.PresenceUpdate, d: packet }); } return this; } diff --git a/packages/discord.js/src/structures/ClientUser.js b/packages/discord.js/src/structures/ClientUser.js index 3da3a5279..98e795a70 100644 --- a/packages/discord.js/src/structures/ClientUser.js +++ b/packages/discord.js/src/structures/ClientUser.js @@ -135,7 +135,7 @@ class ClientUser extends User { /** * Sets the full presence of the client user. * @param {PresenceData} data Data for the presence - * @returns {ClientPresence} + * @returns {Promise} * @example * // Set the client user's presence * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' }); @@ -157,7 +157,7 @@ class ClientUser extends User { * Sets the status of the client user. * @param {PresenceStatusData} status Status to change to * @param {number|number[]} [shardId] Shard id(s) to have the activity set on - * @returns {ClientPresence} + * @returns {Promise} * @example * // Set the client user's status * client.user.setStatus('idle'); @@ -180,7 +180,7 @@ class ClientUser extends User { * Sets the activity the client user is playing. * @param {string|ActivityOptions} name Activity being played, or options for setting the activity * @param {ActivityOptions} [options] Options for setting the activity - * @returns {ClientPresence} + * @returns {Promise} * @example * // Set the client user's activity * client.user.setActivity('discord.js', { type: ActivityType.Watching }); @@ -196,7 +196,7 @@ class ClientUser extends User { * Sets/removes the AFK flag for the client user. * @param {boolean} [afk=true] Whether or not the user is AFK * @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on - * @returns {ClientPresence} + * @returns {Promise} */ setAFK(afk = true, shardId) { return this.setPresence({ afk, shardId }); diff --git a/packages/discord.js/src/structures/Guild.js b/packages/discord.js/src/structures/Guild.js index 6fc78f07a..733b6094b 100644 --- a/packages/discord.js/src/structures/Guild.js +++ b/packages/discord.js/src/structures/Guild.js @@ -27,7 +27,6 @@ const RoleManager = require('../managers/RoleManager'); const StageInstanceManager = require('../managers/StageInstanceManager'); const VoiceStateManager = require('../managers/VoiceStateManager'); const { resolveImage } = require('../util/DataResolver'); -const Status = require('../util/Status'); const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); const { discordSort, getSortableGroupTypes, resolvePartialEmoji } = require('../util/Util'); @@ -126,15 +125,6 @@ class Guild extends AnonymousGuild { this.shardId = data.shardId; } - /** - * The Shard this Guild belongs to. - * @type {WebSocketShard} - * @readonly - */ - get shard() { - return this.client.ws.shards.get(this.shardId); - } - _patch(data) { super._patch(data); this.id = data.id; @@ -1418,8 +1408,7 @@ class Guild extends AnonymousGuild { this.client.voice.adapters.set(this.id, methods); return { sendPayload: data => { - if (this.shard.status !== Status.Ready) return false; - this.shard.send(data); + this.client.ws.send(this.shardId, data); return true; }, destroy: () => { diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 43a8bcf81..8737623d6 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -325,6 +325,10 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayDispatchEvents} */ +/** + * @external GatewayDispatchPayload + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#GatewayDispatchPayload} + */ /** * @external GatewayIntentBits * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayIntentBits} diff --git a/packages/discord.js/src/util/Events.js b/packages/discord.js/src/util/Events.js index a2de59453..1ab65a13d 100644 --- a/packages/discord.js/src/util/Events.js +++ b/packages/discord.js/src/util/Events.js @@ -61,11 +61,6 @@ * @property {string} MessageReactionRemoveEmoji messageReactionRemoveEmoji * @property {string} MessageUpdate messageUpdate * @property {string} PresenceUpdate presenceUpdate - * @property {string} ShardDisconnect shardDisconnect - * @property {string} ShardError shardError - * @property {string} ShardReady shardReady - * @property {string} ShardReconnecting shardReconnecting - * @property {string} ShardResume shardResume * @property {string} StageInstanceCreate stageInstanceCreate * @property {string} StageInstanceDelete stageInstanceDelete * @property {string} StageInstanceUpdate stageInstanceUpdate @@ -99,7 +94,7 @@ module.exports = { ChannelDelete: 'channelDelete', ChannelPinsUpdate: 'channelPinsUpdate', ChannelUpdate: 'channelUpdate', - ClientReady: 'ready', + ClientReady: 'clientReady', Debug: 'debug', EntitlementCreate: 'entitlementCreate', EntitlementUpdate: 'entitlementUpdate', @@ -148,12 +143,6 @@ module.exports = { MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji', MessageUpdate: 'messageUpdate', PresenceUpdate: 'presenceUpdate', - Raw: 'raw', - ShardDisconnect: 'shardDisconnect', - ShardError: 'shardError', - ShardReady: 'shardReady', - ShardReconnecting: 'shardReconnecting', - ShardResume: 'shardResume', StageInstanceCreate: 'stageInstanceCreate', StageInstanceDelete: 'stageInstanceDelete', StageInstanceUpdate: 'stageInstanceUpdate', diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 04ec75840..14af76ea6 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -1,6 +1,7 @@ 'use strict'; const { DefaultRestOptions, DefaultUserAgentAppendix } = require('@discordjs/rest'); +const { DefaultWebSocketManagerOptions } = require('@discordjs/ws'); const { toSnakeCase } = require('./Transformers'); const { version } = require('../../package.json'); @@ -16,13 +17,8 @@ const { version } = require('../../package.json'); /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number|number[]|string} [shards] The shard's id to run, or an array of shard ids. If not specified, - * the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the - * recommended amount of shards from Discord and spawn that amount * @property {number} [closeTimeout=5_000] The amount of time in milliseconds to wait for the close frame to be received * from the WebSocket. Don't have this too high/low. It's best to have it between 2_000-6_000 ms. - * @property {number} [shardCount=1] The total amount of shards used by all processes of this bot - * (e.g. recommended shard count, shard count of the ShardingManager) * @property {CacheFactory} [makeCache] Function to create a cache. * You can use your own function, or the {@link Options} class to customize the Collection used for the cache. * Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`, @@ -33,12 +29,12 @@ const { version } = require('../../package.json'); * [guide](https://discordjs.guide/popular-topics/partials.html) for some * important usage information, as partials require you to put checks in place when handling data. * @property {boolean} [failIfNotExists=true] The default value for {@link MessageReplyOptions#failIfNotExists} - * @property {PresenceData} [presence={}] Presence data to use upon login + * @property {PresenceData} [presence] Presence data to use upon login * @property {IntentsResolvable} intents Intents to enable for this connection * @property {number} [waitGuildTimeout=15_000] Time in milliseconds that clients with the * {@link GatewayIntentBits.Guilds} gateway intent should wait for missing guilds to be received before being ready. * @property {SweeperOptions} [sweepers=this.DefaultSweeperSettings] Options for cache sweeping - * @property {WebsocketOptions} [ws] Options for the WebSocket + * @property {WebSocketManagerOptions} [ws] Options for the WebSocketManager * @property {RESTOptions} [rest] Options for the REST manager * @property {Function} [jsonTransformer] A function used to transform outgoing json data * @property {boolean} [enforceNonce=false] The default value for {@link MessageReplyOptions#enforceNonce} @@ -60,40 +56,6 @@ const { version } = require('../../package.json'); * This property is optional when the key is `invites`, `messages`, or `threads` and `lifetime` is set */ -/** - * A function to determine what strategy to use for sharding internally. - * ```js - * (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 }) - * ``` - * @typedef {Function} BuildStrategyFunction - * @param {WSWebSocketManager} manager The WebSocketManager that is going to initiate the sharding - * @returns {IShardingStrategy} The strategy to use for sharding - */ - -/** - * A function to change the concurrency handling for shard identifies of this manager - * ```js - * async (manager) => { - * const gateway = await manager.fetchGatewayInformation(); - * return new SimpleIdentifyThrottler(gateway.session_start_limit.max_concurrency); - * } - * ``` - * @typedef {Function} IdentifyThrottlerFunction - * @param {WSWebSocketManager} manager The WebSocketManager that is going to initiate the sharding - * @returns {Awaitable} The identify throttler that this ws manager will use - */ - -/** - * WebSocket options (these are left as snake_case to match the API) - * @typedef {Object} WebsocketOptions - * @property {number} [large_threshold=50] Number of members in a guild after which offline users will no longer be - * sent in the initial guild member list, must be between 50 and 250 - * @property {number} [version=10] The Discord gateway version to use Changing this can break the library; - * only set this if you know what you are doing - * @property {BuildStrategyFunction} [buildStrategy] Builds the strategy to use for sharding - * @property {IdentifyThrottlerFunction} [buildIdentifyThrottler] Builds the identify throttler to use for sharding - */ - /** * Contains various utilities for client options. */ @@ -114,15 +76,14 @@ class Options extends null { return { closeTimeout: 5_000, waitGuildTimeout: 15_000, - shardCount: 1, makeCache: this.cacheWithLimits(this.DefaultMakeCacheSettings), partials: [], failIfNotExists: true, enforceNonce: false, - presence: {}, sweepers: this.DefaultSweeperSettings, ws: { - large_threshold: 50, + ...DefaultWebSocketManagerOptions, + largeThreshold: 50, version: 10, }, rest: { @@ -224,7 +185,7 @@ module.exports = Options; */ /** - * @external WSWebSocketManager + * @external WebSocketManager * @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class} */ diff --git a/packages/discord.js/src/util/Status.js b/packages/discord.js/src/util/Status.js index e5241971c..c9daddc27 100644 --- a/packages/discord.js/src/util/Status.js +++ b/packages/discord.js/src/util/Status.js @@ -5,14 +5,8 @@ const { createEnum } = require('./Enums'); /** * @typedef {Object} Status * @property {number} Ready - * @property {number} Connecting - * @property {number} Reconnecting * @property {number} Idle - * @property {number} Nearly - * @property {number} Disconnected * @property {number} WaitingForGuilds - * @property {number} Identifying - * @property {number} Resuming */ // JSDoc for IntelliSense purposes @@ -20,14 +14,4 @@ const { createEnum } = require('./Enums'); * @type {Status} * @ignore */ -module.exports = createEnum([ - 'Ready', - 'Connecting', - 'Reconnecting', - 'Idle', - 'Nearly', - 'Disconnected', - 'WaitingForGuilds', - 'Identifying', - 'Resuming', -]); +module.exports = createEnum(['Ready', 'Idle', 'WaitingForGuilds']); diff --git a/packages/discord.js/src/util/WebSocketShardEvents.js b/packages/discord.js/src/util/WebSocketShardEvents.js deleted file mode 100644 index 81e05f2c4..000000000 --- a/packages/discord.js/src/util/WebSocketShardEvents.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -/** - * @typedef {Object} WebSocketShardEvents - * @property {string} Close close - * @property {string} Destroyed destroyed - * @property {string} InvalidSession invalidSession - * @property {string} Ready ready - * @property {string} Resumed resumed - * @property {string} AllReady allReady - */ - -// JSDoc for IntelliSense purposes -/** - * @type {WebSocketShardEvents} - * @ignore - */ -module.exports = { - Close: 'close', - Destroyed: 'destroyed', - InvalidSession: 'invalidSession', - Ready: 'ready', - Resumed: 'resumed', - AllReady: 'allReady', -}; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 42614f56a..fa08c5f54 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -20,12 +20,7 @@ import { import { Awaitable, JSONEncodable } from '@discordjs/util'; import { Collection, ReadonlyCollection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; -import { - WebSocketManager as WSWebSocketManager, - IShardingStrategy, - IIdentifyThrottler, - SessionInfo, -} from '@discordjs/ws'; +import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws'; import { APIActionRowComponent, APIApplicationCommandInteractionData, @@ -50,7 +45,6 @@ import { ButtonStyle, ChannelType, ComponentType, - GatewayDispatchEvents, GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData, GuildFeature, @@ -170,6 +164,8 @@ import { GuildScheduledEventRecurrenceRuleWeekday, GuildScheduledEventRecurrenceRuleMonth, GuildScheduledEventRecurrenceRuleFrequency, + GatewaySendPayload, + GatewayDispatchPayload, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -956,8 +952,16 @@ export type If = Value ex export class Client extends BaseClient { public constructor(options: ClientOptions); private actions: unknown; + private expectedGuilds: Set; + private readonly packetQueue: unknown[]; private presence: ClientPresence; + private pings: Collection; + private readyTimeout: NodeJS.Timeout | null; + private _broadcast(packet: GatewaySendPayload): void; private _eval(script: string): unknown; + private _handlePacket(packet?: GatewayDispatchPayload, shardId?: number): boolean; + private _checkReady(): void; + private _triggerClientReady(): void; private _validateOptions(options: ClientOptions): void; private get _censoredToken(): string | null; // This a technique used to brand the ready state. Or else we'll get `never` errors on typeguard checks. @@ -979,17 +983,21 @@ export class Client extends BaseClient { public channels: ChannelManager; public get emojis(): BaseGuildEmojiManager; public guilds: GuildManager; + public lastPingTimestamp: number; public options: Omit & { intents: IntentsBitField }; + public get ping(): number; public get readyAt(): If; public readyTimestamp: If; public sweepers: Sweepers; public shard: ShardClientUtil | null; + public status: Status; public token: If; public get uptime(): If; public user: If; public users: UserManager; public voice: ClientVoiceManager; public ws: WebSocketManager; + public destroy(): Promise; public deleteWebhook(id: Snowflake, options?: WebhookDeleteOptions): Promise; public fetchGuildPreview(guild: GuildResolvable): Promise; @@ -1431,7 +1439,6 @@ export class Guild extends AnonymousGuild { public get safetyAlertsChannel(): TextChannel | null; public safetyAlertsChannelId: Snowflake | null; public scheduledEvents: GuildScheduledEventManager; - public get shard(): WebSocketShard; public shardId: number; public stageInstances: StageInstanceManager; public stickers: GuildStickerManager; @@ -3632,70 +3639,6 @@ export class WebhookClient extends BaseClient { public send(options: string | MessagePayload | WebhookMessageCreateOptions): Promise; } -export class WebSocketManager extends EventEmitter { - private constructor(client: Client); - private readonly packetQueue: unknown[]; - private destroyed: boolean; - - public readonly client: Client; - public gateway: string | null; - public shards: Collection; - public status: Status; - public get ping(): number; - - public on(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this; - public once(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this; - - private debug(messages: readonly string[], shardId?: number): void; - private connect(): Promise; - private broadcast(packet: unknown): void; - private destroy(): Promise; - private handlePacket(packet?: unknown, shard?: WebSocketShard): boolean; - private checkShardsReady(): void; - private triggerClientReady(): void; -} - -export interface WebSocketShardEventTypes { - ready: []; - resumed: []; - invalidSession: []; - destroyed: []; - close: [event: CloseEvent]; - allReady: [unavailableGuilds?: Set]; -} - -export class WebSocketShard extends EventEmitter { - private constructor(manager: WebSocketManager, id: number); - private closeSequence: number; - private sessionInfo: SessionInfo | null; - public lastPingTimestamp: number; - private expectedGuilds: Set | null; - private readyTimeout: NodeJS.Timeout | null; - - public manager: WebSocketManager; - public id: number; - public status: Status; - public ping: number; - - private debug(messages: readonly string[]): void; - private onReadyPacket(packet: unknown): void; - private gotGuild(guildId: Snowflake): void; - private checkReady(): void; - private emitClose(event?: CloseEvent): void; - - public send(data: unknown, important?: boolean): void; - - public on( - event: Event, - listener: (...args: WebSocketShardEventTypes[Event]) => void, - ): this; - - public once( - event: Event, - listener: (...args: WebSocketShardEventTypes[Event]) => void, - ): this; -} - export class Widget extends Base { private constructor(client: Client, data: RawWidgetData); private _patch(data: RawWidgetData): void; @@ -5133,6 +5076,7 @@ export interface ClientEvents { oldChannel: DMChannel | NonThreadGuildBasedChannel, newChannel: DMChannel | NonThreadGuildBasedChannel, ]; + clientReady: [client: Client]; debug: [message: string]; warn: [message: string]; emojiCreate: [emoji: GuildEmoji]; @@ -5186,7 +5130,6 @@ export interface ClientEvents { newMessage: OmitPartialGroupDMChannel, ]; presenceUpdate: [oldPresence: Presence | null, newPresence: Presence]; - ready: [client: Client]; invalidated: []; roleCreate: [role: Role]; roleDelete: [role: Role]; @@ -5206,11 +5149,6 @@ export interface ClientEvents { voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; webhooksUpdate: [channel: TextChannel | NewsChannel | VoiceChannel | ForumChannel | MediaChannel]; interactionCreate: [interaction: Interaction]; - shardDisconnect: [closeEvent: CloseEvent, shardId: number]; - shardError: [error: Error, shardId: number]; - shardReady: [shardId: number, unavailableGuilds: Set | undefined]; - shardReconnecting: [shardId: number]; - shardResume: [shardId: number, replayedEvents: number]; stageInstanceCreate: [stageInstance: StageInstance]; stageInstanceUpdate: [oldStageInstance: StageInstance | null, newStageInstance: StageInstance]; stageInstanceDelete: [stageInstance: StageInstance]; @@ -5232,8 +5170,6 @@ export interface ClientFetchInviteOptions { } export interface ClientOptions { - shards?: number | readonly number[] | 'auto'; - shardCount?: number; closeTimeout?: number; makeCache?: CacheFactory; allowedMentions?: MessageMentionOptions; @@ -5243,7 +5179,7 @@ export interface ClientOptions { intents: BitFieldResolvable; waitGuildTimeout?: number; sweepers?: SweeperOptions; - ws?: WebSocketOptions; + ws?: Partial; rest?: Partial; jsonTransformer?: (obj: unknown) => unknown; enforceNonce?: boolean; @@ -5263,14 +5199,6 @@ export interface ClientUserEditOptions { banner?: BufferResolvable | Base64Resolvable | null; } -export interface CloseEvent { - /** @deprecated Not used anymore since using {@link @discordjs/ws#(WebSocketManager:class)} internally */ - wasClean: boolean; - code: number; - /** @deprecated Not used anymore since using {@link @discordjs/ws#(WebSocketManager:class)} internally */ - reason: string; -} - export type CollectorFilter = (...args: Arguments) => Awaitable; export interface CollectorOptions { @@ -5364,7 +5292,7 @@ export enum Events { AutoModerationRuleCreate = 'autoModerationRuleCreate', AutoModerationRuleDelete = 'autoModerationRuleDelete', AutoModerationRuleUpdate = 'autoModerationRuleUpdate', - ClientReady = 'ready', + ClientReady = 'clientReady', EntitlementCreate = 'entitlementCreate', EntitlementDelete = 'entitlementDelete', EntitlementUpdate = 'entitlementUpdate', @@ -5452,15 +5380,6 @@ export enum ShardEvents { Spawn = 'spawn', } -export enum WebSocketShardEvents { - Close = 'close', - Destroyed = 'destroyed', - InvalidSession = 'invalidSession', - Ready = 'ready', - Resumed = 'resumed', - AllReady = 'allReady', -} - export enum Status { Ready = 0, Connecting = 1, @@ -6879,13 +6798,6 @@ export interface WebhookMessageCreateOptions extends Omit; -} - export interface WidgetActivity { name: string; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index df99d3f29..9fc56a981 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -49,7 +49,6 @@ import { Client, ClientApplication, ClientUser, - CloseEvent, Collection, ChatInputCommandInteraction, CommandInteractionOption, @@ -100,7 +99,6 @@ import { User, VoiceChannel, Shard, - WebSocketShard, Collector, GuildAuditLogsEntry, GuildAuditLogs, @@ -112,7 +110,6 @@ import { RepliableInteraction, ThreadChannelType, Events, - WebSocketShardEvents, Status, CategoryChannelChildManager, ActionRowData, @@ -677,7 +674,7 @@ client.on('presenceUpdate', (oldPresence, { client }) => { declare const slashCommandBuilder: SlashCommandBuilder; declare const contextMenuCommandBuilder: ContextMenuCommandBuilder; -client.on('ready', async client => { +client.on('clientReady', async client => { expectType>(client); console.log(`Client is logged in as ${client.user.tag} and ready!`); @@ -1305,8 +1302,8 @@ client.on('guildCreate', async g => { }); // EventEmitter static method overrides -expectType]>>(Client.once(client, 'ready')); -expectType]>>(Client.on(client, 'ready')); +expectType]>>(Client.once(client, 'clientReady')); +expectType]>>(Client.on(client, 'clientReady')); client.login('absolutely-valid-token'); @@ -1426,7 +1423,6 @@ reactionCollector.on('dispose', (...args) => { // Make sure the properties are typed correctly, and that no backwards properties // (K -> V and V -> K) exist: expectAssignable<'messageCreate'>(Events.MessageCreate); -expectAssignable<'close'>(WebSocketShardEvents.Close); expectAssignable<'death'>(ShardEvents.Death); expectAssignable<1>(Status.Connecting); @@ -2100,12 +2096,6 @@ shard.on('death', process => { expectType(process); }); -declare const webSocketShard: WebSocketShard; - -webSocketShard.on('close', event => { - expectType(event); -}); - declare const collector: Collector; collector.on('collect', (collected, ...other) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2340a8c5d..1ff2a3ee1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -932,8 +932,8 @@ importers: specifier: workspace:^ version: link:../util '@discordjs/ws': - specifier: 1.1.1 - version: 1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) + specifier: workspace:^ + version: link:../ws '@sapphire/snowflake': specifier: 3.5.3 version: 3.5.3 @@ -2609,10 +2609,6 @@ packages: resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} engines: {node: '>=16.11.0'} - '@discordjs/collection@2.1.0': - resolution: {integrity: sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==} - engines: {node: '>=18'} - '@discordjs/formatters@0.5.0': resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==} engines: {node: '>=18'} @@ -2625,22 +2621,10 @@ packages: resolution: {integrity: sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==} engines: {node: '>=12.0.0'} - '@discordjs/rest@2.3.0': - resolution: {integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==} - engines: {node: '>=16.11.0'} - - '@discordjs/util@1.1.0': - resolution: {integrity: sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==} - engines: {node: '>=16.11.0'} - '@discordjs/util@1.1.1': resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} engines: {node: '>=18'} - '@discordjs/ws@1.1.1': - resolution: {integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==} - engines: {node: '>=16.11.0'} - '@edge-runtime/format@2.2.1': resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} engines: {node: '>=16'} @@ -7631,9 +7615,6 @@ packages: discord-api-types@0.37.101: resolution: {integrity: sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==} - discord-api-types@0.37.83: - resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} - discord-api-types@0.37.97: resolution: {integrity: sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==} @@ -13035,10 +13016,6 @@ packages: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} - undici@6.13.0: - resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==} - engines: {node: '>=18.0'} - undici@6.19.8: resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} engines: {node: '>=18.17'} @@ -14857,8 +14834,6 @@ snapshots: '@discordjs/collection@1.5.3': {} - '@discordjs/collection@2.1.0': {} - '@discordjs/formatters@0.5.0': dependencies: discord-api-types: 0.37.97 @@ -14886,37 +14861,8 @@ snapshots: - encoding - supports-color - '@discordjs/rest@2.3.0': - dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.3 - '@sapphire/snowflake': 3.5.3 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.83 - magic-bytes.js: 1.10.0 - tslib: 2.6.3 - undici: 6.13.0 - - '@discordjs/util@1.1.0': {} - '@discordjs/util@1.1.1': {} - '@discordjs/ws@1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)': - dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/rest': 2.3.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.3 - '@types/ws': 8.5.12 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.83 - tslib: 2.6.3 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@edge-runtime/format@2.2.1': {} '@edge-runtime/node-utils@2.3.0': {} @@ -21518,8 +21464,6 @@ snapshots: discord-api-types@0.37.101: {} - discord-api-types@0.37.83: {} - discord-api-types@0.37.97: {} dlv@1.1.3: {} @@ -28791,8 +28735,6 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - undici@6.13.0: {} - undici@6.19.8: {} unicode-canonical-property-names-ecmascript@2.0.0: {}