From 1d63ba9b570182d9213fed05bef5ca75458acac1 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:58:40 +0100 Subject: [PATCH 01/20] feat(WebSocketManager): use /ws package internally --- packages/discord.js/package.json | 1 + .../src/client/websocket/WebSocketManager.js | 279 +++---- .../src/client/websocket/WebSocketShard.js | 743 +----------------- packages/discord.js/typings/index.d.ts | 5 - yarn.lock | 1 + 5 files changed, 137 insertions(+), 892 deletions(-) diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 6def7b2d16c8..98cc89047945 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -55,6 +55,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/rest": "workspace:^", "@discordjs/util": "workspace:^", + "@discordjs/ws": "workspace:^", "@sapphire/snowflake": "^3.4.2", "@types/ws": "^8.5.4", "discord-api-types": "^0.37.40", diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index f96cdf12e5af..81de47fccb71 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -2,9 +2,13 @@ const EventEmitter = require('node:events'); const { setImmediate } = require('node:timers'); -const { setTimeout: sleep } = require('node:timers/promises'); const { Collection } = require('@discordjs/collection'); -const { GatewayCloseCodes, GatewayDispatchEvents, Routes } = require('discord-api-types/v10'); +const { + WebSocketManager: WSWebSocketManager, + WebSocketShardEvents: WSWebSocketShardEvents, + 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'); @@ -22,16 +26,6 @@ const BeforeReadyWhitelist = [ GatewayDispatchEvents.GuildMemberRemove, ]; -const unrecoverableErrorCodeMap = { - [GatewayCloseCodes.AuthenticationFailed]: ErrorCodes.TokenInvalid, - [GatewayCloseCodes.InvalidShard]: ErrorCodes.ShardingInvalid, - [GatewayCloseCodes.ShardingRequired]: ErrorCodes.ShardingRequired, - [GatewayCloseCodes.InvalidIntents]: ErrorCodes.InvalidIntents, - [GatewayCloseCodes.DisallowedIntents]: ErrorCodes.DisallowedIntents, -}; - -const UNRESUMABLE_CLOSE_CODES = [1000, GatewayCloseCodes.AlreadyAuthenticated, GatewayCloseCodes.InvalidSeq]; - /** * The WebSocket manager for this client. * This class forwards raw dispatch events, @@ -56,27 +50,12 @@ class WebSocketManager extends EventEmitter { */ this.gateway = null; - /** - * The amount of shards this manager handles - * @private - * @type {number} - */ - this.totalShards = this.client.options.shards.length; - /** * A collection of all shards this manager handles * @type {Collection} */ this.shards = new Collection(); - /** - * An array of shards to be connected or that need to reconnect - * @type {Set} - * @private - * @name WebSocketManager#shardQueue - */ - Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true }); - /** * An array of queued events before this WebSocketManager became ready * @type {Object[]} @@ -98,12 +77,7 @@ class WebSocketManager extends EventEmitter { */ this.destroyed = false; - /** - * If this manager is currently reconnecting one or multiple shards - * @type {boolean} - * @private - */ - this.reconnecting = false; + this._ws = null; } /** @@ -119,11 +93,11 @@ class WebSocketManager extends EventEmitter { /** * Emits a debug message. * @param {string} message The debug message - * @param {?WebSocketShard} [shard] The shard that emitted this message, if any + * @param {?number} [shardId] The id of the shard that emitted this message, if any * @private */ - debug(message, shard) { - this.client.emit(Events.Debug, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`); + debug(message, shardId) { + this.client.emit(Events.Debug, `[WS => ${shardId ? `Shard ${shardId}` : 'Manager'}] ${message}`); } /** @@ -132,11 +106,30 @@ class WebSocketManager extends EventEmitter { */ 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: 1000, reason: 'Login with differing token requested' }); + this._ws = null; + } + if (!this._ws) { + this._ws = new WSWebSocketManager({ + 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, + }); + this.attachEvents(); + } + const { url: gatewayURL, shards: recommendedShards, session_start_limit: sessionStartLimit, - } = await this.client.rest.get(Routes.gatewayBot()).catch(error => { + } = await this._ws.fetchGatewayInformation().catch(error => { throw error.status === 401 ? invalidToken : error; }); @@ -152,156 +145,95 @@ class WebSocketManager extends EventEmitter { this.gateway = `${gatewayURL}/`; - let { shards } = this.client.options; - - if (shards === 'auto') { - this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); - this.totalShards = this.client.options.shardCount = recommendedShards; - shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); - } - - this.totalShards = shards.length; - this.debug(`Spawning shards: ${shards.join(', ')}`); - this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); - - return this.createShards(); - } - - /** - * Handles the creation of a shard. - * @returns {Promise} - * @private - */ - async createShards() { - // If we don't have any shards to handle, return - if (!this.shardQueue.size) return false; - - const [shard] = this.shardQueue; - - this.shardQueue.delete(shard); + await this._ws.connect(); - if (!shard.eventsAttached) { - 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); - - if (!this.shardQueue.size) this.reconnecting = false; - this.checkShardsReady(); - }); + this.totalShards = this.client.options.shardCount = await this._ws.getShardCount(); + this.client.options.shards = await this._ws.getShardIds(); + 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.Close, event => { - if (event.code === 1_000 ? this.destroyed : event.code in unrecoverableErrorCodeMap) { + shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => { /** - * 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 + * 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.ShardDisconnect, event, shard.id); - this.debug(GatewayCloseCodes[event.code], shard); - return; - } + this.client.emit(Events.ShardReady, shard.id, unavailableGuilds); - if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) { - // These event codes cannot be resumed - shard.sessionId = null; - } - - /** - * 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, shard.id); - - this.shardQueue.add(shard); - - if (shard.sessionId) this.debug(`Session id is present, attempting an immediate reconnect...`, shard); - this.reconnect(); - }); - - shard.on(WebSocketShardEvents.InvalidSession, () => { - this.client.emit(Events.ShardReconnecting, shard.id); - }); - - shard.on(WebSocketShardEvents.Destroyed, () => { - this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); - - this.client.emit(Events.ShardReconnecting, shard.id); - - this.shardQueue.add(shard); - this.reconnect(); - }); - - shard.eventsAttached = true; - } - - this.shards.set(shard.id, shard); - - try { - await shard.connect(); - } catch (error) { - if (error?.code && error.code in unrecoverableErrorCodeMap) { - throw new DiscordjsError(unrecoverableErrorCodeMap[error.code]); - // 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); - } else { - throw error; + this.checkShardsReady(); + }); } } - // If we have more shards, add a 5s delay - if (this.shardQueue.size) { - this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`); - await sleep(5_000); - return this.createShards(); - } - - return true; } /** - * Handles reconnects for this manager. + * Attaches event handlers to the internal WebSocketShardManager from `@discordjs/ws`. * @private - * @returns {Promise} */ - async reconnect() { - if (this.reconnecting || this.status !== Status.Ready) return false; - this.reconnecting = true; - try { - await this.createShards(); - } catch (error) { - this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); - if (error.httpStatus !== 401) { - this.debug(`Possible network error occurred. Retrying in 5s...`); - await sleep(5_000); - this.reconnecting = false; - return this.reconnect(); + attachEvents() { + this._ws.on(WSWebSocketShardEvents.Debug, ({ message, shardId }) => this.debug(message, shardId)); + this._ws.on(WSWebSocketShardEvents.Dispatch, ({ packet, shardId }) => { + this.client.emit(Events.Raw, packet, shardId); + const shard = this.shards.get(shardId); + this.handlePacket(packet, shard); + if (shard.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) { + shard.onGuildPacket(packet); } - // If we get an error at this point, it means we cannot reconnect anymore - if (this.client.listenerCount(Events.Invalidated)) { + }); + + this._ws.on(WSWebSocketShardEvents.Ready, ({ data, shardId }) => { + this.shards.get(shardId).onReadyPacket(data); + }); + + this._ws.on(WSWebSocketShardEvents.Closed, ({ code, reason = '', shardId }) => { + this.shards.get(shardId).status = code === CloseCodes.Resuming ? Status.Resuming : Status.Disconnected; + if (code === CloseCodes.Normal && this.destroyed) { /** - * Emitted when the client's session becomes invalidated. - * You are expected to handle closing the process gracefully and preventing a boot loop - * if you are listening to this event. - * @event Client#invalidated + * 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.Invalidated); - // Destroy just the shards. This means you have to handle the cleanup yourself - this.destroy(); - } else { - this.client.destroy(); + this.client.emit(Events.ShardDisconnect, { code, reason, wasClean: true }, shardId); + this.debug(GatewayCloseCodes[code], shardId); + return; } - } finally { - this.reconnecting = false; - } - return true; + + /** + * 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.Resumed, ({ shardId }) => { + /** + * Emitted when the shard resumes successfully + * @event WebSocketShard#resumed + */ + this.shards.get(shardId).emit(WebSocketShardEvents.Resumed); + }); + + this._ws.on(WSWebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency, shardId }) => { + const shard = this.shards.get(shardId); + shard.lastPingTimestamp = heartbeatAt; + shard.ping = latency; + }); + + // TODO: refactor once error event gets exposed publicly + this._ws.on('error', ({ err, 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, err, shardId); + }); } /** @@ -310,7 +242,7 @@ class WebSocketManager extends EventEmitter { * @private */ broadcast(packet) { - for (const shard of this.shards.values()) shard.send(packet); + for (const shardId of this.shards.keys()) this._ws.send(shardId, packet); } /** @@ -322,8 +254,7 @@ class WebSocketManager extends EventEmitter { // TODO: Make a util for getting a stack this.debug(`Manager was destroyed. Called by:\n${new Error().stack}`); this.destroyed = true; - this.shardQueue.clear(); - for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false }); + this._ws.destroy({ code: 1_000 }); } /** diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index c41b656360a7..7ffee867a431 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -1,22 +1,12 @@ 'use strict'; const EventEmitter = require('node:events'); -const { setTimeout, setInterval, clearTimeout, clearInterval } = require('node:timers'); -const { GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes } = require('discord-api-types/v10'); -const WebSocket = require('../../WebSocket'); -const Events = require('../../util/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'); -const STATUS_KEYS = Object.keys(Status); -const CONNECTION_STATE = Object.keys(WebSocket.WebSocket); - -let zlib; - -try { - zlib = require('zlib-sync'); -} catch {} // eslint-disable-line no-empty - /** * Represents a Shard's WebSocket connection * @extends {EventEmitter} @@ -43,34 +33,6 @@ class WebSocketShard extends EventEmitter { */ this.status = Status.Idle; - /** - * The current sequence of the shard - * @type {number} - * @private - */ - this.sequence = -1; - - /** - * The sequence of the shard after close - * @type {number} - * @private - */ - this.closeSequence = 0; - - /** - * The current session id of the shard - * @type {?string} - * @private - */ - this.sessionId = null; - - /** - * The resume url for this shard - * @type {?string} - * @private - */ - this.resumeURL = null; - /** * The previous heartbeat ping of the shard * @type {number} @@ -83,81 +45,6 @@ class WebSocketShard extends EventEmitter { */ this.lastPingTimestamp = -1; - /** - * If we received a heartbeat ack back. Used to identify zombie connections - * @type {boolean} - * @private - */ - this.lastHeartbeatAcked = true; - - /** - * Used to prevent calling {@link WebSocketShard#event:close} twice while closing or terminating the WebSocket. - * @type {boolean} - * @private - */ - this.closeEmitted = false; - - /** - * Contains the rate limit queue and metadata - * @name WebSocketShard#ratelimit - * @type {Object} - * @private - */ - Object.defineProperty(this, 'ratelimit', { - value: { - queue: [], - total: 120, - remaining: 120, - time: 60e3, - timer: null, - }, - }); - - /** - * The WebSocket connection for the current shard - * @name WebSocketShard#connection - * @type {?WebSocket} - * @private - */ - Object.defineProperty(this, 'connection', { value: null, writable: true }); - - /** - * @external Inflate - * @see {@link https://www.npmjs.com/package/zlib-sync} - */ - - /** - * The compression to use - * @name WebSocketShard#inflate - * @type {?Inflate} - * @private - */ - Object.defineProperty(this, 'inflate', { value: null, writable: true }); - - /** - * The HELLO timeout - * @name WebSocketShard#helloTimeout - * @type {?NodeJS.Timeout} - * @private - */ - Object.defineProperty(this, 'helloTimeout', { value: null, writable: true }); - - /** - * The WebSocket timeout. - * @name WebSocketShard#wsCloseTimeout - * @type {?NodeJS.Timeout} - * @private - */ - Object.defineProperty(this, 'wsCloseTimeout', { value: null, writable: true }); - - /** - * If the manager attached its event handlers on the shard - * @name WebSocketShard#eventsAttached - * @type {boolean} - * @private - */ - Object.defineProperty(this, 'eventsAttached', { value: false, writable: true }); - /** * A set of guild ids this shard expects to receive * @name WebSocketShard#expectedGuilds @@ -173,14 +60,6 @@ class WebSocketShard extends EventEmitter { * @private */ Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); - - /** - * Time when the WebSocket connection was opened - * @name WebSocketShard#connectedAt - * @type {number} - * @private - */ - Object.defineProperty(this, 'connectedAt', { value: 0, writable: true }); } /** @@ -192,160 +71,6 @@ class WebSocketShard extends EventEmitter { this.manager.debug(message, this); } - /** - * Connects the shard to the gateway. - * @private - * @returns {Promise} A promise that will resolve if the shard turns ready successfully, - * or reject if we couldn't connect - */ - connect() { - const { client } = this.manager; - - if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.Ready) { - return Promise.resolve(); - } - - const gateway = this.resumeURL ?? this.manager.gateway; - - return new Promise((resolve, reject) => { - const cleanup = () => { - this.removeListener(WebSocketShardEvents.Close, onClose); - this.removeListener(WebSocketShardEvents.Ready, onReady); - this.removeListener(WebSocketShardEvents.Resumed, onResumed); - this.removeListener(WebSocketShardEvents.InvalidSession, onInvalidOrDestroyed); - this.removeListener(WebSocketShardEvents.Destroyed, onInvalidOrDestroyed); - }; - - const onReady = () => { - cleanup(); - resolve(); - }; - - const onResumed = () => { - cleanup(); - resolve(); - }; - - const onClose = event => { - cleanup(); - reject(event); - }; - - const onInvalidOrDestroyed = () => { - cleanup(); - // eslint-disable-next-line prefer-promise-reject-errors - reject(); - }; - - this.once(WebSocketShardEvents.Ready, onReady); - this.once(WebSocketShardEvents.Resumed, onResumed); - this.once(WebSocketShardEvents.Close, onClose); - this.once(WebSocketShardEvents.InvalidSession, onInvalidOrDestroyed); - this.once(WebSocketShardEvents.Destroyed, onInvalidOrDestroyed); - - if (this.connection?.readyState === WebSocket.OPEN) { - this.debug('An open connection was found, attempting an immediate identify.'); - this.identify(); - return; - } - - if (this.connection) { - this.debug(`A connection object was found. Cleaning up before continuing. - State: ${CONNECTION_STATE[this.connection.readyState]}`); - this.destroy({ emit: false }); - } - - const wsQuery = { v: client.options.ws.version }; - - if (zlib) { - this.inflate = new zlib.Inflate({ - chunkSize: 65535, - flush: zlib.Z_SYNC_FLUSH, - to: WebSocket.encoding === 'json' ? 'string' : '', - }); - wsQuery.compress = 'zlib-stream'; - } - - this.debug( - `[CONNECT] - Gateway : ${gateway} - Version : ${client.options.ws.version} - Encoding : ${WebSocket.encoding} - Compression: ${zlib ? 'zlib-stream' : 'none'}`, - ); - - this.status = this.status === Status.Disconnected ? Status.Reconnecting : Status.Connecting; - this.setHelloTimeout(); - - this.connectedAt = Date.now(); - - // Adding a handshake timeout to just make sure no zombie connection appears. - const ws = (this.connection = WebSocket.create(gateway, wsQuery, { handshakeTimeout: 30_000 })); - ws.onopen = this.onOpen.bind(this); - ws.onmessage = this.onMessage.bind(this); - ws.onerror = this.onError.bind(this); - ws.onclose = this.onClose.bind(this); - }); - } - - /** - * Called whenever a connection is opened to the gateway. - * @private - */ - onOpen() { - this.debug(`[CONNECTED] Took ${Date.now() - this.connectedAt}ms`); - this.status = Status.Nearly; - } - - /** - * Called whenever a message is received. - * @param {MessageEvent} event Event received - * @private - */ - onMessage({ data }) { - let raw; - if (data instanceof ArrayBuffer) data = new Uint8Array(data); - if (zlib) { - const l = data.length; - const flush = - l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff; - - this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); - if (!flush) return; - raw = this.inflate.result; - } else { - raw = data; - } - let packet; - try { - packet = WebSocket.unpack(raw); - } catch (err) { - this.manager.client.emit(Events.ShardError, err, this.id); - return; - } - this.manager.client.emit(Events.Raw, packet, this.id); - if (packet.op === GatewayOpcodes.Dispatch) this.manager.emit(packet.t, packet.d, this.id); - this.onPacket(packet); - } - - /** - * Called whenever an error occurs with the WebSocket. - * @param {ErrorEvent} event The error that occurred - * @private - */ - onError(event) { - const error = event?.error ?? event; - if (!error) return; - - /** - * 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.manager.client.emit(Events.ShardError, error, this.id); - } - /** * @external CloseEvent * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} @@ -361,33 +86,11 @@ class WebSocketShard extends EventEmitter { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} */ - /** - * Called whenever a connection to the gateway is closed. - * @param {CloseEvent} event Close event that was received - * @private - */ - onClose(event) { - this.closeEmitted = true; - if (this.sequence !== -1) this.closeSequence = this.sequence; - this.sequence = -1; - this.setHeartbeatTimer(-1); - this.setHelloTimeout(-1); - // Clearing the WebSocket close timeout as close was emitted. - this.setWsCloseTimeout(-1); - // If we still have a connection object, clean up its listeners - if (this.connection) { - this._cleanupConnection(); - // Having this after _cleanupConnection to just clean up the connection and not listen to ws.onclose - this.destroy({ reset: !this.sessionId, emit: false, log: false }); - } - this.status = Status.Disconnected; - this.emitClose(event); - } - /** * 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 = { @@ -410,93 +113,35 @@ class WebSocketShard extends EventEmitter { } /** - * Called whenever a packet is received. + * Called when the shard receives the READY payload. * @param {Object} packet The received packet * @private */ - onPacket(packet) { + onReadyPacket(packet) { if (!packet) { this.debug(`Received broken packet: '${packet}'.`); return; } - switch (packet.t) { - case GatewayDispatchEvents.Ready: - /** - * Emitted when the shard receives the READY payload and is now waiting for guilds - * @event WebSocketShard#ready - */ - this.emit(WebSocketShardEvents.Ready); - - this.sessionId = packet.d.session_id; - this.resumeURL = packet.d.resume_gateway_url; - this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); - this.status = Status.WaitingForGuilds; - this.debug(`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`); - this.lastHeartbeatAcked = true; - this.sendHeartbeat('ReadyHeartbeat'); - break; - case GatewayDispatchEvents.Resumed: { - /** - * Emitted when the shard resumes successfully - * @event WebSocketShard#resumed - */ - this.emit(WebSocketShardEvents.Resumed); - - this.status = Status.Ready; - const replayed = packet.s - this.closeSequence; - this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`); - this.lastHeartbeatAcked = true; - this.sendHeartbeat('ResumeHeartbeat'); - break; - } - } + /** + * Emitted when the shard receives the READY payload and is now waiting for guilds + * @event WebSocketShard#ready + */ + this.emit(WebSocketShardEvents.Ready); - if (packet.s > this.sequence) this.sequence = packet.s; + this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); + this.status = Status.WaitingForGuilds; + } - switch (packet.op) { - case GatewayOpcodes.Hello: - this.setHelloTimeout(-1); - this.setHeartbeatTimer(packet.d.heartbeat_interval); - this.identify(); - break; - case GatewayOpcodes.Reconnect: - this.debug('[RECONNECT] Discord asked us to reconnect'); - this.destroy({ closeCode: 4_000 }); - break; - case GatewayOpcodes.InvalidSession: - this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); - // If we can resume the session, do so immediately - if (packet.d) { - this.identifyResume(); - return; - } - // Reset the sequence - this.sequence = -1; - // Reset the session id as it's invalid - this.sessionId = null; - // Set the status to reconnecting - this.status = Status.Reconnecting; - // Finally, emit the INVALID_SESSION event - /** - * Emitted when the session has been invalidated. - * @event WebSocketShard#invalidSession - */ - this.emit(WebSocketShardEvents.InvalidSession); - break; - case GatewayOpcodes.HeartbeatAck: - this.ackHeartbeat(); - break; - case GatewayOpcodes.Heartbeat: - this.sendHeartbeat('HeartbeatRequest', true); - break; - default: - this.manager.handlePacket(packet, this); - if (this.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) { - this.expectedGuilds.delete(packet.d.id); - this.checkReady(); - } - } + /** + * Called when a GuildCreate 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(); } /** @@ -552,190 +197,6 @@ class WebSocketShard extends EventEmitter { ).unref(); } - /** - * Sets the HELLO packet timeout. - * @param {number} [time] If set to -1, it will clear the hello timeout - * @private - */ - setHelloTimeout(time) { - if (time === -1) { - if (this.helloTimeout) { - this.debug('Clearing the HELLO timeout.'); - clearTimeout(this.helloTimeout); - this.helloTimeout = null; - } - return; - } - this.debug('Setting a HELLO timeout for 20s.'); - this.helloTimeout = setTimeout(() => { - this.debug('Did not receive HELLO in time. Destroying and connecting again.'); - this.destroy({ reset: true, closeCode: 4009 }); - }, 20_000).unref(); - } - - /** - * Sets the WebSocket Close timeout. - * This method is responsible for detecting any zombie connections if the WebSocket fails to close properly. - * @param {number} [time] If set to -1, it will clear the timeout - * @private - */ - setWsCloseTimeout(time) { - if (this.wsCloseTimeout) { - this.debug('[WebSocket] Clearing the close timeout.'); - clearTimeout(this.wsCloseTimeout); - } - if (time === -1) { - this.wsCloseTimeout = null; - return; - } - this.wsCloseTimeout = setTimeout(() => { - this.setWsCloseTimeout(-1); - this.debug(`[WebSocket] Close Emitted: ${this.closeEmitted}`); - // Check if close event was emitted. - if (this.closeEmitted) { - this.debug( - `[WebSocket] was closed. | WS State: ${CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED]}`, - ); - // Setting the variable false to check for zombie connections. - this.closeEmitted = false; - return; - } - - this.debug( - `[WebSocket] did not close properly, assuming a zombie connection.\nEmitting close and reconnecting again.`, - ); - - // Cleanup connection listeners - if (this.connection) this._cleanupConnection(); - - this.emitClose({ - code: 4009, - reason: 'Session time out.', - wasClean: false, - }); - }, time); - } - - /** - * Sets the heartbeat timer for this shard. - * @param {number} time If -1, clears the interval, any other number sets an interval - * @private - */ - setHeartbeatTimer(time) { - if (time === -1) { - if (this.heartbeatInterval) { - this.debug('Clearing the heartbeat interval.'); - clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } - return; - } - this.debug(`Setting a heartbeat interval for ${time}ms.`); - // Sanity checks - if (this.heartbeatInterval) clearInterval(this.heartbeatInterval); - this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), time).unref(); - } - - /** - * Sends a heartbeat to the WebSocket. - * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect - * @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent - * @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully. - * @private - */ - sendHeartbeat( - tag = 'HeartbeatTimer', - ignoreHeartbeatAck = [Status.WaitingForGuilds, Status.Identifying, Status.Resuming].includes(this.status), - ) { - if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) { - this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`); - } else if (!this.lastHeartbeatAcked) { - this.debug( - `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. - Status : ${STATUS_KEYS[this.status]} - Sequence : ${this.sequence} - Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`, - ); - - this.destroy({ reset: true, closeCode: 4009 }); - return; - } - - this.debug(`[${tag}] Sending a heartbeat.`); - this.lastHeartbeatAcked = false; - this.lastPingTimestamp = Date.now(); - this.send({ op: GatewayOpcodes.Heartbeat, d: this.sequence }, true); - } - - /** - * Acknowledges a heartbeat. - * @private - */ - ackHeartbeat() { - this.lastHeartbeatAcked = true; - const latency = Date.now() - this.lastPingTimestamp; - this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`); - this.ping = latency; - } - - /** - * Identifies the client on the connection. - * @private - * @returns {void} - */ - identify() { - return this.sessionId ? this.identifyResume() : this.identifyNew(); - } - - /** - * Identifies as a new connection on the gateway. - * @private - */ - identifyNew() { - const { client } = this.manager; - if (!client.token) { - this.debug('[IDENTIFY] No token available to identify a new session.'); - return; - } - - this.status = Status.Identifying; - - // Clone the identify payload and assign the token and shard info - const d = { - ...client.options.ws, - intents: client.options.intents.bitfield, - token: client.token, - shard: [this.id, Number(client.options.shardCount)], - }; - - this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: ${d.intents}`); - this.send({ op: GatewayOpcodes.Identify, d }, true); - } - - /** - * Resumes a session on the gateway. - * @private - */ - identifyResume() { - if (!this.sessionId) { - this.debug('[RESUME] No session id was present; identifying as a new session.'); - this.identifyNew(); - return; - } - - this.status = Status.Resuming; - - this.debug(`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`); - - const d = { - token: this.manager.client.token, - session_id: this.sessionId, - seq: this.closeSequence, - }; - - this.send({ op: GatewayOpcodes.Resume, d }, true); - } - /** * 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 @@ -743,161 +204,17 @@ class WebSocketShard extends EventEmitter { * 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) { - this.ratelimit.queue[important ? 'unshift' : 'push'](data); - this.processQueue(); - } - - /** - * Sends data, bypassing the queue. - * @param {Object} data Packet to send - * @returns {void} - * @private - */ - _send(data) { - if (this.connection?.readyState !== WebSocket.OPEN) { - this.debug( - `Tried to send packet '${JSON.stringify(data).replaceAll( - this.manager.client.token, - this.manager.client._censoredToken, - )}' but no WebSocket is available!`, + if (important) { + process.emitWarning( + 'Sending important payloads explicitly is deprecated. They are determined by their opcode implicitly now.', + 'DeprecationWarning', ); - this.destroy({ closeCode: 4_000 }); - return; - } - - this.connection.send(WebSocket.pack(data), err => { - if (err) this.manager.client.emit(Events.ShardError, err, this.id); - }); - } - - /** - * Processes the current WebSocket queue. - * @returns {void} - * @private - */ - processQueue() { - if (this.ratelimit.remaining === 0) return; - if (this.ratelimit.queue.length === 0) return; - if (this.ratelimit.remaining === this.ratelimit.total) { - this.ratelimit.timer = setTimeout(() => { - this.ratelimit.remaining = this.ratelimit.total; - this.processQueue(); - }, this.ratelimit.time).unref(); - } - while (this.ratelimit.remaining > 0) { - const item = this.ratelimit.queue.shift(); - if (!item) return; - this._send(item); - this.ratelimit.remaining--; - } - } - - /** - * Destroys this shard and closes its WebSocket connection. - * @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard - * @private - */ - destroy({ closeCode = 1_000, reset = false, emit = true, log = true } = {}) { - if (log) { - this.debug(`[DESTROY] - Close Code : ${closeCode} - Reset : ${reset} - Emit DESTROYED: ${emit}`); - } - - // Step 0: Remove all timers - this.setHeartbeatTimer(-1); - this.setHelloTimeout(-1); - - this.debug( - `[WebSocket] Destroy: Attempting to close the WebSocket. | WS State: ${ - CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED] - }`, - ); - // Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED - if (this.connection) { - // If the connection is currently opened, we will (hopefully) receive close - if (this.connection.readyState === WebSocket.OPEN) { - this.connection.close(closeCode); - this.debug(`[WebSocket] Close: Tried closing. | WS State: ${CONNECTION_STATE[this.connection.readyState]}`); - } else { - // Connection is not OPEN - this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`); - // Remove listeners from the connection - this._cleanupConnection(); - // Attempt to close the connection just in case - try { - this.connection.close(closeCode); - } catch (err) { - this.debug( - `[WebSocket] Close: Something went wrong while closing the WebSocket: ${ - err.message || err - }. Forcefully terminating the connection | WS State: ${CONNECTION_STATE[this.connection.readyState]}`, - ); - this.connection.terminate(); - } - - // Emit the destroyed event if needed - if (emit) this._emitDestroyed(); - } - } else if (emit) { - // We requested a destroy, but we had no connection. Emit destroyed - this._emitDestroyed(); - } - - this.debug( - `[WebSocket] Adding a WebSocket close timeout to ensure a correct WS reconnect. - Timeout: ${this.manager.client.options.closeTimeout}ms`, - ); - this.setWsCloseTimeout(this.manager.client.options.closeTimeout); - - // Step 2: Null the connection object - this.connection = null; - - // Step 3: Set the shard status to disconnected - this.status = Status.Disconnected; - - // Step 4: Cache the old sequence (use to attempt a resume) - if (this.sequence !== -1) this.closeSequence = this.sequence; - - // Step 5: Reset the sequence, resume url and session id if requested - if (reset) { - this.sequence = -1; - this.sessionId = null; - this.resumeURL = null; - } - - // Step 6: reset the rate limit data - this.ratelimit.remaining = this.ratelimit.total; - this.ratelimit.queue.length = 0; - if (this.ratelimit.timer) { - clearTimeout(this.ratelimit.timer); - this.ratelimit.timer = null; } - } - - /** - * Cleans up the WebSocket connection listeners. - * @private - */ - _cleanupConnection() { - this.connection.onopen = this.connection.onclose = this.connection.onmessage = null; - this.connection.onerror = () => null; - } - - /** - * Emits the DESTROYED event on the shard - * @private - */ - _emitDestroyed() { - /** - * Emitted when a shard is destroyed, but no WebSocket connection was present. - * @private - * @event WebSocketShard#destroyed - */ - this.emit(WebSocketShardEvents.Destroyed); + this.manager._ws.send(this.id, data); + this.processQueue(); } } diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 8d4eacc1d36a..421ee43bb15d 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3302,11 +3302,8 @@ export class WebhookClient extends WebhookMixin(BaseClient) { export class WebSocketManager extends EventEmitter { private constructor(client: Client); - private totalShards: number | string; - private shardQueue: Set; private readonly packetQueue: unknown[]; private destroyed: boolean; - private reconnecting: boolean; public readonly client: Client; public gateway: string | null; @@ -3319,8 +3316,6 @@ export class WebSocketManager extends EventEmitter { private debug(message: string, shard?: WebSocketShard): void; private connect(): Promise; - private createShards(): Promise; - private reconnect(): Promise; private broadcast(packet: unknown): void; private destroy(): void; private handlePacket(packet?: unknown, shard?: WebSocketShard): boolean; diff --git a/yarn.lock b/yarn.lock index 80771945ae94..d332fa5c978b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11301,6 +11301,7 @@ __metadata: "@discordjs/formatters": "workspace:^" "@discordjs/rest": "workspace:^" "@discordjs/util": "workspace:^" + "@discordjs/ws": "workspace:^" "@favware/cliff-jumper": ^2.0.0 "@sapphire/snowflake": ^3.4.2 "@types/node": 16.18.24 From 73cd71100ead2706daf024bbf800f7ca3dfab98a Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 30 Jan 2023 16:54:01 +0100 Subject: [PATCH 02/20] feat(WebSocketManager): fix bugs and deprecate --- packages/discord.js/src/WebSocket.js | 39 ------------------- .../src/client/websocket/WebSocketManager.js | 24 +++++++----- .../src/client/websocket/WebSocketShard.js | 3 +- packages/discord.js/src/errors/ErrorCodes.js | 7 ++++ packages/discord.js/src/index.js | 2 - packages/discord.js/typings/index.d.ts | 7 ++++ 6 files changed, 29 insertions(+), 53 deletions(-) delete mode 100644 packages/discord.js/src/WebSocket.js diff --git a/packages/discord.js/src/WebSocket.js b/packages/discord.js/src/WebSocket.js deleted file mode 100644 index efbd6f9a9587..000000000000 --- a/packages/discord.js/src/WebSocket.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -let erlpack; -const { Buffer } = require('node:buffer'); - -try { - erlpack = require('erlpack'); - if (!erlpack.pack) erlpack = null; -} catch {} // eslint-disable-line no-empty - -exports.WebSocket = require('ws'); - -const ab = new TextDecoder(); - -exports.encoding = erlpack ? 'etf' : 'json'; - -exports.pack = erlpack ? erlpack.pack : JSON.stringify; - -exports.unpack = (data, type) => { - if (exports.encoding === 'json' || type === 'json') { - if (typeof data !== 'string') { - data = ab.decode(data); - } - return JSON.parse(data); - } - if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data)); - return erlpack.unpack(data); -}; - -exports.create = (gateway, query = {}, ...args) => { - const [g, q] = gateway.split('?'); - query.encoding = exports.encoding; - query = new URLSearchParams(query); - if (q) new URLSearchParams(q).forEach((v, k) => query.set(k, v)); - const ws = new exports.WebSocket(`${g}?${query}`, ...args); - return ws; -}; - -for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) exports[state] = exports.WebSocket[state]; diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 81de47fccb71..d56c40045212 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -97,11 +97,15 @@ class WebSocketManager extends EventEmitter { * @private */ debug(message, shardId) { - this.client.emit(Events.Debug, `[WS => ${shardId ? `Shard ${shardId}` : 'Manager'}] ${message}`); + this.client.emit( + Events.Debug, + `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`, + ); } /** * Connects this manager to the gateway. + * @returns {Promise} * @private */ async connect() { @@ -145,8 +149,6 @@ class WebSocketManager extends EventEmitter { this.gateway = `${gatewayURL}/`; - await this._ws.connect(); - this.totalShards = this.client.options.shardCount = await this._ws.getShardCount(); this.client.options.shards = await this._ws.getShardIds(); for (const id of this.client.options.shards) { @@ -167,6 +169,8 @@ class WebSocketManager extends EventEmitter { }); } } + + return this._ws.connect(); } /** @@ -175,12 +179,12 @@ class WebSocketManager extends EventEmitter { */ attachEvents() { this._ws.on(WSWebSocketShardEvents.Debug, ({ message, shardId }) => this.debug(message, shardId)); - this._ws.on(WSWebSocketShardEvents.Dispatch, ({ packet, shardId }) => { - this.client.emit(Events.Raw, packet, shardId); + this._ws.on(WSWebSocketShardEvents.Dispatch, ({ data, shardId }) => { + this.client.emit(Events.Raw, data, shardId); const shard = this.shards.get(shardId); - this.handlePacket(packet, shard); - if (shard.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) { - shard.onGuildPacket(packet); + this.handlePacket(data, shard); + if (shard.status === Status.WaitingForGuilds && data.t === GatewayDispatchEvents.GuildCreate) { + shard.gotGuild(data.d.id); } }); @@ -225,14 +229,14 @@ class WebSocketManager extends EventEmitter { }); // TODO: refactor once error event gets exposed publicly - this._ws.on('error', ({ err, shardId }) => { + this._ws.on('error', err => { /** * 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, err, shardId); + this.client.emit(Events.ShardError, err, err.shardId); }); } diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index 7ffee867a431..00bfef6bbcc7 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -129,7 +129,7 @@ class WebSocketShard extends EventEmitter { */ this.emit(WebSocketShardEvents.Ready); - this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); + this.expectedGuilds = new Set(packet.guilds.map(d => d.id)); this.status = Status.WaitingForGuilds; } @@ -214,7 +214,6 @@ class WebSocketShard extends EventEmitter { ); } this.manager._ws.send(this.id, data); - this.processQueue(); } } diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index 9b36fe2efbd2..9cd2f4dab36c 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -13,16 +13,23 @@ * @property {'ApplicationCommandPermissionsTokenMissing'} ApplicationCommandPermissionsTokenMissing * @property {'WSCloseRequested'} WSCloseRequested + * This property is deprecated. * @property {'WSConnectionExists'} WSConnectionExists + * This property is deprecated. * @property {'WSNotOpen'} WSNotOpen + * This property is deprecated. * @property {'ManagerDestroyed'} ManagerDestroyed * @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 diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index d5a585f67468..d4ee0d402aba 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -204,8 +204,6 @@ exports.WidgetMember = require('./structures/WidgetMember'); exports.WelcomeChannel = require('./structures/WelcomeChannel'); exports.WelcomeScreen = require('./structures/WelcomeScreen'); -exports.WebSocket = require('./WebSocket'); - // External __exportStar(require('discord-api-types/v10'), exports); __exportStar(require('@discordjs/builders'), exports); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 421ee43bb15d..c025db664da5 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3504,16 +3504,23 @@ export enum DiscordjsErrorCodes { TokenMissing = 'TokenMissing', ApplicationCommandPermissionsTokenMissing = 'ApplicationCommandPermissionsTokenMissing', + /** @deprecated */ WSCloseRequested = 'WSCloseRequested', + /** @deprecated */ WSConnectionExists = 'WSConnectionExists', + /** @deprecated */ WSNotOpen = 'WSNotOpen', ManagerDestroyed = 'ManagerDestroyed', BitFieldInvalid = 'BitFieldInvalid', + /** @deprecated */ ShardingInvalid = 'ShardingInvalid', + /** @deprecated */ ShardingRequired = 'ShardingRequired', + /** @deprecated */ InvalidIntents = 'InvalidIntents', + /** @deprecated */ DisallowedIntents = 'DisallowedIntents', ShardingNoShards = 'ShardingNoShards', ShardingInProcess = 'ShardingInProcess', From c59de9adf9013ca6501f9e4244bcb9f09404703d Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 30 Jan 2023 18:02:58 +0100 Subject: [PATCH 03/20] feat(WebSocketManager): refactor status properties --- .../src/client/websocket/WebSocketManager.js | 1 - .../src/client/websocket/WebSocketShard.js | 42 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index d56c40045212..89a03693930f 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -193,7 +193,6 @@ class WebSocketManager extends EventEmitter { }); this._ws.on(WSWebSocketShardEvents.Closed, ({ code, reason = '', shardId }) => { - this.shards.get(shardId).status = code === CloseCodes.Resuming ? Status.Resuming : Status.Disconnected; if (code === CloseCodes.Normal && this.destroyed) { /** * Emitted when a shard's WebSocket disconnects and will no longer reconnect. diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index 00bfef6bbcc7..8772005c1bf4 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -3,6 +3,7 @@ const EventEmitter = require('node:events'); const process = require('node:process'); const { setTimeout, clearTimeout } = require('node:timers'); +const { WebSocketShardStatus } = require('@discordjs/ws'); const { GatewayIntentBits } = require('discord-api-types/v10'); const Status = require('../../util/Status'); const WebSocketShardEvents = require('../../util/WebSocketShardEvents'); @@ -27,12 +28,6 @@ class WebSocketShard extends EventEmitter { */ this.id = id; - /** - * The current status of the shard - * @type {Status} - */ - this.status = Status.Idle; - /** * The previous heartbeat ping of the shard * @type {number} @@ -62,6 +57,27 @@ class WebSocketShard extends EventEmitter { Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); } + /** + * The current status of the shard + * @type {Status} + */ + get status() { + if (this.readyTimeout) return Status.WaitingForGuilds; + const status = this.manager._ws.fetchStatus().get(this.id); + switch (status) { + case WebSocketShardStatus.Idle: + return Status.Idle; + case WebSocketShardStatus.Connecting: + return Status.Connecting; + case WebSocketShardStatus.Ready: + return Status.Ready; + case WebSocketShardStatus.Resuming: + return Status.Resuming; + default: + return Status.Idle; + } + } + /** * Emits a debug event. * @param {string} message The debug message @@ -76,16 +92,6 @@ class WebSocketShard extends EventEmitter { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} */ - /** - * @external ErrorEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent} - */ - - /** - * @external MessageEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} - */ - /** * This method is responsible to emit close event for this shard. * This method helps the shard reconnect. @@ -130,7 +136,6 @@ class WebSocketShard extends EventEmitter { this.emit(WebSocketShardEvents.Ready); this.expectedGuilds = new Set(packet.guilds.map(d => d.id)); - this.status = Status.WaitingForGuilds; } /** @@ -157,7 +162,6 @@ class WebSocketShard extends EventEmitter { // 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. @@ -189,8 +193,6 @@ class WebSocketShard extends EventEmitter { this.readyTimeout = null; - this.status = Status.Ready; - this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds); }, hasGuildsIntent ? waitGuildTimeout : 0, From 3144f5087211b2a93474b6a296661fb5a6259ba2 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 30 Jan 2023 21:27:31 +0100 Subject: [PATCH 04/20] feat(WebSocketManager): remove erlpack, fix status --- packages/discord.js/README.md | 1 - .../src/client/websocket/WebSocketManager.js | 7 ++++- .../src/client/websocket/WebSocketShard.js | 29 +++++++++---------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/discord.js/README.md b/packages/discord.js/README.md index 885440ff9a31..26b3f0985d57 100644 --- a/packages/discord.js/README.md +++ b/packages/discord.js/README.md @@ -39,7 +39,6 @@ pnpm add discord.js ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) -- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/erlpack`) - [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`) - [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`) - [@discordjs/voice](https://www.npmjs.com/package/@discordjs/voice) for interacting with the Discord Voice API (`npm install @discordjs/voice`) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 89a03693930f..edd6366c2eb5 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -167,6 +167,7 @@ class WebSocketManager extends EventEmitter { this.checkShardsReady(); }); + shard.status = Status.Connecting; } } @@ -194,6 +195,7 @@ class WebSocketManager extends EventEmitter { this._ws.on(WSWebSocketShardEvents.Closed, ({ code, reason = '', shardId }) => { if (code === CloseCodes.Normal && this.destroyed) { + this.shards.get(shardId).status = Status.Disconnected; /** * Emitted when a shard's WebSocket disconnects and will no longer reconnect. * @event Client#shardDisconnect @@ -205,6 +207,7 @@ class WebSocketManager extends EventEmitter { return; } + this.shards.get(shardId).status = code === CloseCodes.Resuming ? Status.Resuming : Status.Reconnecting; /** * Emitted when a shard is attempting to reconnect or re-identify. * @event Client#shardReconnecting @@ -214,11 +217,13 @@ class WebSocketManager extends EventEmitter { }); this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => { + const shard = this.shards.get(shardId); + shard.setStatus(); /** * Emitted when the shard resumes successfully * @event WebSocketShard#resumed */ - this.shards.get(shardId).emit(WebSocketShardEvents.Resumed); + shard.emit(WebSocketShardEvents.Resumed); }); this._ws.on(WSWebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency, shardId }) => { diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index 8772005c1bf4..44c502573e1c 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -28,6 +28,12 @@ class WebSocketShard extends EventEmitter { */ this.id = id; + /** + * The current status of the shard + * @type {Status} + */ + this.status = Status.Idle; + /** * The previous heartbeat ping of the shard * @type {number} @@ -58,23 +64,14 @@ class WebSocketShard extends EventEmitter { } /** - * The current status of the shard - * @type {Status} + * Syncronizes the status property with the `@discordjs/ws` implementation. */ - get status() { - if (this.readyTimeout) return Status.WaitingForGuilds; - const status = this.manager._ws.fetchStatus().get(this.id); - switch (status) { - case WebSocketShardStatus.Idle: - return Status.Idle; - case WebSocketShardStatus.Connecting: - return Status.Connecting; - case WebSocketShardStatus.Ready: - return Status.Ready; - case WebSocketShardStatus.Resuming: - return Status.Resuming; - default: - return Status.Idle; + async setStatus() { + if (this.readyTimeout) { + this.status = Status.WaitingForGuilds; + } else { + const status = (await this.manager._ws.fetchStatus()).get(this.id); + this.status = Status[WebSocketShardStatus[status]]; } } From 311170512711bdcec83fc85908534f45c36b5764 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 30 Jan 2023 22:08:45 +0100 Subject: [PATCH 05/20] feat(WebSocketManager): fix status again --- .../discord.js/src/client/websocket/WebSocketManager.js | 5 +++-- .../discord.js/src/client/websocket/WebSocketShard.js | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index edd6366c2eb5..30c5a2cd3f05 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -105,7 +105,6 @@ class WebSocketManager extends EventEmitter { /** * Connects this manager to the gateway. - * @returns {Promise} * @private */ async connect() { @@ -171,7 +170,8 @@ class WebSocketManager extends EventEmitter { } } - return this._ws.connect(); + await this._ws.connect(); + await Promise.all(this.shards.map(shard => shard.setStatus())); } /** @@ -227,6 +227,7 @@ class WebSocketManager extends EventEmitter { }); 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; diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index 44c502573e1c..60ef20d56383 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -81,7 +81,7 @@ class WebSocketShard extends EventEmitter { * @private */ debug(message) { - this.manager.debug(message, this); + this.manager.debug(message, this.id); } /** @@ -150,7 +150,7 @@ class WebSocketShard extends EventEmitter { * Checks if the shard can be marked as ready * @private */ - checkReady() { + async checkReady() { // Step 0. Clear the ready timeout, if it exists if (this.readyTimeout) { clearTimeout(this.readyTimeout); @@ -159,6 +159,7 @@ class WebSocketShard extends EventEmitter { // 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.'); + await this.setStatus(); /** * Emitted when the shard is fully ready. @@ -180,7 +181,7 @@ class WebSocketShard extends EventEmitter { const { waitGuildTimeout } = this.manager.client.options; this.readyTimeout = setTimeout( - () => { + async () => { this.debug( `Shard ${hasGuildsIntent ? 'did' : 'will'} not receive any more guild packets` + `${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${ @@ -189,6 +190,7 @@ class WebSocketShard extends EventEmitter { ); this.readyTimeout = null; + await this.setStatus(); this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds); }, From c3760170805c59a509ee973f2eac761cf81bbdac Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:10:24 +0100 Subject: [PATCH 06/20] chore(WebSocketShard): fix status race condition --- packages/discord.js/src/client/websocket/WebSocketShard.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index 60ef20d56383..c1337a6107bb 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -196,6 +196,7 @@ class WebSocketShard extends EventEmitter { }, hasGuildsIntent ? waitGuildTimeout : 0, ).unref(); + await this.setStatus(); } /** From 824353b43d89c97c6d8d2fb5e3792e81aa074aba Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Wed, 1 Feb 2023 18:51:28 +0100 Subject: [PATCH 07/20] chore(WebSocketManager): fix status checks and shardResume event --- .../src/client/actions/GuildMemberRemove.js | 14 ++++---- .../src/client/actions/GuildMemberUpdate.js | 16 +++++---- .../src/client/websocket/WebSocketManager.js | 23 +++++++++---- .../src/client/websocket/WebSocketShard.js | 34 ++++++++++++++++--- .../websocket/handlers/GUILD_MEMBER_ADD.js | 18 +++++----- .../src/client/websocket/handlers/RESUMED.js | 2 +- 6 files changed, 74 insertions(+), 33 deletions(-) diff --git a/packages/discord.js/src/client/actions/GuildMemberRemove.js b/packages/discord.js/src/client/actions/GuildMemberRemove.js index 45eb6c41931f..e0c825719899 100644 --- a/packages/discord.js/src/client/actions/GuildMemberRemove.js +++ b/packages/discord.js/src/client/actions/GuildMemberRemove.js @@ -14,12 +14,14 @@ class GuildMemberRemoveAction extends Action { guild.memberCount--; if (member) { guild.members.cache.delete(member.id); - /** - * Emitted whenever a member leaves a guild, or is kicked. - * @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); + shard.getStatus().then(status => { + /** + * Emitted whenever a member leaves a guild, or is kicked. + * @event Client#guildMemberRemove + * @param {GuildMember} member The member that has left/been kicked from the guild + */ + if (status === Status.Ready) 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 491b36181e0a..30ddfbf84676 100644 --- a/packages/discord.js/src/client/actions/GuildMemberUpdate.js +++ b/packages/discord.js/src/client/actions/GuildMemberUpdate.js @@ -21,13 +21,15 @@ class GuildMemberUpdateAction extends Action { const member = this.getMember({ user: data.user }, guild); if (member) { const old = member._update(data); - /** - * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. - * @event Client#guildMemberUpdate - * @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); + shard.getStatus().then(status => { + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. + * @event Client#guildMemberUpdate + * @param {GuildMember} oldMember The member before the update + * @param {GuildMember} newMember The member after the update + */ + if (status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); + }); } else { const newMember = guild.members._add(data); /** diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 30c5a2cd3f05..42592c5a64b0 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -124,6 +124,10 @@ class WebSocketManager extends EventEmitter { 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; + }, }); this.attachEvents(); } @@ -171,7 +175,7 @@ class WebSocketManager extends EventEmitter { } await this._ws.connect(); - await Promise.all(this.shards.map(shard => shard.setStatus())); + await Promise.all(this.shards.map(shard => shard.getStatus())); } /** @@ -207,7 +211,7 @@ class WebSocketManager extends EventEmitter { return; } - this.shards.get(shardId).status = code === CloseCodes.Resuming ? Status.Resuming : Status.Reconnecting; + this.shards.get(shardId).status = Status.Connecting; /** * Emitted when a shard is attempting to reconnect or re-identify. * @event Client#shardReconnecting @@ -215,10 +219,15 @@ class WebSocketManager extends EventEmitter { */ 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.getStatus(); + }); - this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => { + this._ws.on(WSWebSocketShardEvents.Resumed, async ({ shardId }) => { const shard = this.shards.get(shardId); - shard.setStatus(); + await shard.getStatus(); /** * Emitted when the shard resumes successfully * @event WebSocketShard#resumed @@ -301,11 +310,13 @@ class WebSocketManager extends EventEmitter { */ checkShardsReady() { if (this.status === Status.Ready) return; - if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.Ready)) { + if (this.shards.size !== this.totalShards) { return; } - this.triggerClientReady(); + Promise.all(this.shards.map(shard => shard.getStatus())).then(statuses => { + if (statuses.every(status => status === Status.Ready)) this.triggerClientReady(); + }); } /** diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index c1337a6107bb..a1c7a47ec051 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -34,6 +34,13 @@ class WebSocketShard extends EventEmitter { */ 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} @@ -61,18 +68,35 @@ class WebSocketShard extends EventEmitter { * @private */ Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); + + /** + * @external SessionInfo + * @see {@link https://discord.js.org/#/docs/ws/main/typedef/SessionInfo} + */ + + /** + * The session info used by `@discordjs/ws` package. + * @name WebSocketShard#sessionInfo + * @type {?SessionInfo} + * @private + */ + Object.defineProperty(this, 'sessionInfo', { value: null, writable: true }); } /** - * Syncronizes the status property with the `@discordjs/ws` implementation. + * Syncronizes the {@link WebSocketShard#status} property with the `@discordjs/ws` implementation + * and returns the new value. + * @returns {Promise} + * @private */ - async setStatus() { + async getStatus() { if (this.readyTimeout) { this.status = Status.WaitingForGuilds; } else { const status = (await this.manager._ws.fetchStatus()).get(this.id); this.status = Status[WebSocketShardStatus[status]]; } + return this.status; } /** @@ -159,7 +183,7 @@ class WebSocketShard extends EventEmitter { // 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.'); - await this.setStatus(); + await this.getStatus(); /** * Emitted when the shard is fully ready. @@ -190,13 +214,13 @@ class WebSocketShard extends EventEmitter { ); this.readyTimeout = null; - await this.setStatus(); + await this.getStatus(); this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds); }, hasGuildsIntent ? waitGuildTimeout : 0, ).unref(); - await this.setStatus(); + await this.getStatus(); } /** 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 fece5d76f456..4930c6996a5f 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 @@ -8,13 +8,15 @@ module.exports = (client, { d: data }, shard) => { 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); - } + shard.getStatus().then(status => { + if (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); + } + }); } }; diff --git a/packages/discord.js/src/client/websocket/handlers/RESUMED.js b/packages/discord.js/src/client/websocket/handlers/RESUMED.js index 39824bc9242d..27ed7ddc5df3 100644 --- a/packages/discord.js/src/client/websocket/handlers/RESUMED.js +++ b/packages/discord.js/src/client/websocket/handlers/RESUMED.js @@ -3,7 +3,7 @@ const Events = require('../../../util/Events'); module.exports = (client, packet, shard) => { - const replayed = shard.sequence - shard.closeSequence; + const replayed = shard.sessionInfo.sequence - shard.closeSequence; /** * Emitted when a shard resumes successfully. * @event Client#shardResume From a4c0abf2d1868db1c0cfbe80f3c1f7a1bc955f15 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Thu, 2 Feb 2023 13:02:06 +0100 Subject: [PATCH 08/20] fix(WebsocketShard): don't wait for Guilds we got a GuildDelete for --- packages/discord.js/src/client/websocket/WebSocketManager.js | 4 +++- packages/discord.js/src/client/websocket/WebSocketShard.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 42592c5a64b0..c0694f3448d8 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -26,6 +26,8 @@ const BeforeReadyWhitelist = [ GatewayDispatchEvents.GuildMemberRemove, ]; +const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; + /** * The WebSocket manager for this client. * This class forwards raw dispatch events, @@ -188,7 +190,7 @@ class WebSocketManager extends EventEmitter { this.client.emit(Events.Raw, data, shardId); const shard = this.shards.get(shardId); this.handlePacket(data, shard); - if (shard.status === Status.WaitingForGuilds && data.t === GatewayDispatchEvents.GuildCreate) { + if (shard.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(data.t)) { shard.gotGuild(data.d.id); } }); diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index a1c7a47ec051..ecce2e4ef29c 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -160,7 +160,7 @@ class WebSocketShard extends EventEmitter { } /** - * Called when a GuildCreate for this shard was sent after READY payload was received, + * 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 From 3375d5c90fbe13594f7ecf4a488a8e93c830a345 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 6 Feb 2023 09:34:55 +0100 Subject: [PATCH 09/20] fix(WebSocketShard): revert some status property changes --- .../src/client/actions/GuildMemberRemove.js | 14 +++++----- .../src/client/actions/GuildMemberUpdate.js | 16 +++++------ .../src/client/websocket/WebSocketManager.js | 27 ++++++++++++------- .../src/client/websocket/WebSocketShard.js | 27 ++++--------------- .../websocket/handlers/GUILD_MEMBER_ADD.js | 18 ++++++------- 5 files changed, 43 insertions(+), 59 deletions(-) diff --git a/packages/discord.js/src/client/actions/GuildMemberRemove.js b/packages/discord.js/src/client/actions/GuildMemberRemove.js index e0c825719899..45eb6c41931f 100644 --- a/packages/discord.js/src/client/actions/GuildMemberRemove.js +++ b/packages/discord.js/src/client/actions/GuildMemberRemove.js @@ -14,14 +14,12 @@ class GuildMemberRemoveAction extends Action { guild.memberCount--; if (member) { guild.members.cache.delete(member.id); - shard.getStatus().then(status => { - /** - * Emitted whenever a member leaves a guild, or is kicked. - * @event Client#guildMemberRemove - * @param {GuildMember} member The member that has left/been kicked from the guild - */ - if (status === Status.Ready) client.emit(Events.GuildMemberRemove, member); - }); + /** + * Emitted whenever a member leaves a guild, or is kicked. + * @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); } 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 30ddfbf84676..491b36181e0a 100644 --- a/packages/discord.js/src/client/actions/GuildMemberUpdate.js +++ b/packages/discord.js/src/client/actions/GuildMemberUpdate.js @@ -21,15 +21,13 @@ class GuildMemberUpdateAction extends Action { const member = this.getMember({ user: data.user }, guild); if (member) { const old = member._update(data); - shard.getStatus().then(status => { - /** - * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. - * @event Client#guildMemberUpdate - * @param {GuildMember} oldMember The member before the update - * @param {GuildMember} newMember The member after the update - */ - if (status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); - }); + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. + * @event Client#guildMemberUpdate + * @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); } else { const newMember = guild.members._add(data); /** diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index c0694f3448d8..3ffc50f2504f 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -28,6 +28,12 @@ const BeforeReadyWhitelist = [ const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; +const UNRESUMABLE_CLOSE_CODES = [ + CloseCodes.Normal, + GatewayCloseCodes.AlreadyAuthenticated, + GatewayCloseCodes.InvalidSeq, +]; + /** * The WebSocket manager for this client. * This class forwards raw dispatch events, @@ -177,7 +183,6 @@ class WebSocketManager extends EventEmitter { } await this._ws.connect(); - await Promise.all(this.shards.map(shard => shard.getStatus())); } /** @@ -200,7 +205,7 @@ class WebSocketManager extends EventEmitter { }); this._ws.on(WSWebSocketShardEvents.Closed, ({ code, reason = '', shardId }) => { - if (code === CloseCodes.Normal && this.destroyed) { + if (UNRESUMABLE_CLOSE_CODES.includes(code) && this.destroyed) { this.shards.get(shardId).status = Status.Disconnected; /** * Emitted when a shard's WebSocket disconnects and will no longer reconnect. @@ -223,13 +228,17 @@ class WebSocketManager extends EventEmitter { }); this._ws.on(WSWebSocketShardEvents.Hello, ({ shardId }) => { const shard = this.shards.get(shardId); - if (shard.sessionInfo) shard.closeSequence = shard.sessionInfo.sequence; - shard.getStatus(); + if (shard.sessionInfo) { + shard.closeSequence = shard.sessionInfo.sequence; + shard.status = Status.Resuming; + } else { + shard.status = Status.Identifying; + } }); - this._ws.on(WSWebSocketShardEvents.Resumed, async ({ shardId }) => { + this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => { const shard = this.shards.get(shardId); - await shard.getStatus(); + shard.status = Status.Ready; /** * Emitted when the shard resumes successfully * @event WebSocketShard#resumed @@ -312,13 +321,11 @@ class WebSocketManager extends EventEmitter { */ checkShardsReady() { if (this.status === Status.Ready) return; - if (this.shards.size !== this.totalShards) { + if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.Ready)) { return; } - Promise.all(this.shards.map(shard => shard.getStatus())).then(statuses => { - if (statuses.every(status => status === Status.Ready)) this.triggerClientReady(); - }); + this.triggerClientReady(); } /** diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index ecce2e4ef29c..76319def7e82 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -3,7 +3,6 @@ const EventEmitter = require('node:events'); const process = require('node:process'); const { setTimeout, clearTimeout } = require('node:timers'); -const { WebSocketShardStatus } = require('@discordjs/ws'); const { GatewayIntentBits } = require('discord-api-types/v10'); const Status = require('../../util/Status'); const WebSocketShardEvents = require('../../util/WebSocketShardEvents'); @@ -83,22 +82,6 @@ class WebSocketShard extends EventEmitter { Object.defineProperty(this, 'sessionInfo', { value: null, writable: true }); } - /** - * Syncronizes the {@link WebSocketShard#status} property with the `@discordjs/ws` implementation - * and returns the new value. - * @returns {Promise} - * @private - */ - async getStatus() { - if (this.readyTimeout) { - this.status = Status.WaitingForGuilds; - } else { - const status = (await this.manager._ws.fetchStatus()).get(this.id); - this.status = Status[WebSocketShardStatus[status]]; - } - return this.status; - } - /** * Emits a debug event. * @param {string} message The debug message @@ -157,6 +140,7 @@ class WebSocketShard extends EventEmitter { this.emit(WebSocketShardEvents.Ready); this.expectedGuilds = new Set(packet.guilds.map(d => d.id)); + this.status = Status.WaitingForGuilds; } /** @@ -174,7 +158,7 @@ class WebSocketShard extends EventEmitter { * Checks if the shard can be marked as ready * @private */ - async checkReady() { + checkReady() { // Step 0. Clear the ready timeout, if it exists if (this.readyTimeout) { clearTimeout(this.readyTimeout); @@ -183,7 +167,7 @@ class WebSocketShard extends EventEmitter { // 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.'); - await this.getStatus(); + this.status = Status.Ready; /** * Emitted when the shard is fully ready. @@ -205,7 +189,7 @@ class WebSocketShard extends EventEmitter { const { waitGuildTimeout } = this.manager.client.options; this.readyTimeout = setTimeout( - async () => { + () => { this.debug( `Shard ${hasGuildsIntent ? 'did' : 'will'} not receive any more guild packets` + `${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${ @@ -214,13 +198,12 @@ class WebSocketShard extends EventEmitter { ); this.readyTimeout = null; - await this.getStatus(); + this.status = Status.Ready; this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds); }, hasGuildsIntent ? waitGuildTimeout : 0, ).unref(); - await this.getStatus(); } /** 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 4930c6996a5f..fece5d76f456 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 @@ -8,15 +8,13 @@ module.exports = (client, { d: data }, shard) => { if (guild) { guild.memberCount++; const member = guild.members._add(data); - shard.getStatus().then(status => { - if (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); - } - }); + 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); + } } }; From 367f21e0bff48cecdaf3b78ecbcbbb4dbb7fc2e2 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 3 Mar 2023 17:22:08 +0100 Subject: [PATCH 10/20] refactor(WebSocketManager): allow passing strategy --- .../src/client/websocket/WebSocketManager.js | 13 +++++++------ packages/discord.js/src/util/Options.js | 12 ++++++++++++ packages/discord.js/typings/index.d.ts | 2 ++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 3ffc50f2504f..dc67d476d17c 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -119,11 +119,11 @@ class WebSocketManager extends EventEmitter { 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: 1000, reason: 'Login with differing token requested' }); + await this._ws.destroy({ code: CloseCodes.Normal, reason: 'Login with differing token requested' }); this._ws = null; } if (!this._ws) { - this._ws = new WSWebSocketManager({ + const wsOptions = { intents: intents.bitfield, rest: this.client.rest, token: this.client.token, @@ -136,7 +136,9 @@ class WebSocketManager extends EventEmitter { updateSessionInfo: (shardId, sessionInfo) => { this.shards.get(shardId).sessionInfo = sessionInfo; }, - }); + }; + if (ws.buildStrategy) wsOptions.buildStrategy = ws.buildStrategy; + this._ws = new WSWebSocketManager(wsOptions); this.attachEvents(); } @@ -253,8 +255,7 @@ class WebSocketManager extends EventEmitter { shard.ping = latency; }); - // TODO: refactor once error event gets exposed publicly - this._ws.on('error', err => { + this._ws.on(WSWebSocketShardEvents.Error, err => { /** * Emitted whenever a shard's WebSocket encounters a connection error. * @event Client#shardError @@ -283,7 +284,7 @@ class WebSocketManager extends EventEmitter { // TODO: Make a util for getting a stack this.debug(`Manager was destroyed. Called by:\n${new Error().stack}`); this.destroyed = true; - this._ws.destroy({ code: 1_000 }); + this._ws.destroy({ code: CloseCodes.Normal }); } /** diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index a0669a720144..863e1d15c615 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -65,6 +65,8 @@ const { version } = require('../../package.json'); * 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 {Function} [buildStrategy] Builds the strategy to use for sharding; + * example: `(manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 })` */ /** @@ -200,3 +202,13 @@ module.exports = Options; * @external RESTOptions * @see {@link https://discord.js.org/docs/packages/rest/stable/RESTOptions:Interface} */ + +/** + * @external WSWebSocketManager + * @see {@link https://discord.js.org/#/docs/ws/main/class/WebSocketManager} + */ + +/** + * @external IShardingStrategy + * @see {@link https://discord.js.org/#/docs/ws/main/typedef/IShardingStrategy} + */ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index c025db664da5..18ad06442e22 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -40,6 +40,7 @@ import { import { Awaitable, JSONEncodable } from '@discordjs/util'; import { Collection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; +import { WebSocketManager as WSWebSocketManager, IShardingStrategy } from '@discordjs/ws'; import { APIActionRowComponent, APIApplicationCommandInteractionData, @@ -6337,6 +6338,7 @@ export interface WebSocketOptions { compress?: boolean; properties?: WebSocketProperties; version?: number; + buildStrategy?(manager: WSWebSocketManager): IShardingStrategy; } export interface WebSocketProperties { From bfdab229e5b87b83a6b6cb2e3a2e13e9ae4e422d Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 3 Mar 2023 17:39:33 +0100 Subject: [PATCH 11/20] refactor: re-export all of /ws and jsdoc fix --- packages/discord.js/src/index.js | 1 + packages/discord.js/src/util/Options.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index d4ee0d402aba..c7c530ced537 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -210,3 +210,4 @@ __exportStar(require('@discordjs/builders'), exports); __exportStar(require('@discordjs/formatters'), exports); __exportStar(require('@discordjs/rest'), exports); __exportStar(require('@discordjs/util'), exports); +__exportStar(require('@discordjs/ws'), exports); diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 863e1d15c615..4f9be9c2d0e9 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -65,7 +65,7 @@ const { version } = require('../../package.json'); * 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 {Function} [buildStrategy] Builds the strategy to use for sharding; + * @property {Function} [buildStrategy] Builds the strategy to use for sharding; * example: `(manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 })` */ From a8b803077b4121ad0adf0331bbc9fd29acd3b26b Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Fri, 3 Mar 2023 20:41:11 +0100 Subject: [PATCH 12/20] fix: typings and JSDoc --- .../src/client/websocket/WebSocketManager.js | 10 +++++ packages/discord.js/typings/index.d.ts | 43 +++---------------- 2 files changed, 15 insertions(+), 38 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index dc67d476d17c..56e86dd20c2f 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -85,6 +85,16 @@ class WebSocketManager extends EventEmitter { */ this.destroyed = false; + /** + * @external WSWebSocketManager + * @see {@link https://discord.js.org/#/docs/ws/main/class/WebSocketManager} + */ + + /** + * The internal WebSocketManager from `@discordjs/ws`. + * @type {WSWebSocketManager} + * @private + */ this._ws = null; } diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 18ad06442e22..7e9308e3bb1a 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -40,7 +40,7 @@ import { import { Awaitable, JSONEncodable } from '@discordjs/util'; import { Collection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; -import { WebSocketManager as WSWebSocketManager, IShardingStrategy } from '@discordjs/ws'; +import { WebSocketManager as WSWebSocketManager, IShardingStrategy, SessionInfo } from '@discordjs/ws'; import { APIActionRowComponent, APIApplicationCommandInteractionData, @@ -3315,7 +3315,7 @@ export class WebSocketManager extends EventEmitter { public on(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this; public once(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this; - private debug(message: string, shard?: WebSocketShard): void; + private debug(message: string, shardId?: number): void; private connect(): Promise; private broadcast(packet: unknown): void; private destroy(): void; @@ -3335,26 +3335,11 @@ export interface WebSocketShardEventTypes { export class WebSocketShard extends EventEmitter { private constructor(manager: WebSocketManager, id: number); - private sequence: number; private closeSequence: number; - private sessionId: string | null; - private resumeURL: string | null; + private sessionInfo: SessionInfo | null; public lastPingTimestamp: number; - private lastHeartbeatAcked: boolean; - private readonly ratelimit: { - queue: unknown[]; - total: number; - remaining: number; - time: 60e3; - timer: NodeJS.Timeout | null; - }; - private connection: WebSocket | null; - private helloTimeout: NodeJS.Timeout | null; - private eventsAttached: boolean; private expectedGuilds: Set | null; private readyTimeout: NodeJS.Timeout | null; - private closeEmitted: boolean; - private wsCloseTimeout: NodeJS.Timeout | null; public manager: WebSocketManager; public id: number; @@ -3362,27 +3347,10 @@ export class WebSocketShard extends EventEmitter { public ping: number; private debug(message: string): void; - private connect(): Promise; - private onOpen(): void; - private onMessage(event: MessageEvent): void; - private onError(error: ErrorEvent | unknown): void; - private onClose(event: CloseEvent): void; - private onPacket(packet: unknown): void; + private onReadyPacket(packet: unknown): void; + private gotGuild(guildId: Snowflake): void; private checkReady(): void; - private setHelloTimeout(time?: number): void; - private setWsCloseTimeout(time?: number): void; - private setHeartbeatTimer(time: number): void; - private sendHeartbeat(): void; - private ackHeartbeat(): void; - private identify(): void; - private identifyNew(): void; - private identifyResume(): void; - private _send(data: unknown): void; - private processQueue(): void; - private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean }): void; private emitClose(event?: CloseEvent): void; - private _cleanupConnection(): void; - private _emitDestroyed(): void; public send(data: unknown, important?: boolean): void; @@ -4880,7 +4848,6 @@ export interface CloseEvent { wasClean: boolean; code: number; reason: string; - target: WebSocket; } export type CollectorFilter = (...args: T) => boolean | Promise; From 46ed6c11de8e798c4984bd73176bad705af42233 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 6 Mar 2023 10:19:27 +0100 Subject: [PATCH 13/20] refactor(WebSocketManager): deprecate events --- .../src/client/websocket/WebSocketManager.js | 32 +++++++++++++++++-- packages/discord.js/typings/index.d.ts | 2 ++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 56e86dd20c2f..8a1084b316f1 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -1,6 +1,7 @@ 'use strict'; const EventEmitter = require('node:events'); +const process = require('node:process'); const { setImmediate } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { @@ -34,6 +35,10 @@ const UNRESUMABLE_CLOSE_CODES = [ 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, @@ -195,6 +200,25 @@ class WebSocketManager extends EventEmitter { } 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; + } + }); } /** @@ -216,16 +240,18 @@ class WebSocketManager extends EventEmitter { this.shards.get(shardId).onReadyPacket(data); }); - this._ws.on(WSWebSocketShardEvents.Closed, ({ code, reason = '', shardId }) => { + 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) { - this.shards.get(shardId).status = Status.Disconnected; + 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, wasClean: true }, shardId); + this.client.emit(Events.ShardDisconnect, { code, reason: reasonIsDeprecated, wasClean: true }, shardId); this.debug(GatewayCloseCodes[code], shardId); return; } diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 7e9308e3bb1a..199732638cf7 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -4845,8 +4845,10 @@ export interface ClientUserEditOptions { } export interface CloseEvent { + /** @deprecated */ wasClean: boolean; code: number; + /** @deprecated */ reason: string; } From 4e972fa94e4ff60763854a2f3716d5044b443d44 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 11 Mar 2023 08:07:33 +0100 Subject: [PATCH 14/20] types(ws): re-export in mainlib --- packages/discord.js/typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 199732638cf7..5278e1fb7e0c 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -6391,3 +6391,4 @@ export * from '@discordjs/builders'; export * from '@discordjs/formatters'; export * from '@discordjs/rest'; export * from '@discordjs/util'; +export * from '@discordjs/ws'; From b899ec88e046be863999d24d5842257b447385e9 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 25 Mar 2023 15:04:32 +0100 Subject: [PATCH 15/20] fix(WebSocketManager): pass compression to /ws --- .../discord.js/src/client/websocket/WebSocketManager.js | 8 ++++++++ packages/discord.js/src/util/Options.js | 1 - packages/discord.js/typings/index.d.ts | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 8a1084b316f1..3de96a580355 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -7,6 +7,7 @@ const { Collection } = require('@discordjs/collection'); const { WebSocketManager: WSWebSocketManager, WebSocketShardEvents: WSWebSocketShardEvents, + CompressionMethod, CloseCodes, } = require('@discordjs/ws'); const { GatewayCloseCodes, GatewayDispatchEvents } = require('discord-api-types/v10'); @@ -17,6 +18,12 @@ 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, @@ -151,6 +158,7 @@ class WebSocketManager extends EventEmitter { updateSessionInfo: (shardId, sessionInfo) => { this.shards.get(shardId).sessionInfo = sessionInfo; }, + compression: zlib ? CompressionMethod.ZlibStream : null, }; if (ws.buildStrategy) wsOptions.buildStrategy = ws.buildStrategy; this._ws = new WSWebSocketManager(wsOptions); diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 4f9be9c2d0e9..6cc20883d9a4 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -97,7 +97,6 @@ class Options extends null { sweepers: this.DefaultSweeperSettings, ws: { large_threshold: 50, - compress: false, properties: { os: process.platform, browser: 'discord.js', diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 5278e1fb7e0c..d58c611ae9be 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -6304,7 +6304,6 @@ export interface WebhookMessageCreateOptions extends Omit Date: Wed, 29 Mar 2023 18:35:41 +0200 Subject: [PATCH 16/20] fix(WebSocketManager): timeout ShardingManager --- packages/discord.js/src/client/websocket/WebSocketManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index 3de96a580355..f1da9a95926c 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -185,8 +185,9 @@ class WebSocketManager extends EventEmitter { this.gateway = `${gatewayURL}/`; - this.totalShards = this.client.options.shardCount = await this._ws.getShardCount(); + 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); From 5ad2ee700e548ba26f494cf800f92c5a7a923451 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 2 Apr 2023 17:43:55 +0200 Subject: [PATCH 17/20] docs(Options): apply suggestions --- packages/discord.js/src/util/Options.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 6cc20883d9a4..51c957ff714f 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -58,6 +58,16 @@ 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 + */ + /** * WebSocket options (these are left as snake_case to match the API) * @typedef {Object} WebsocketOptions @@ -65,8 +75,7 @@ const { version } = require('../../package.json'); * 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 {Function} [buildStrategy] Builds the strategy to use for sharding; - * example: `(manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 })` + * @property {BuildStrategyFunction} [buildStrategy] Builds the strategy to use for sharding */ /** @@ -204,10 +213,10 @@ module.exports = Options; /** * @external WSWebSocketManager - * @see {@link https://discord.js.org/#/docs/ws/main/class/WebSocketManager} + * @see {@link https://discord.js.org/docs/packages/ws/main/WebSocketManager:Class} */ /** * @external IShardingStrategy - * @see {@link https://discord.js.org/#/docs/ws/main/typedef/IShardingStrategy} + * @see {@link https://discord.js.org/docs/packages/ws/main/IShardingStrategy:Interface} */ From e9cda1985579113915a3d6507ee87bee9222a1bb Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 8 Apr 2023 08:37:04 +0200 Subject: [PATCH 18/20] docs: fix external links --- packages/discord.js/src/client/websocket/WebSocketManager.js | 2 +- packages/discord.js/src/util/Options.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index f1da9a95926c..e04ab55ffc0d 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -99,7 +99,7 @@ class WebSocketManager extends EventEmitter { /** * @external WSWebSocketManager - * @see {@link https://discord.js.org/#/docs/ws/main/class/WebSocketManager} + * @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class} */ /** diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 51c957ff714f..4685800f0d88 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -213,10 +213,10 @@ module.exports = Options; /** * @external WSWebSocketManager - * @see {@link https://discord.js.org/docs/packages/ws/main/WebSocketManager:Class} + * @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class} */ /** * @external IShardingStrategy - * @see {@link https://discord.js.org/docs/packages/ws/main/IShardingStrategy:Interface} + * @see {@link https://discord.js.org/docs/packages/ws/stable/IShardingStrategy:Interface} */ From f1ef37be8a9fa0ce5ad68253db21fdf1c1ec98c2 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 8 Apr 2023 20:56:56 +0200 Subject: [PATCH 19/20] fix(Options): remove unused ws properties --- packages/discord.js/src/util/Options.js | 6 ------ packages/discord.js/typings/index.d.ts | 7 ------- 2 files changed, 13 deletions(-) diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 4685800f0d88..7855d3e2703c 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -1,6 +1,5 @@ 'use strict'; -const process = require('node:process'); const { DefaultRestOptions, DefaultUserAgentAppendix } = require('@discordjs/rest'); const { toSnakeCase } = require('./Transformers'); const { version } = require('../../package.json'); @@ -106,11 +105,6 @@ class Options extends null { sweepers: this.DefaultSweeperSettings, ws: { large_threshold: 50, - properties: { - os: process.platform, - browser: 'discord.js', - device: 'discord.js', - }, version: 10, }, rest: { diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index d58c611ae9be..6de8e29569aa 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -6304,17 +6304,10 @@ export interface WebhookMessageCreateOptions extends Omit Date: Thu, 13 Apr 2023 19:48:17 +0200 Subject: [PATCH 20/20] fix: apply requested changes --- packages/discord.js/src/client/websocket/WebSocketManager.js | 5 ----- packages/discord.js/src/client/websocket/WebSocketShard.js | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index e04ab55ffc0d..7bd72f185d0d 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -97,11 +97,6 @@ class WebSocketManager extends EventEmitter { */ this.destroyed = false; - /** - * @external WSWebSocketManager - * @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class} - */ - /** * The internal WebSocketManager from `@discordjs/ws`. * @type {WSWebSocketManager} diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index 76319def7e82..babca23f20fc 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -7,6 +7,7 @@ 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} @@ -216,11 +217,12 @@ class WebSocketShard extends EventEmitter { * This parameter is **deprecated**. Important payloads are determined by their opcode instead. */ send(data, important = false) { - if (important) { + 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); }