refactor!: fully integrate /ws into mainlib (#10420)

BREAKING CHANGE: `Client#ws` is now a `@discordjs/ws#WebSocketManager`
BREAKING CHANGE: `WebSocketManager` and `WebSocketShard` are now re-exports from `@discordjs/ws`
BREAKING CHANGE: Removed the `WebSocketShardEvents` enum
BREAKING CHANGE: Renamed the `Client#ready` event to `Client#clientReady` event to not confuse it with the gateway `READY` event
BREAKING CHANGE: Added `Client#ping` to replace the old `WebSocketManager#ping`
BREAKING CHANGE: Removed the `Shard#reconnecting` event which wasn’t emitted anymore since 14.8.0 anyway
BREAKING CHANGE: Removed `ShardClientUtil#ids` and `ShardClientUtil#count` in favor of `Client#ws#getShardIds()` and `Client#ws#getShardCount()`
BREAKING CHANGE: `ClientUser#setPresence()` and `ClientPresence#set()` now return a Promise which resolves when the gateway call was sent successfully
BREAKING CHANGE: Removed `Guild#shard` as `WebSocketShard`s are now handled by `@discordjs/ws`
BREAKING CHANGE: Removed the following deprecated `Client` events: `raw`, `shardDisconnect`, `shardError`, `shardReady`, `shardReconnecting`, `shardResume` in favor of events from `@discordjs/ws#WebSocketManager`
BREAKING CHANGE: Removed `ClientOptions#shards` and `ClientOptions#shardCount` in favor of `ClientOptions#ws#shardIds` and `ClientOptions#ws#shardCount`
This commit is contained in:
Qjuh
2024-10-08 23:41:25 +02:00
committed by GitHub
parent 8ab4124ef9
commit a65c762950
31 changed files with 409 additions and 1086 deletions

View File

@@ -70,7 +70,7 @@
"@discordjs/formatters": "workspace:^", "@discordjs/formatters": "workspace:^",
"@discordjs/rest": "workspace:^", "@discordjs/rest": "workspace:^",
"@discordjs/util": "workspace:^", "@discordjs/util": "workspace:^",
"@discordjs/ws": "1.1.1", "@discordjs/ws": "workspace:^",
"@sapphire/snowflake": "3.5.3", "@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.37.101", "discord-api-types": "^0.37.101",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",

View File

@@ -1,14 +1,16 @@
'use strict'; 'use strict';
const process = require('node:process'); const process = require('node:process');
const { clearTimeout, setImmediate, setTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest'); const { makeURLSearchParams } = require('@discordjs/rest');
const { OAuth2Scopes, Routes } = require('discord-api-types/v10'); const { WebSocketManager, WebSocketShardEvents, WebSocketShardStatus } = require('@discordjs/ws');
const { GatewayDispatchEvents, GatewayIntentBits, OAuth2Scopes, Routes } = require('discord-api-types/v10');
const BaseClient = require('./BaseClient'); const BaseClient = require('./BaseClient');
const ActionsManager = require('./actions/ActionsManager'); const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager'); const PacketHandlers = require('./websocket/handlers');
const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const ChannelManager = require('../managers/ChannelManager'); const ChannelManager = require('../managers/ChannelManager');
const GuildManager = require('../managers/GuildManager'); const GuildManager = require('../managers/GuildManager');
@@ -31,6 +33,17 @@ const PermissionsBitField = require('../util/PermissionsBitField');
const Status = require('../util/Status'); const Status = require('../util/Status');
const Sweepers = require('../util/Sweepers'); const Sweepers = require('../util/Sweepers');
const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete];
const BeforeReadyWhitelist = [
GatewayDispatchEvents.Ready,
GatewayDispatchEvents.Resumed,
GatewayDispatchEvents.GuildCreate,
GatewayDispatchEvents.GuildDelete,
GatewayDispatchEvents.GuildMembersChunk,
GatewayDispatchEvents.GuildMemberAdd,
GatewayDispatchEvents.GuildMemberRemove,
];
/** /**
* The main hub for interacting with the Discord API, and the starting point for any bot. * The main hub for interacting with the Discord API, and the starting point for any bot.
* @extends {BaseClient} * @extends {BaseClient}
@@ -45,43 +58,45 @@ class Client extends BaseClient {
const data = require('node:worker_threads').workerData ?? process.env; const data = require('node:worker_threads').workerData ?? process.env;
const defaults = Options.createDefault(); const defaults = Options.createDefault();
if (this.options.shards === defaults.shards) { if (this.options.ws.shardIds === defaults.ws.shardIds && 'SHARDS' in data) {
if ('SHARDS' in data) { this.options.ws.shardIds = JSON.parse(data.SHARDS);
this.options.shards = JSON.parse(data.SHARDS);
}
} }
if (this.options.shardCount === defaults.shardCount) { if (this.options.ws.shardCount === defaults.ws.shardCount && 'SHARD_COUNT' in data) {
if ('SHARD_COUNT' in data) { this.options.ws.shardCount = Number(data.SHARD_COUNT);
this.options.shardCount = Number(data.SHARD_COUNT);
} else if (Array.isArray(this.options.shards)) {
this.options.shardCount = this.options.shards.length;
}
} }
const typeofShards = typeof this.options.shards; /**
* The presence of the Client
if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { * @private
this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); * @type {ClientPresence}
} */
this.presence = new ClientPresence(this, this.options.ws.initialPresence ?? this.options.presence);
if (typeofShards === 'number') this.options.shards = [this.options.shards];
if (Array.isArray(this.options.shards)) {
this.options.shards = [
...new Set(
this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)),
),
];
}
this._validateOptions(); this._validateOptions();
/** /**
* The WebSocket manager of the client * The current status of this Client
* @type {WebSocketManager} * @type {Status}
* @private
*/ */
this.ws = new WebSocketManager(this); this.status = Status.Idle;
/**
* A set of guild ids this Client expects to receive
* @name Client#expectedGuilds
* @type {Set<string>}
* @private
*/
Object.defineProperty(this, 'expectedGuilds', { value: new Set(), writable: true });
/**
* The ready timeout
* @name Client#readyTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/** /**
* The action manager of the client * The action manager of the client
@@ -90,12 +105,6 @@ class Client extends BaseClient {
*/ */
this.actions = new ActionsManager(this); this.actions = new ActionsManager(this);
/**
* The voice manager of the client
* @type {ClientVoiceManager}
*/
this.voice = new ClientVoiceManager(this);
/** /**
* Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
* @type {?ShardClientUtil} * @type {?ShardClientUtil}
@@ -119,7 +128,7 @@ class Client extends BaseClient {
/** /**
* All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids - * All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids -
* as long as sharding isn't being used, this will be *every* channel in *every* guild the bot * as long as no sharding manager is being used, this will be *every* channel in *every* guild the bot
* is a member of. Note that DM channels will not be initially cached, and thus not be present * is a member of. Note that DM channels will not be initially cached, and thus not be present
* in the Manager without their explicit fetching or use. * in the Manager without their explicit fetching or use.
* @type {ChannelManager} * @type {ChannelManager}
@@ -132,13 +141,6 @@ class Client extends BaseClient {
*/ */
this.sweepers = new Sweepers(this, this.options.sweepers); this.sweepers = new Sweepers(this, this.options.sweepers);
/**
* The presence of the Client
* @private
* @type {ClientPresence}
*/
this.presence = new ClientPresence(this, this.options.presence);
Object.defineProperty(this, 'token', { writable: true }); Object.defineProperty(this, 'token', { writable: true });
if (!this.token && 'DISCORD_TOKEN' in process.env) { if (!this.token && 'DISCORD_TOKEN' in process.env) {
/** /**
@@ -148,10 +150,31 @@ class Client extends BaseClient {
* @type {?string} * @type {?string}
*/ */
this.token = process.env.DISCORD_TOKEN; this.token = process.env.DISCORD_TOKEN;
} else if (this.options.ws.token) {
this.token = this.options.ws.token;
} else { } else {
this.token = null; this.token = null;
} }
const wsOptions = {
...this.options.ws,
intents: this.options.intents.bitfield,
rest: this.rest,
token: this.token,
};
/**
* The WebSocket manager of the client
* @type {WebSocketManager}
*/
this.ws = new WebSocketManager(wsOptions);
/**
* The voice manager of the client
* @type {ClientVoiceManager}
*/
this.voice = new ClientVoiceManager(this);
/** /**
* User that the client is logged in as * User that the client is logged in as
* @type {?ClientUser} * @type {?ClientUser}
@@ -164,11 +187,33 @@ class Client extends BaseClient {
*/ */
this.application = null; this.application = null;
/**
* The latencies of the WebSocketShard connections
* @type {Collection<number, number>}
*/
this.pings = new Collection();
/**
* The last time a ping was sent (a timestamp) for each WebSocketShard connection
* @type {Collection<number,number>}
*/
this.lastPingTimestamps = new Collection();
/** /**
* Timestamp of the time the client was last {@link Status.Ready} at * Timestamp of the time the client was last {@link Status.Ready} at
* @type {?number} * @type {?number}
*/ */
this.readyTimestamp = null; this.readyTimestamp = null;
/**
* An array of queued events before this Client became ready
* @type {Object[]}
* @private
* @name Client#incomingPacketQueue
*/
Object.defineProperty(this, 'incomingPacketQueue', { value: [] });
this._attachEvents();
} }
/** /**
@@ -215,13 +260,10 @@ class Client extends BaseClient {
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
this.rest.setToken(token); this.rest.setToken(token);
this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); this.emit(Events.Debug, `Provided token: ${this._censoredToken}`);
if (this.options.presence) {
this.options.ws.presence = this.presence._parse(this.options.presence);
}
this.emit(Events.Debug, 'Preparing to connect to the gateway...'); this.emit(Events.Debug, 'Preparing to connect to the gateway...');
this.ws.setToken(this.token);
try { try {
await this.ws.connect(); await this.ws.connect();
return this.token; return this.token;
@@ -231,13 +273,150 @@ class Client extends BaseClient {
} }
} }
/**
* Checks if the client can be marked as ready
* @private
*/
async _checkReady() {
// Step 0. Clear the ready timeout, if it exists
if (this.readyTimeout) {
clearTimeout(this.readyTimeout);
this.readyTimeout = null;
}
// Step 1. If we don't have any other guilds pending, we are ready
if (
!this.expectedGuilds.size &&
(await this.ws.fetchStatus()).every(status => status === WebSocketShardStatus.Ready)
) {
this.emit(Events.Debug, 'Client received all its guilds. Marking as fully ready.');
this.status = Status.Ready;
this._triggerClientReady();
return;
}
const hasGuildsIntent = this.options.intents.has(GatewayIntentBits.Guilds);
// Step 2. Create a timeout that will mark the client as ready if there are still unavailable guilds
// * The timeout is 15 seconds by default
// * This can be optionally changed in the client options via the `waitGuildTimeout` option
// * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds.
this.readyTimeout = setTimeout(
() => {
this.emit(
Events.Debug,
`${
hasGuildsIntent
? `Client did not receive any guild packets in ${this.options.waitGuildTimeout} ms.`
: 'Client will not receive anymore guild packets.'
}\nUnavailable guild count: ${this.expectedGuilds.size}`,
);
this.readyTimeout = null;
this.status = Status.Ready;
this._triggerClientReady();
},
hasGuildsIntent ? this.options.waitGuildTimeout : 0,
).unref();
}
/**
* Attaches event handlers to the WebSocketShardManager from `@discordjs/ws`.
* @private
*/
_attachEvents() {
this.ws.on(WebSocketShardEvents.Debug, (message, shardId) =>
this.emit(Events.Debug, `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`),
);
this.ws.on(WebSocketShardEvents.Dispatch, this._handlePacket.bind(this));
this.ws.on(WebSocketShardEvents.Ready, data => {
for (const guild of data.guilds) {
this.expectedGuilds.add(guild.id);
}
this.status = Status.WaitingForGuilds;
this._checkReady();
});
this.ws.on(WebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency }, shardId) => {
this.emit(Events.Debug, `[WS => Shard ${shardId}] Heartbeat acknowledged, latency of ${latency}ms.`);
this.lastPingTimestamps.set(shardId, heartbeatAt);
this.pings.set(shardId, latency);
});
}
/**
* Processes a packet and queues it if this WebSocketManager is not ready.
* @param {GatewayDispatchPayload} packet The packet to be handled
* @param {number} shardId The shardId that received this packet
* @private
*/
_handlePacket(packet, shardId) {
if (this.status !== Status.Ready && !BeforeReadyWhitelist.includes(packet.t)) {
this.incomingPacketQueue.push({ packet, shardId });
} else {
if (this.incomingPacketQueue.length) {
const item = this.incomingPacketQueue.shift();
setImmediate(() => {
this._handlePacket(item.packet, item.shardId);
}).unref();
}
if (PacketHandlers[packet.t]) {
PacketHandlers[packet.t](this, packet, shardId);
}
if (this.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(packet.t)) {
this.expectedGuilds.delete(packet.d.id);
this._checkReady();
}
}
}
/**
* Broadcasts a packet to every shard of this client handles.
* @param {Object} packet The packet to send
* @private
*/
async _broadcast(packet) {
const shardIds = await this.ws.getShardIds();
return Promise.all(shardIds.map(shardId => this.ws.send(shardId, packet)));
}
/**
* Causes the client to be marked as ready and emits the ready event.
* @private
*/
_triggerClientReady() {
this.status = Status.Ready;
this.readyTimestamp = Date.now();
/**
* Emitted when the client becomes ready to start working.
* @event Client#clientReady
* @param {Client} client The client
*/
this.emit(Events.ClientReady, this);
}
/** /**
* Returns whether the client has logged in, indicative of being able to access * Returns whether the client has logged in, indicative of being able to access
* properties such as `user` and `application`. * properties such as `user` and `application`.
* @returns {boolean} * @returns {boolean}
*/ */
isReady() { isReady() {
return !this.ws.destroyed && this.ws.status === Status.Ready; return this.status === Status.Ready;
}
/**
* The average ping of all WebSocketShards
* @type {number}
* @readonly
*/
get ping() {
const sum = this.pings.reduce((a, b) => a + b, 0);
return sum / this.pings.size;
} }
/** /**
@@ -505,20 +684,10 @@ class Client extends BaseClient {
* @private * @private
*/ */
_validateOptions(options = this.options) { _validateOptions(options = this.options) {
if (options.intents === undefined) { if (options.intents === undefined && options.ws?.intents === undefined) {
throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents); throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents);
} else { } else {
options.intents = new IntentsBitField(options.intents).freeze(); options.intents = new IntentsBitField(options.intents ?? options.ws.intents).freeze();
}
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shardCount', 'a number greater than or equal to 1');
}
if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shards', "'auto', a number or array of numbers");
}
if (options.shards && !options.shards.length) throw new DiscordjsRangeError(ErrorCodes.ClientInvalidProvidedShards);
if (typeof options.makeCache !== 'function') {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'makeCache', 'a function');
} }
if (typeof options.sweepers !== 'object' || options.sweepers === null) { if (typeof options.sweepers !== 'object' || options.sweepers === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'sweepers', 'an object'); throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'sweepers', 'an object');
@@ -541,12 +710,17 @@ class Client extends BaseClient {
) { ) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'allowedMentions', 'an object'); throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'allowedMentions', 'an object');
} }
if (typeof options.presence !== 'object' || options.presence === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object');
}
if (typeof options.ws !== 'object' || options.ws === null) { if (typeof options.ws !== 'object' || options.ws === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'ws', 'an object'); throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'ws', 'an object');
} }
if (
(typeof options.presence !== 'object' || options.presence === null) &&
options.ws.initialPresence === undefined
) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object');
} else {
options.ws.initialPresence = options.ws.initialPresence ?? this.presence._parse(this.options.presence);
}
if (typeof options.rest !== 'object' || options.rest === null) { if (typeof options.rest !== 'object' || options.rest === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'rest', 'an object'); throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'rest', 'an object');
} }

View File

@@ -2,10 +2,9 @@
const Action = require('./Action'); const Action = require('./Action');
const Events = require('../../util/Events'); const Events = require('../../util/Events');
const Status = require('../../util/Status');
class GuildMemberRemoveAction extends Action { class GuildMemberRemoveAction extends Action {
handle(data, shard) { handle(data) {
const client = this.client; const client = this.client;
const guild = client.guilds.cache.get(data.guild_id); const guild = client.guilds.cache.get(data.guild_id);
let member = null; let member = null;
@@ -19,7 +18,7 @@ class GuildMemberRemoveAction extends Action {
* @event Client#guildMemberRemove * @event Client#guildMemberRemove
* @param {GuildMember} member The member that has left/been kicked from the guild * @param {GuildMember} member The member that has left/been kicked from the guild
*/ */
if (shard.status === Status.Ready) client.emit(Events.GuildMemberRemove, member); client.emit(Events.GuildMemberRemove, member);
} }
guild.presences.cache.delete(data.user.id); guild.presences.cache.delete(data.user.id);
guild.voiceStates.cache.delete(data.user.id); guild.voiceStates.cache.delete(data.user.id);

View File

@@ -2,10 +2,9 @@
const Action = require('./Action'); const Action = require('./Action');
const Events = require('../../util/Events'); const Events = require('../../util/Events');
const Status = require('../../util/Status');
class GuildMemberUpdateAction extends Action { class GuildMemberUpdateAction extends Action {
handle(data, shard) { handle(data) {
const { client } = this; const { client } = this;
if (data.user.username) { if (data.user.username) {
const user = client.users.cache.get(data.user.id); const user = client.users.cache.get(data.user.id);
@@ -27,7 +26,7 @@ class GuildMemberUpdateAction extends Action {
* @param {GuildMember} oldMember The member before the update * @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update * @param {GuildMember} newMember The member after the update
*/ */
if (shard.status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); if (!member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member);
} else { } else {
const newMember = guild.members._add(data); const newMember = guild.members._add(data);
/** /**

View File

@@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Events = require('../../util/Events'); const { WebSocketShardEvents, CloseCodes } = require('@discordjs/ws');
/** /**
* Manages voice connections for the client * Manages voice connections for the client
@@ -21,10 +21,12 @@ class ClientVoiceManager {
*/ */
this.adapters = new Map(); this.adapters = new Map();
client.on(Events.ShardDisconnect, (_, shardId) => { client.ws.on(WebSocketShardEvents.Closed, (code, shardId) => {
for (const [guildId, adapter] of this.adapters.entries()) { if (code === CloseCodes.Normal) {
if (client.guilds.cache.get(guildId)?.shardId === shardId) { for (const [guildId, adapter] of this.adapters.entries()) {
adapter.destroy(); if (client.guilds.cache.get(guildId)?.shardId === shardId) {
adapter.destroy();
}
} }
} }
}); });

View File

@@ -1,387 +0,0 @@
'use strict';
const EventEmitter = require('node:events');
const process = require('node:process');
const { setImmediate } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const {
WebSocketManager: WSWebSocketManager,
WebSocketShardEvents: WSWebSocketShardEvents,
CompressionMethod,
CloseCodes,
} = require('@discordjs/ws');
const { GatewayCloseCodes, GatewayDispatchEvents } = require('discord-api-types/v10');
const WebSocketShard = require('./WebSocketShard');
const PacketHandlers = require('./handlers');
const { DiscordjsError, ErrorCodes } = require('../../errors');
const Events = require('../../util/Events');
const Status = require('../../util/Status');
const WebSocketShardEvents = require('../../util/WebSocketShardEvents');
let zlib;
try {
zlib = require('zlib-sync');
} catch {} // eslint-disable-line no-empty
const BeforeReadyWhitelist = [
GatewayDispatchEvents.Ready,
GatewayDispatchEvents.Resumed,
GatewayDispatchEvents.GuildCreate,
GatewayDispatchEvents.GuildDelete,
GatewayDispatchEvents.GuildMembersChunk,
GatewayDispatchEvents.GuildMemberAdd,
GatewayDispatchEvents.GuildMemberRemove,
];
const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete];
const UNRESUMABLE_CLOSE_CODES = [
CloseCodes.Normal,
GatewayCloseCodes.AlreadyAuthenticated,
GatewayCloseCodes.InvalidSeq,
];
const reasonIsDeprecated = 'the reason property is deprecated, use the code property to determine the reason';
let deprecationEmittedForInvalidSessionEvent = false;
let deprecationEmittedForDestroyedEvent = false;
/**
* The WebSocket manager for this client.
* <info>This class forwards raw dispatch events,
* read more about it here {@link https://discord.com/developers/docs/topics/gateway}</info>
* @extends {EventEmitter}
*/
class WebSocketManager extends EventEmitter {
constructor(client) {
super();
/**
* The client that instantiated this WebSocketManager
* @type {Client}
* @readonly
* @name WebSocketManager#client
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The gateway this manager uses
* @type {?string}
*/
this.gateway = null;
/**
* A collection of all shards this manager handles
* @type {Collection<number, WebSocketShard>}
*/
this.shards = new Collection();
/**
* An array of queued events before this WebSocketManager became ready
* @type {Object[]}
* @private
* @name WebSocketManager#packetQueue
*/
Object.defineProperty(this, 'packetQueue', { value: [] });
/**
* The current status of this WebSocketManager
* @type {Status}
*/
this.status = Status.Idle;
/**
* If this manager was destroyed. It will prevent shards from reconnecting
* @type {boolean}
* @private
*/
this.destroyed = false;
/**
* The internal WebSocketManager from `@discordjs/ws`.
* @type {WSWebSocketManager}
* @private
*/
this._ws = null;
}
/**
* The average ping of all WebSocketShards
* @type {number}
* @readonly
*/
get ping() {
const sum = this.shards.reduce((a, b) => a + b.ping, 0);
return sum / this.shards.size;
}
/**
* Emits a debug message.
* @param {string[]} messages The debug message
* @param {?number} [shardId] The id of the shard that emitted this message, if any
* @private
*/
debug(messages, shardId) {
this.client.emit(
Events.Debug,
`[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${messages.join('\n\t')}`,
);
}
/**
* Connects this manager to the gateway.
* @private
*/
async connect() {
const invalidToken = new DiscordjsError(ErrorCodes.TokenInvalid);
const { shards, shardCount, intents, ws } = this.client.options;
if (this._ws && this._ws.options.token !== this.client.token) {
await this._ws.destroy({ code: CloseCodes.Normal, reason: 'Login with differing token requested' });
this._ws = null;
}
if (!this._ws) {
const wsOptions = {
intents: intents.bitfield,
rest: this.client.rest,
token: this.client.token,
largeThreshold: ws.large_threshold,
version: ws.version,
shardIds: shards === 'auto' ? null : shards,
shardCount: shards === 'auto' ? null : shardCount,
initialPresence: ws.presence,
retrieveSessionInfo: shardId => this.shards.get(shardId).sessionInfo,
updateSessionInfo: (shardId, sessionInfo) => {
this.shards.get(shardId).sessionInfo = sessionInfo;
},
compression: zlib ? CompressionMethod.ZlibStream : null,
};
if (ws.buildIdentifyThrottler) wsOptions.buildIdentifyThrottler = ws.buildIdentifyThrottler;
if (ws.buildStrategy) wsOptions.buildStrategy = ws.buildStrategy;
this._ws = new WSWebSocketManager(wsOptions);
this.attachEvents();
}
const {
url: gatewayURL,
shards: recommendedShards,
session_start_limit: sessionStartLimit,
} = await this._ws.fetchGatewayInformation().catch(error => {
throw error.status === 401 ? invalidToken : error;
});
const { total, remaining } = sessionStartLimit;
this.debug(['Fetched Gateway Information', `URL: ${gatewayURL}`, `Recommended Shards: ${recommendedShards}`]);
this.debug(['Session Limit Information', `Total: ${total}`, `Remaining: ${remaining}`]);
this.gateway = `${gatewayURL}/`;
this.client.options.shardCount = await this._ws.getShardCount();
this.client.options.shards = await this._ws.getShardIds();
this.totalShards = this.client.options.shards.length;
for (const id of this.client.options.shards) {
if (!this.shards.has(id)) {
const shard = new WebSocketShard(this, id);
this.shards.set(id, shard);
shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => {
/**
* Emitted when a shard turns ready.
* @event Client#shardReady
* @param {number} id The shard id that turned ready
* @param {?Set<Snowflake>} unavailableGuilds Set of unavailable guild ids, if any
*/
this.client.emit(Events.ShardReady, shard.id, unavailableGuilds);
this.checkShardsReady();
});
shard.status = Status.Connecting;
}
}
await this._ws.connect();
this.shards.forEach(shard => {
if (shard.listenerCount(WebSocketShardEvents.InvalidSession) > 0 && !deprecationEmittedForInvalidSessionEvent) {
process.emitWarning(
'The WebSocketShard#invalidSession event is deprecated and will never emit.',
'DeprecationWarning',
);
deprecationEmittedForInvalidSessionEvent = true;
}
if (shard.listenerCount(WebSocketShardEvents.Destroyed) > 0 && !deprecationEmittedForDestroyedEvent) {
process.emitWarning(
'The WebSocketShard#destroyed event is deprecated and will never emit.',
'DeprecationWarning',
);
deprecationEmittedForDestroyedEvent = true;
}
});
}
/**
* Attaches event handlers to the internal WebSocketShardManager from `@discordjs/ws`.
* @private
*/
attachEvents() {
this._ws.on(WSWebSocketShardEvents.Debug, ({ message, shardId }) => this.debug([message], shardId));
this._ws.on(WSWebSocketShardEvents.Dispatch, ({ data, shardId }) => {
this.client.emit(Events.Raw, data, shardId);
this.emit(data.t, data.d, shardId);
const shard = this.shards.get(shardId);
this.handlePacket(data, shard);
if (shard.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(data.t)) {
shard.gotGuild(data.d.id);
}
});
this._ws.on(WSWebSocketShardEvents.Ready, ({ data, shardId }) => {
this.shards.get(shardId).onReadyPacket(data);
});
this._ws.on(WSWebSocketShardEvents.Closed, ({ code, shardId }) => {
const shard = this.shards.get(shardId);
shard.emit(WebSocketShardEvents.Close, { code, reason: reasonIsDeprecated, wasClean: true });
if (UNRESUMABLE_CLOSE_CODES.includes(code) && this.destroyed) {
shard.status = Status.Disconnected;
/**
* Emitted when a shard's WebSocket disconnects and will no longer reconnect.
* @event Client#shardDisconnect
* @param {CloseEvent} event The WebSocket close event
* @param {number} id The shard id that disconnected
*/
this.client.emit(Events.ShardDisconnect, { code, reason: reasonIsDeprecated, wasClean: true }, shardId);
this.debug([`Shard not resumable: ${code} (${GatewayCloseCodes[code] ?? CloseCodes[code]})`], shardId);
return;
}
this.shards.get(shardId).status = Status.Connecting;
/**
* Emitted when a shard is attempting to reconnect or re-identify.
* @event Client#shardReconnecting
* @param {number} id The shard id that is attempting to reconnect
*/
this.client.emit(Events.ShardReconnecting, shardId);
});
this._ws.on(WSWebSocketShardEvents.Hello, ({ shardId }) => {
const shard = this.shards.get(shardId);
if (shard.sessionInfo) {
shard.closeSequence = shard.sessionInfo.sequence;
shard.status = Status.Resuming;
} else {
shard.status = Status.Identifying;
}
});
this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => {
const shard = this.shards.get(shardId);
shard.status = Status.Ready;
/**
* Emitted when the shard resumes successfully
* @event WebSocketShard#resumed
*/
shard.emit(WebSocketShardEvents.Resumed);
});
this._ws.on(WSWebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency, shardId }) => {
this.debug([`Heartbeat acknowledged, latency of ${latency}ms.`], shardId);
const shard = this.shards.get(shardId);
shard.lastPingTimestamp = heartbeatAt;
shard.ping = latency;
});
this._ws.on(WSWebSocketShardEvents.Error, ({ error, shardId }) => {
/**
* Emitted whenever a shard's WebSocket encounters a connection error.
* @event Client#shardError
* @param {Error} error The encountered error
* @param {number} shardId The shard that encountered this error
*/
this.client.emit(Events.ShardError, error, shardId);
});
}
/**
* Broadcasts a packet to every shard this manager handles.
* @param {Object} packet The packet to send
* @private
*/
broadcast(packet) {
for (const shardId of this.shards.keys()) this._ws.send(shardId, packet);
}
/**
* Destroys this manager and all its shards.
* @private
*/
async destroy() {
if (this.destroyed) return;
// TODO: Make a util for getting a stack
this.debug([Object.assign(new Error(), { name: 'Manager was destroyed:' }).stack]);
this.destroyed = true;
await this._ws?.destroy({ code: CloseCodes.Normal, reason: 'Manager was destroyed' });
}
/**
* Processes a packet and queues it if this WebSocketManager is not ready.
* @param {Object} [packet] The packet to be handled
* @param {WebSocketShard} [shard] The shard that will handle this packet
* @returns {boolean}
* @private
*/
handlePacket(packet, shard) {
if (packet && this.status !== Status.Ready) {
if (!BeforeReadyWhitelist.includes(packet.t)) {
this.packetQueue.push({ packet, shard });
return false;
}
}
if (this.packetQueue.length) {
const item = this.packetQueue.shift();
setImmediate(() => {
this.handlePacket(item.packet, item.shard);
}).unref();
}
if (packet && PacketHandlers[packet.t]) {
PacketHandlers[packet.t](this.client, packet, shard);
}
return true;
}
/**
* Checks whether the client is ready to be marked as ready.
* @private
*/
checkShardsReady() {
if (this.status === Status.Ready) return;
if (this.shards.size !== this.totalShards || this.shards.some(shard => shard.status !== Status.Ready)) {
return;
}
this.triggerClientReady();
}
/**
* Causes the client to be marked as ready and emits the ready event.
* @private
*/
triggerClientReady() {
this.status = Status.Ready;
this.client.readyTimestamp = Date.now();
/**
* Emitted when the client becomes ready to start working.
* @event Client#ready
* @param {Client} client The client
*/
this.client.emit(Events.ClientReady, this.client);
this.handlePacket();
}
}
module.exports = WebSocketManager;

View File

@@ -1,234 +0,0 @@
'use strict';
const EventEmitter = require('node:events');
const process = require('node:process');
const { setTimeout, clearTimeout } = require('node:timers');
const { GatewayIntentBits } = require('discord-api-types/v10');
const Status = require('../../util/Status');
const WebSocketShardEvents = require('../../util/WebSocketShardEvents');
let deprecationEmittedForImportant = false;
/**
* Represents a Shard's WebSocket connection
* @extends {EventEmitter}
*/
class WebSocketShard extends EventEmitter {
constructor(manager, id) {
super();
/**
* The WebSocketManager of the shard
* @type {WebSocketManager}
*/
this.manager = manager;
/**
* The shard's id
* @type {number}
*/
this.id = id;
/**
* The current status of the shard
* @type {Status}
*/
this.status = Status.Idle;
/**
* The sequence of the shard after close
* @type {number}
* @private
*/
this.closeSequence = 0;
/**
* The previous heartbeat ping of the shard
* @type {number}
*/
this.ping = -1;
/**
* The last time a ping was sent (a timestamp)
* @type {number}
*/
this.lastPingTimestamp = -1;
/**
* A set of guild ids this shard expects to receive
* @name WebSocketShard#expectedGuilds
* @type {?Set<string>}
* @private
*/
Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
/**
* The ready timeout
* @name WebSocketShard#readyTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/**
* @external SessionInfo
* @see {@link https://discord.js.org/docs/packages/ws/stable/SessionInfo:Interface}
*/
/**
* The session info used by `@discordjs/ws` package.
* @name WebSocketShard#sessionInfo
* @type {?SessionInfo}
* @private
*/
Object.defineProperty(this, 'sessionInfo', { value: null, writable: true });
}
/**
* Emits a debug event.
* @param {string[]} messages The debug message
* @private
*/
debug(messages) {
this.manager.debug(messages, this.id);
}
/**
* @external CloseEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
*/
/**
* This method is responsible to emit close event for this shard.
* This method helps the shard reconnect.
* @param {CloseEvent} [event] Close event that was received
* @deprecated
*/
emitClose(
event = {
code: 1011,
reason: 'INTERNAL_ERROR',
wasClean: false,
},
) {
this.debug([
'[CLOSE]',
`Event Code: ${event.code}`,
`Clean : ${event.wasClean}`,
`Reason : ${event.reason ?? 'No reason received'}`,
]);
/**
* Emitted when a shard's WebSocket closes.
* @private
* @event WebSocketShard#close
* @param {CloseEvent} event The received event
*/
this.emit(WebSocketShardEvents.Close, event);
}
/**
* Called when the shard receives the READY payload.
* @param {Object} packet The received packet
* @private
*/
onReadyPacket(packet) {
if (!packet) {
this.debug([`Received broken packet: '${packet}'.`]);
return;
}
/**
* Emitted when the shard receives the READY payload and is now waiting for guilds
* @event WebSocketShard#ready
*/
this.emit(WebSocketShardEvents.Ready);
this.expectedGuilds = new Set(packet.guilds.map(guild => guild.id));
this.status = Status.WaitingForGuilds;
}
/**
* Called when a GuildCreate or GuildDelete for this shard was sent after READY payload was received,
* but before we emitted the READY event.
* @param {Snowflake} guildId the id of the Guild sent in the payload
* @private
*/
gotGuild(guildId) {
this.expectedGuilds.delete(guildId);
this.checkReady();
}
/**
* Checks if the shard can be marked as ready
* @private
*/
checkReady() {
// Step 0. Clear the ready timeout, if it exists
if (this.readyTimeout) {
clearTimeout(this.readyTimeout);
this.readyTimeout = null;
}
// Step 1. If we don't have any other guilds pending, we are ready
if (!this.expectedGuilds.size) {
this.debug(['Shard received all its guilds. Marking as fully ready.']);
this.status = Status.Ready;
/**
* Emitted when the shard is fully ready.
* This event is emitted if:
* * all guilds were received by this shard
* * the ready timeout expired, and some guilds are unavailable
* @event WebSocketShard#allReady
* @param {?Set<string>} unavailableGuilds Set of unavailable guilds, if any
*/
this.emit(WebSocketShardEvents.AllReady);
return;
}
const hasGuildsIntent = this.manager.client.options.intents.has(GatewayIntentBits.Guilds);
// Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds
// * The timeout is 15 seconds by default
// * This can be optionally changed in the client options via the `waitGuildTimeout` option
// * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds.
const { waitGuildTimeout } = this.manager.client.options;
this.readyTimeout = setTimeout(
() => {
this.debug([
hasGuildsIntent
? `Shard did not receive any guild packets in ${waitGuildTimeout} ms.`
: 'Shard will not receive anymore guild packets.',
`Unavailable guild count: ${this.expectedGuilds.size}`,
]);
this.readyTimeout = null;
this.status = Status.Ready;
this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds);
},
hasGuildsIntent ? waitGuildTimeout : 0,
).unref();
}
/**
* Adds a packet to the queue to be sent to the gateway.
* <warn>If you use this method, make sure you understand that you need to provide
* a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* Do not use this method if you don't know what you're doing.</warn>
* @param {Object} data The full packet to send
* @param {boolean} [important=false] If this packet should be added first in queue
* <warn>This parameter is **deprecated**. Important payloads are determined by their opcode instead.</warn>
*/
send(data, important = false) {
if (important && !deprecationEmittedForImportant) {
process.emitWarning(
'Sending important payloads explicitly is deprecated. They are determined by their opcode implicitly now.',
'DeprecationWarning',
);
deprecationEmittedForImportant = true;
}
this.manager._ws.send(this.id, data);
}
}
module.exports = WebSocketShard;

View File

@@ -3,7 +3,7 @@
const Events = require('../../../util/Events'); const Events = require('../../../util/Events');
const Status = require('../../../util/Status'); const Status = require('../../../util/Status');
module.exports = (client, { d: data }, shard) => { module.exports = (client, { d: data }, shardId) => {
let guild = client.guilds.cache.get(data.id); let guild = client.guilds.cache.get(data.id);
if (guild) { if (guild) {
if (!guild.available && !data.unavailable) { if (!guild.available && !data.unavailable) {
@@ -19,9 +19,9 @@ module.exports = (client, { d: data }, shard) => {
} }
} else { } else {
// A new guild // A new guild
data.shardId = shard.id; data.shardId = shardId;
guild = client.guilds._add(data); guild = client.guilds._add(data);
if (client.ws.status === Status.Ready) { if (client.status === Status.Ready) {
/** /**
* Emitted whenever the client joins a guild. * Emitted whenever the client joins a guild.
* @event Client#guildCreate * @event Client#guildCreate

View File

@@ -1,20 +1,17 @@
'use strict'; 'use strict';
const Events = require('../../../util/Events'); const Events = require('../../../util/Events');
const Status = require('../../../util/Status');
module.exports = (client, { d: data }, shard) => { module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id); const guild = client.guilds.cache.get(data.guild_id);
if (guild) { if (guild) {
guild.memberCount++; guild.memberCount++;
const member = guild.members._add(data); const member = guild.members._add(data);
if (shard.status === Status.Ready) { /**
/** * Emitted whenever a user joins a guild.
* Emitted whenever a user joins a guild. * @event Client#guildMemberAdd
* @event Client#guildMemberAdd * @param {GuildMember} member The member that has joined a guild
* @param {GuildMember} member The member that has joined a guild */
*/ client.emit(Events.GuildMemberAdd, member);
client.emit(Events.GuildMemberAdd, member);
}
} }
}; };

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
module.exports = (client, packet, shard) => { module.exports = (client, packet) => {
client.actions.GuildMemberRemove.handle(packet.d, shard); client.actions.GuildMemberRemove.handle(packet.d);
}; };

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
module.exports = (client, packet, shard) => { module.exports = (client, packet) => {
client.actions.GuildMemberUpdate.handle(packet.d, shard); client.actions.GuildMemberUpdate.handle(packet.d);
}; };

View File

@@ -3,7 +3,7 @@
const ClientApplication = require('../../../structures/ClientApplication'); const ClientApplication = require('../../../structures/ClientApplication');
let ClientUser; let ClientUser;
module.exports = (client, { d: data }, shard) => { module.exports = (client, { d: data }, shardId) => {
if (client.user) { if (client.user) {
client.user._patch(data.user); client.user._patch(data.user);
} else { } else {
@@ -13,7 +13,7 @@ module.exports = (client, { d: data }, shard) => {
} }
for (const guild of data.guilds) { for (const guild of data.guilds) {
guild.shardId = shard.id; guild.shardId = shardId;
client.guilds._add(guild); client.guilds._add(guild);
} }
@@ -22,6 +22,4 @@ module.exports = (client, { d: data }, shard) => {
} else { } else {
client.application = new ClientApplication(client, data.application); client.application = new ClientApplication(client, data.application);
} }
shard.checkReady();
}; };

View File

@@ -1,14 +0,0 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, packet, shard) => {
const replayed = shard.sessionInfo.sequence - shard.closeSequence;
/**
* Emitted when a shard resumes successfully.
* @event Client#shardResume
* @param {number} id The shard id that resumed
* @param {number} replayedEvents The amount of replayed events
*/
client.emit(Events.ShardResume, shard.id, replayed);
};

View File

@@ -49,7 +49,6 @@ const handlers = Object.fromEntries([
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')], ['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')],
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')], ['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')],
['READY', require('./READY')], ['READY', require('./READY')],
['RESUMED', require('./RESUMED')],
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')], ['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')], ['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')], ['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],

View File

@@ -12,8 +12,25 @@
* @property {'TokenMissing'} TokenMissing * @property {'TokenMissing'} TokenMissing
* @property {'ApplicationCommandPermissionsTokenMissing'} ApplicationCommandPermissionsTokenMissing * @property {'ApplicationCommandPermissionsTokenMissing'} ApplicationCommandPermissionsTokenMissing
* @property {'BitFieldInvalid'} BitFieldInvalid * @property {'WSCloseRequested'} WSCloseRequested
* <warn>This property is deprecated.</warn>
* @property {'WSConnectionExists'} WSConnectionExists
* <warn>This property is deprecated.</warn>
* @property {'WSNotOpen'} WSNotOpen
* <warn>This property is deprecated.</warn>
* @property {'ManagerDestroyed'} ManagerDestroyed
* <warn>This property is deprecated.</warn>
* @property {'BitFieldInvalid'} BitFieldInvalid
* @property {'ShardingInvalid'} ShardingInvalid
* <warn>This property is deprecated.</warn>
* @property {'ShardingRequired'} ShardingRequired
* <warn>This property is deprecated.</warn>
* @property {'InvalidIntents'} InvalidIntents
* <warn>This property is deprecated.</warn>
* @property {'DisallowedIntents'} DisallowedIntents
* <warn>This property is deprecated.</warn>
* @property {'ShardingNoShards'} ShardingNoShards * @property {'ShardingNoShards'} ShardingNoShards
* @property {'ShardingInProcess'} ShardingInProcess * @property {'ShardingInProcess'} ShardingInProcess
* @property {'ShardingInvalidEvalBroadcast'} ShardingInvalidEvalBroadcast * @property {'ShardingInvalidEvalBroadcast'} ShardingInvalidEvalBroadcast
@@ -32,10 +49,30 @@
* @property {'InviteOptionsMissingChannel'} InviteOptionsMissingChannel * @property {'InviteOptionsMissingChannel'} InviteOptionsMissingChannel
* @property {'ButtonLabel'} ButtonLabel
* <warn>This property is deprecated.</warn>
* @property {'ButtonURL'} ButtonURL
* <warn>This property is deprecated.</warn>
* @property {'ButtonCustomId'} ButtonCustomId
* <warn>This property is deprecated.</warn>
* @property {'SelectMenuCustomId'} SelectMenuCustomId
* <warn>This property is deprecated.</warn>
* @property {'SelectMenuPlaceholder'} SelectMenuPlaceholder
* <warn>This property is deprecated.</warn>
* @property {'SelectOptionLabel'} SelectOptionLabel
* <warn>This property is deprecated.</warn>
* @property {'SelectOptionValue'} SelectOptionValue
* <warn>This property is deprecated.</warn>
* @property {'SelectOptionDescription'} SelectOptionDescription
* <warn>This property is deprecated.</warn>
* @property {'InteractionCollectorError'} InteractionCollectorError * @property {'InteractionCollectorError'} InteractionCollectorError
* @property {'FileNotFound'} FileNotFound * @property {'FileNotFound'} FileNotFound
* @property {'UserBannerNotFetched'} UserBannerNotFetched
* <warn>This property is deprecated.</warn>
* @property {'UserNoDMChannel'} UserNoDMChannel * @property {'UserNoDMChannel'} UserNoDMChannel
* @property {'VoiceNotStageChannel'} VoiceNotStageChannel * @property {'VoiceNotStageChannel'} VoiceNotStageChannel
@@ -45,11 +82,19 @@
* @property {'ReqResourceType'} ReqResourceType * @property {'ReqResourceType'} ReqResourceType
* @property {'ImageFormat'} ImageFormat
* <warn>This property is deprecated.</warn>
* @property {'ImageSize'} ImageSize
* <warn>This property is deprecated.</warn>
* @property {'MessageBulkDeleteType'} MessageBulkDeleteType * @property {'MessageBulkDeleteType'} MessageBulkDeleteType
* @property {'MessageContentType'} MessageContentType * @property {'MessageContentType'} MessageContentType
* @property {'MessageNonceRequired'} MessageNonceRequired * @property {'MessageNonceRequired'} MessageNonceRequired
* @property {'MessageNonceType'} MessageNonceType * @property {'MessageNonceType'} MessageNonceType
* @property {'SplitMaxLen'} SplitMaxLen
* <warn>This property is deprecated.</warn>
* @property {'BanResolveId'} BanResolveId * @property {'BanResolveId'} BanResolveId
* @property {'FetchBanResolveId'} FetchBanResolveId * @property {'FetchBanResolveId'} FetchBanResolveId
@@ -83,11 +128,16 @@
* @property {'EmojiType'} EmojiType * @property {'EmojiType'} EmojiType
* @property {'EmojiManaged'} EmojiManaged * @property {'EmojiManaged'} EmojiManaged
* @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission * @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission
* @property {'MissingManageEmojisAndStickersPermission'} MissingManageEmojisAndStickersPermission
* <warn>This property is deprecated. Use `MissingManageGuildExpressionsPermission` instead.</warn>
* *
* @property {'NotGuildSticker'} NotGuildSticker * @property {'NotGuildSticker'} NotGuildSticker
* @property {'ReactionResolveUser'} ReactionResolveUser * @property {'ReactionResolveUser'} ReactionResolveUser
* @property {'VanityURL'} VanityURL
* <warn>This property is deprecated.</warn>
* @property {'InviteResolveCode'} InviteResolveCode * @property {'InviteResolveCode'} InviteResolveCode
* @property {'InviteNotFound'} InviteNotFound * @property {'InviteNotFound'} InviteNotFound
@@ -102,6 +152,8 @@
* @property {'InteractionAlreadyReplied'} InteractionAlreadyReplied * @property {'InteractionAlreadyReplied'} InteractionAlreadyReplied
* @property {'InteractionNotReplied'} InteractionNotReplied * @property {'InteractionNotReplied'} InteractionNotReplied
* @property {'InteractionEphemeralReplied'} InteractionEphemeralReplied
* <warn>This property is deprecated.</warn>
* @property {'CommandInteractionOptionNotFound'} CommandInteractionOptionNotFound * @property {'CommandInteractionOptionNotFound'} CommandInteractionOptionNotFound
* @property {'CommandInteractionOptionType'} CommandInteractionOptionType * @property {'CommandInteractionOptionType'} CommandInteractionOptionType
@@ -140,8 +192,17 @@ const keys = [
'TokenMissing', 'TokenMissing',
'ApplicationCommandPermissionsTokenMissing', 'ApplicationCommandPermissionsTokenMissing',
'WSCloseRequested',
'WSConnectionExists',
'WSNotOpen',
'ManagerDestroyed',
'BitFieldInvalid', 'BitFieldInvalid',
'ShardingInvalid',
'ShardingRequired',
'InvalidIntents',
'DisallowedIntents',
'ShardingNoShards', 'ShardingNoShards',
'ShardingInProcess', 'ShardingInProcess',
'ShardingInvalidEvalBroadcast', 'ShardingInvalidEvalBroadcast',
@@ -160,10 +221,21 @@ const keys = [
'InviteOptionsMissingChannel', 'InviteOptionsMissingChannel',
'ButtonLabel',
'ButtonURL',
'ButtonCustomId',
'SelectMenuCustomId',
'SelectMenuPlaceholder',
'SelectOptionLabel',
'SelectOptionValue',
'SelectOptionDescription',
'InteractionCollectorError', 'InteractionCollectorError',
'FileNotFound', 'FileNotFound',
'UserBannerNotFetched',
'UserNoDMChannel', 'UserNoDMChannel',
'VoiceNotStageChannel', 'VoiceNotStageChannel',
@@ -173,11 +245,16 @@ const keys = [
'ReqResourceType', 'ReqResourceType',
'ImageFormat',
'ImageSize',
'MessageBulkDeleteType', 'MessageBulkDeleteType',
'MessageContentType', 'MessageContentType',
'MessageNonceRequired', 'MessageNonceRequired',
'MessageNonceType', 'MessageNonceType',
'SplitMaxLen',
'BanResolveId', 'BanResolveId',
'FetchBanResolveId', 'FetchBanResolveId',
@@ -211,11 +288,14 @@ const keys = [
'EmojiType', 'EmojiType',
'EmojiManaged', 'EmojiManaged',
'MissingManageGuildExpressionsPermission', 'MissingManageGuildExpressionsPermission',
'MissingManageEmojisAndStickersPermission',
'NotGuildSticker', 'NotGuildSticker',
'ReactionResolveUser', 'ReactionResolveUser',
'VanityURL',
'InviteResolveCode', 'InviteResolveCode',
'InviteNotFound', 'InviteNotFound',
@@ -230,6 +310,7 @@ const keys = [
'InteractionAlreadyReplied', 'InteractionAlreadyReplied',
'InteractionNotReplied', 'InteractionNotReplied',
'InteractionEphemeralReplied',
'CommandInteractionOptionNotFound', 'CommandInteractionOptionNotFound',
'CommandInteractionOptionType', 'CommandInteractionOptionType',

View File

@@ -48,7 +48,6 @@ exports.SystemChannelFlagsBitField = require('./util/SystemChannelFlagsBitField'
exports.ThreadMemberFlagsBitField = require('./util/ThreadMemberFlagsBitField'); exports.ThreadMemberFlagsBitField = require('./util/ThreadMemberFlagsBitField');
exports.UserFlagsBitField = require('./util/UserFlagsBitField'); exports.UserFlagsBitField = require('./util/UserFlagsBitField');
__exportStar(require('./util/Util.js'), exports); __exportStar(require('./util/Util.js'), exports);
exports.WebSocketShardEvents = require('./util/WebSocketShardEvents');
exports.version = require('../package.json').version; exports.version = require('../package.json').version;
// Managers // Managers
@@ -88,8 +87,6 @@ exports.ThreadManager = require('./managers/ThreadManager');
exports.ThreadMemberManager = require('./managers/ThreadMemberManager'); exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
exports.UserManager = require('./managers/UserManager'); exports.UserManager = require('./managers/UserManager');
exports.VoiceStateManager = require('./managers/VoiceStateManager'); exports.VoiceStateManager = require('./managers/VoiceStateManager');
exports.WebSocketManager = require('./client/websocket/WebSocketManager');
exports.WebSocketShard = require('./client/websocket/WebSocketShard');
// Structures // Structures
exports.ActionRow = require('./structures/ActionRow'); exports.ActionRow = require('./structures/ActionRow');

View File

@@ -273,7 +273,7 @@ class GuildManager extends CachedManager {
const data = await this.client.rest.get(Routes.guild(id), { const data = await this.client.rest.get(Routes.guild(id), {
query: makeURLSearchParams({ with_counts: options.withCounts ?? true }), query: makeURLSearchParams({ with_counts: options.withCounts ?? true }),
}); });
data.shardId = ShardClientUtil.shardIdForGuildId(id, this.client.options.shardCount); data.shardId = ShardClientUtil.shardIdForGuildId(id, await this.client.ws.fetchShardCount());
return this._add(data, options.cache); return this._add(data, options.cache);
} }

View File

@@ -235,7 +235,7 @@ class GuildMemberManager extends CachedManager {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!query && !users) query = ''; if (!query && !users) query = '';
this.guild.shard.send({ this.guild.client.ws.send(this.guild.shardId, {
op: GatewayOpcodes.RequestGuildMembers, op: GatewayOpcodes.RequestGuildMembers,
d: { d: {
guild_id: this.guild.id, guild_id: this.guild.id,

View File

@@ -352,7 +352,7 @@ class Shard extends EventEmitter {
if (message._ready) { if (message._ready) {
this.ready = true; this.ready = true;
/** /**
* Emitted upon the shard's {@link Client#event:shardReady} event. * Emitted upon the shard's {@link Client#event:clientReady} event.
* @event Shard#ready * @event Shard#ready
*/ */
this.emit(ShardEvents.Ready); this.emit(ShardEvents.Ready);
@@ -363,29 +363,18 @@ class Shard extends EventEmitter {
if (message._disconnect) { if (message._disconnect) {
this.ready = false; this.ready = false;
/** /**
* Emitted upon the shard's {@link Client#event:shardDisconnect} event. * Emitted upon the shard's {@link WebSocketShardEvents#Closed} event.
* @event Shard#disconnect * @event Shard#disconnect
*/ */
this.emit(ShardEvents.Disconnect); this.emit(ShardEvents.Disconnect);
return; return;
} }
// Shard is attempting to reconnect
if (message._reconnecting) {
this.ready = false;
/**
* Emitted upon the shard's {@link Client#event:shardReconnecting} event.
* @event Shard#reconnecting
*/
this.emit(ShardEvents.Reconnecting);
return;
}
// Shard has resumed // Shard has resumed
if (message._resume) { if (message._resume) {
this.ready = true; this.ready = true;
/** /**
* Emitted upon the shard's {@link Client#event:shardResume} event. * Emitted upon the shard's {@link WebSocketShardEvents#Resumed} event.
* @event Shard#resume * @event Shard#resume
*/ */
this.emit(ShardEvents.Resume); this.emit(ShardEvents.Resume);

View File

@@ -2,6 +2,7 @@
const process = require('node:process'); const process = require('node:process');
const { calculateShardId } = require('@discordjs/util'); const { calculateShardId } = require('@discordjs/util');
const { WebSocketShardEvents } = require('@discordjs/ws');
const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors');
const Events = require('../util/Events'); const Events = require('../util/Events');
const { makeError, makePlainError } = require('../util/Util'); const { makeError, makePlainError } = require('../util/Util');
@@ -33,56 +34,32 @@ class ShardClientUtil {
switch (mode) { switch (mode) {
case 'process': case 'process':
process.on('message', this._handleMessage.bind(this)); process.on('message', this._handleMessage.bind(this));
client.on(Events.ShardReady, () => { client.on(Events.ClientReady, () => {
process.send({ _ready: true }); process.send({ _ready: true });
}); });
client.on(Events.ShardDisconnect, () => { client.ws.on(WebSocketShardEvents.Closed, () => {
process.send({ _disconnect: true }); process.send({ _disconnect: true });
}); });
client.on(Events.ShardReconnecting, () => { client.ws.on(WebSocketShardEvents.Resumed, () => {
process.send({ _reconnecting: true });
});
client.on(Events.ShardResume, () => {
process.send({ _resume: true }); process.send({ _resume: true });
}); });
break; break;
case 'worker': case 'worker':
this.parentPort = require('node:worker_threads').parentPort; this.parentPort = require('node:worker_threads').parentPort;
this.parentPort.on('message', this._handleMessage.bind(this)); this.parentPort.on('message', this._handleMessage.bind(this));
client.on(Events.ShardReady, () => { client.on(Events.ClientReady, () => {
this.parentPort.postMessage({ _ready: true }); this.parentPort.postMessage({ _ready: true });
}); });
client.on(Events.ShardDisconnect, () => { client.ws.on(WebSocketShardEvents.Closed, () => {
this.parentPort.postMessage({ _disconnect: true }); this.parentPort.postMessage({ _disconnect: true });
}); });
client.on(Events.ShardReconnecting, () => { client.ws.on(WebSocketShardEvents.Resumed, () => {
this.parentPort.postMessage({ _reconnecting: true });
});
client.on(Events.ShardResume, () => {
this.parentPort.postMessage({ _resume: true }); this.parentPort.postMessage({ _resume: true });
}); });
break; break;
} }
} }
/**
* Array of shard ids of this client
* @type {number[]}
* @readonly
*/
get ids() {
return this.client.options.shards;
}
/**
* Total number of shards
* @type {number}
* @readonly
*/
get count() {
return this.client.options.shardCount;
}
/** /**
* Sends a message to the master process. * Sends a message to the master process.
* @param {*} message Message to send * @param {*} message Message to send

View File

@@ -16,19 +16,19 @@ class ClientPresence extends Presence {
/** /**
* Sets the client's presence * Sets the client's presence
* @param {PresenceData} presence The data to set the presence to * @param {PresenceData} presence The data to set the presence to
* @returns {ClientPresence} * @returns {Promise<ClientPresence>}
*/ */
set(presence) { async set(presence) {
const packet = this._parse(presence); const packet = this._parse(presence);
this._patch(packet); this._patch(packet);
if (presence.shardId === undefined) { if (presence.shardId === undefined) {
this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet }); await this.client._broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet });
} else if (Array.isArray(presence.shardId)) { } else if (Array.isArray(presence.shardId)) {
for (const shardId of presence.shardId) { await Promise.all(
this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); presence.shardId.map(shardId => this.client.ws.send(shardId, { op: GatewayOpcodes.PresenceUpdate, d: packet })),
} );
} else { } else {
this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); await this.client.ws.send(presence.shardId, { op: GatewayOpcodes.PresenceUpdate, d: packet });
} }
return this; return this;
} }

View File

@@ -135,7 +135,7 @@ class ClientUser extends User {
/** /**
* Sets the full presence of the client user. * Sets the full presence of the client user.
* @param {PresenceData} data Data for the presence * @param {PresenceData} data Data for the presence
* @returns {ClientPresence} * @returns {Promise<ClientPresence>}
* @example * @example
* // Set the client user's presence * // Set the client user's presence
* client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' }); * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' });
@@ -157,7 +157,7 @@ class ClientUser extends User {
* Sets the status of the client user. * Sets the status of the client user.
* @param {PresenceStatusData} status Status to change to * @param {PresenceStatusData} status Status to change to
* @param {number|number[]} [shardId] Shard id(s) to have the activity set on * @param {number|number[]} [shardId] Shard id(s) to have the activity set on
* @returns {ClientPresence} * @returns {Promise<ClientPresence>}
* @example * @example
* // Set the client user's status * // Set the client user's status
* client.user.setStatus('idle'); * client.user.setStatus('idle');
@@ -180,7 +180,7 @@ class ClientUser extends User {
* Sets the activity the client user is playing. * Sets the activity the client user is playing.
* @param {string|ActivityOptions} name Activity being played, or options for setting the activity * @param {string|ActivityOptions} name Activity being played, or options for setting the activity
* @param {ActivityOptions} [options] Options for setting the activity * @param {ActivityOptions} [options] Options for setting the activity
* @returns {ClientPresence} * @returns {Promise<ClientPresence>}
* @example * @example
* // Set the client user's activity * // Set the client user's activity
* client.user.setActivity('discord.js', { type: ActivityType.Watching }); * client.user.setActivity('discord.js', { type: ActivityType.Watching });
@@ -196,7 +196,7 @@ class ClientUser extends User {
* Sets/removes the AFK flag for the client user. * Sets/removes the AFK flag for the client user.
* @param {boolean} [afk=true] Whether or not the user is AFK * @param {boolean} [afk=true] Whether or not the user is AFK
* @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on * @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on
* @returns {ClientPresence} * @returns {Promise<ClientPresence>}
*/ */
setAFK(afk = true, shardId) { setAFK(afk = true, shardId) {
return this.setPresence({ afk, shardId }); return this.setPresence({ afk, shardId });

View File

@@ -27,7 +27,6 @@ const RoleManager = require('../managers/RoleManager');
const StageInstanceManager = require('../managers/StageInstanceManager'); const StageInstanceManager = require('../managers/StageInstanceManager');
const VoiceStateManager = require('../managers/VoiceStateManager'); const VoiceStateManager = require('../managers/VoiceStateManager');
const { resolveImage } = require('../util/DataResolver'); const { resolveImage } = require('../util/DataResolver');
const Status = require('../util/Status');
const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField');
const { discordSort, getSortableGroupTypes, resolvePartialEmoji } = require('../util/Util'); const { discordSort, getSortableGroupTypes, resolvePartialEmoji } = require('../util/Util');
@@ -126,15 +125,6 @@ class Guild extends AnonymousGuild {
this.shardId = data.shardId; this.shardId = data.shardId;
} }
/**
* The Shard this Guild belongs to.
* @type {WebSocketShard}
* @readonly
*/
get shard() {
return this.client.ws.shards.get(this.shardId);
}
_patch(data) { _patch(data) {
super._patch(data); super._patch(data);
this.id = data.id; this.id = data.id;
@@ -1418,8 +1408,7 @@ class Guild extends AnonymousGuild {
this.client.voice.adapters.set(this.id, methods); this.client.voice.adapters.set(this.id, methods);
return { return {
sendPayload: data => { sendPayload: data => {
if (this.shard.status !== Status.Ready) return false; this.client.ws.send(this.shardId, data);
this.shard.send(data);
return true; return true;
}, },
destroy: () => { destroy: () => {

View File

@@ -325,6 +325,10 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayDispatchEvents} * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayDispatchEvents}
*/ */
/**
* @external GatewayDispatchPayload
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#GatewayDispatchPayload}
*/
/** /**
* @external GatewayIntentBits * @external GatewayIntentBits
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayIntentBits} * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayIntentBits}

View File

@@ -61,11 +61,6 @@
* @property {string} MessageReactionRemoveEmoji messageReactionRemoveEmoji * @property {string} MessageReactionRemoveEmoji messageReactionRemoveEmoji
* @property {string} MessageUpdate messageUpdate * @property {string} MessageUpdate messageUpdate
* @property {string} PresenceUpdate presenceUpdate * @property {string} PresenceUpdate presenceUpdate
* @property {string} ShardDisconnect shardDisconnect
* @property {string} ShardError shardError
* @property {string} ShardReady shardReady
* @property {string} ShardReconnecting shardReconnecting
* @property {string} ShardResume shardResume
* @property {string} StageInstanceCreate stageInstanceCreate * @property {string} StageInstanceCreate stageInstanceCreate
* @property {string} StageInstanceDelete stageInstanceDelete * @property {string} StageInstanceDelete stageInstanceDelete
* @property {string} StageInstanceUpdate stageInstanceUpdate * @property {string} StageInstanceUpdate stageInstanceUpdate
@@ -99,7 +94,7 @@ module.exports = {
ChannelDelete: 'channelDelete', ChannelDelete: 'channelDelete',
ChannelPinsUpdate: 'channelPinsUpdate', ChannelPinsUpdate: 'channelPinsUpdate',
ChannelUpdate: 'channelUpdate', ChannelUpdate: 'channelUpdate',
ClientReady: 'ready', ClientReady: 'clientReady',
Debug: 'debug', Debug: 'debug',
EntitlementCreate: 'entitlementCreate', EntitlementCreate: 'entitlementCreate',
EntitlementUpdate: 'entitlementUpdate', EntitlementUpdate: 'entitlementUpdate',
@@ -148,12 +143,6 @@ module.exports = {
MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji', MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji',
MessageUpdate: 'messageUpdate', MessageUpdate: 'messageUpdate',
PresenceUpdate: 'presenceUpdate', PresenceUpdate: 'presenceUpdate',
Raw: 'raw',
ShardDisconnect: 'shardDisconnect',
ShardError: 'shardError',
ShardReady: 'shardReady',
ShardReconnecting: 'shardReconnecting',
ShardResume: 'shardResume',
StageInstanceCreate: 'stageInstanceCreate', StageInstanceCreate: 'stageInstanceCreate',
StageInstanceDelete: 'stageInstanceDelete', StageInstanceDelete: 'stageInstanceDelete',
StageInstanceUpdate: 'stageInstanceUpdate', StageInstanceUpdate: 'stageInstanceUpdate',

View File

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
const { DefaultRestOptions, DefaultUserAgentAppendix } = require('@discordjs/rest'); const { DefaultRestOptions, DefaultUserAgentAppendix } = require('@discordjs/rest');
const { DefaultWebSocketManagerOptions } = require('@discordjs/ws');
const { toSnakeCase } = require('./Transformers'); const { toSnakeCase } = require('./Transformers');
const { version } = require('../../package.json'); const { version } = require('../../package.json');
@@ -16,13 +17,8 @@ const { version } = require('../../package.json');
/** /**
* Options for a client. * Options for a client.
* @typedef {Object} ClientOptions * @typedef {Object} ClientOptions
* @property {number|number[]|string} [shards] The shard's id to run, or an array of shard ids. If not specified,
* the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the
* recommended amount of shards from Discord and spawn that amount
* @property {number} [closeTimeout=5_000] The amount of time in milliseconds to wait for the close frame to be received * @property {number} [closeTimeout=5_000] The amount of time in milliseconds to wait for the close frame to be received
* from the WebSocket. Don't have this too high/low. It's best to have it between 2_000-6_000 ms. * from the WebSocket. Don't have this too high/low. It's best to have it between 2_000-6_000 ms.
* @property {number} [shardCount=1] The total amount of shards used by all processes of this bot
* (e.g. recommended shard count, shard count of the ShardingManager)
* @property {CacheFactory} [makeCache] Function to create a cache. * @property {CacheFactory} [makeCache] Function to create a cache.
* You can use your own function, or the {@link Options} class to customize the Collection used for the cache. * You can use your own function, or the {@link Options} class to customize the Collection used for the cache.
* <warn>Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`, * <warn>Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`,
@@ -33,12 +29,12 @@ const { version } = require('../../package.json');
* [guide](https://discordjs.guide/popular-topics/partials.html) for some * [guide](https://discordjs.guide/popular-topics/partials.html) for some
* important usage information, as partials require you to put checks in place when handling data. * important usage information, as partials require you to put checks in place when handling data.
* @property {boolean} [failIfNotExists=true] The default value for {@link MessageReplyOptions#failIfNotExists} * @property {boolean} [failIfNotExists=true] The default value for {@link MessageReplyOptions#failIfNotExists}
* @property {PresenceData} [presence={}] Presence data to use upon login * @property {PresenceData} [presence] Presence data to use upon login
* @property {IntentsResolvable} intents Intents to enable for this connection * @property {IntentsResolvable} intents Intents to enable for this connection
* @property {number} [waitGuildTimeout=15_000] Time in milliseconds that clients with the * @property {number} [waitGuildTimeout=15_000] Time in milliseconds that clients with the
* {@link GatewayIntentBits.Guilds} gateway intent should wait for missing guilds to be received before being ready. * {@link GatewayIntentBits.Guilds} gateway intent should wait for missing guilds to be received before being ready.
* @property {SweeperOptions} [sweepers=this.DefaultSweeperSettings] Options for cache sweeping * @property {SweeperOptions} [sweepers=this.DefaultSweeperSettings] Options for cache sweeping
* @property {WebsocketOptions} [ws] Options for the WebSocket * @property {WebSocketManagerOptions} [ws] Options for the WebSocketManager
* @property {RESTOptions} [rest] Options for the REST manager * @property {RESTOptions} [rest] Options for the REST manager
* @property {Function} [jsonTransformer] A function used to transform outgoing json data * @property {Function} [jsonTransformer] A function used to transform outgoing json data
* @property {boolean} [enforceNonce=false] The default value for {@link MessageReplyOptions#enforceNonce} * @property {boolean} [enforceNonce=false] The default value for {@link MessageReplyOptions#enforceNonce}
@@ -60,40 +56,6 @@ const { version } = require('../../package.json');
* <info>This property is optional when the key is `invites`, `messages`, or `threads` and `lifetime` is set</info> * <info>This property is optional when the key is `invites`, `messages`, or `threads` and `lifetime` is set</info>
*/ */
/**
* A function to determine what strategy to use for sharding internally.
* ```js
* (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 })
* ```
* @typedef {Function} BuildStrategyFunction
* @param {WSWebSocketManager} manager The WebSocketManager that is going to initiate the sharding
* @returns {IShardingStrategy} The strategy to use for sharding
*/
/**
* A function to change the concurrency handling for shard identifies of this manager
* ```js
* async (manager) => {
* const gateway = await manager.fetchGatewayInformation();
* return new SimpleIdentifyThrottler(gateway.session_start_limit.max_concurrency);
* }
* ```
* @typedef {Function} IdentifyThrottlerFunction
* @param {WSWebSocketManager} manager The WebSocketManager that is going to initiate the sharding
* @returns {Awaitable<IIdentifyThrottler>} The identify throttler that this ws manager will use
*/
/**
* WebSocket options (these are left as snake_case to match the API)
* @typedef {Object} WebsocketOptions
* @property {number} [large_threshold=50] Number of members in a guild after which offline users will no longer be
* sent in the initial guild member list, must be between 50 and 250
* @property {number} [version=10] The Discord gateway version to use <warn>Changing this can break the library;
* only set this if you know what you are doing</warn>
* @property {BuildStrategyFunction} [buildStrategy] Builds the strategy to use for sharding
* @property {IdentifyThrottlerFunction} [buildIdentifyThrottler] Builds the identify throttler to use for sharding
*/
/** /**
* Contains various utilities for client options. * Contains various utilities for client options.
*/ */
@@ -114,15 +76,14 @@ class Options extends null {
return { return {
closeTimeout: 5_000, closeTimeout: 5_000,
waitGuildTimeout: 15_000, waitGuildTimeout: 15_000,
shardCount: 1,
makeCache: this.cacheWithLimits(this.DefaultMakeCacheSettings), makeCache: this.cacheWithLimits(this.DefaultMakeCacheSettings),
partials: [], partials: [],
failIfNotExists: true, failIfNotExists: true,
enforceNonce: false, enforceNonce: false,
presence: {},
sweepers: this.DefaultSweeperSettings, sweepers: this.DefaultSweeperSettings,
ws: { ws: {
large_threshold: 50, ...DefaultWebSocketManagerOptions,
largeThreshold: 50,
version: 10, version: 10,
}, },
rest: { rest: {
@@ -224,7 +185,7 @@ module.exports = Options;
*/ */
/** /**
* @external WSWebSocketManager * @external WebSocketManager
* @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class} * @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class}
*/ */

View File

@@ -5,14 +5,8 @@ const { createEnum } = require('./Enums');
/** /**
* @typedef {Object} Status * @typedef {Object} Status
* @property {number} Ready * @property {number} Ready
* @property {number} Connecting
* @property {number} Reconnecting
* @property {number} Idle * @property {number} Idle
* @property {number} Nearly
* @property {number} Disconnected
* @property {number} WaitingForGuilds * @property {number} WaitingForGuilds
* @property {number} Identifying
* @property {number} Resuming
*/ */
// JSDoc for IntelliSense purposes // JSDoc for IntelliSense purposes
@@ -20,14 +14,4 @@ const { createEnum } = require('./Enums');
* @type {Status} * @type {Status}
* @ignore * @ignore
*/ */
module.exports = createEnum([ module.exports = createEnum(['Ready', 'Idle', 'WaitingForGuilds']);
'Ready',
'Connecting',
'Reconnecting',
'Idle',
'Nearly',
'Disconnected',
'WaitingForGuilds',
'Identifying',
'Resuming',
]);

View File

@@ -1,25 +0,0 @@
'use strict';
/**
* @typedef {Object} WebSocketShardEvents
* @property {string} Close close
* @property {string} Destroyed destroyed
* @property {string} InvalidSession invalidSession
* @property {string} Ready ready
* @property {string} Resumed resumed
* @property {string} AllReady allReady
*/
// JSDoc for IntelliSense purposes
/**
* @type {WebSocketShardEvents}
* @ignore
*/
module.exports = {
Close: 'close',
Destroyed: 'destroyed',
InvalidSession: 'invalidSession',
Ready: 'ready',
Resumed: 'resumed',
AllReady: 'allReady',
};

View File

@@ -20,12 +20,7 @@ import {
import { Awaitable, JSONEncodable } from '@discordjs/util'; import { Awaitable, JSONEncodable } from '@discordjs/util';
import { Collection, ReadonlyCollection } from '@discordjs/collection'; import { Collection, ReadonlyCollection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
import { import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws';
WebSocketManager as WSWebSocketManager,
IShardingStrategy,
IIdentifyThrottler,
SessionInfo,
} from '@discordjs/ws';
import { import {
APIActionRowComponent, APIActionRowComponent,
APIApplicationCommandInteractionData, APIApplicationCommandInteractionData,
@@ -50,7 +45,6 @@ import {
ButtonStyle, ButtonStyle,
ChannelType, ChannelType,
ComponentType, ComponentType,
GatewayDispatchEvents,
GatewayVoiceServerUpdateDispatchData, GatewayVoiceServerUpdateDispatchData,
GatewayVoiceStateUpdateDispatchData, GatewayVoiceStateUpdateDispatchData,
GuildFeature, GuildFeature,
@@ -170,6 +164,8 @@ import {
GuildScheduledEventRecurrenceRuleWeekday, GuildScheduledEventRecurrenceRuleWeekday,
GuildScheduledEventRecurrenceRuleMonth, GuildScheduledEventRecurrenceRuleMonth,
GuildScheduledEventRecurrenceRuleFrequency, GuildScheduledEventRecurrenceRuleFrequency,
GatewaySendPayload,
GatewayDispatchPayload,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import { ChildProcess } from 'node:child_process'; import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
@@ -956,8 +952,16 @@ export type If<Value extends boolean, TrueResult, FalseResult = null> = Value ex
export class Client<Ready extends boolean = boolean> extends BaseClient { export class Client<Ready extends boolean = boolean> extends BaseClient {
public constructor(options: ClientOptions); public constructor(options: ClientOptions);
private actions: unknown; private actions: unknown;
private expectedGuilds: Set<Snowflake>;
private readonly packetQueue: unknown[];
private presence: ClientPresence; private presence: ClientPresence;
private pings: Collection<number, number>;
private readyTimeout: NodeJS.Timeout | null;
private _broadcast(packet: GatewaySendPayload): void;
private _eval(script: string): unknown; private _eval(script: string): unknown;
private _handlePacket(packet?: GatewayDispatchPayload, shardId?: number): boolean;
private _checkReady(): void;
private _triggerClientReady(): void;
private _validateOptions(options: ClientOptions): void; private _validateOptions(options: ClientOptions): void;
private get _censoredToken(): string | null; private get _censoredToken(): string | null;
// This a technique used to brand the ready state. Or else we'll get `never` errors on typeguard checks. // This a technique used to brand the ready state. Or else we'll get `never` errors on typeguard checks.
@@ -979,17 +983,21 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
public channels: ChannelManager; public channels: ChannelManager;
public get emojis(): BaseGuildEmojiManager; public get emojis(): BaseGuildEmojiManager;
public guilds: GuildManager; public guilds: GuildManager;
public lastPingTimestamp: number;
public options: Omit<ClientOptions, 'intents'> & { intents: IntentsBitField }; public options: Omit<ClientOptions, 'intents'> & { intents: IntentsBitField };
public get ping(): number;
public get readyAt(): If<Ready, Date>; public get readyAt(): If<Ready, Date>;
public readyTimestamp: If<Ready, number>; public readyTimestamp: If<Ready, number>;
public sweepers: Sweepers; public sweepers: Sweepers;
public shard: ShardClientUtil | null; public shard: ShardClientUtil | null;
public status: Status;
public token: If<Ready, string, string | null>; public token: If<Ready, string, string | null>;
public get uptime(): If<Ready, number>; public get uptime(): If<Ready, number>;
public user: If<Ready, ClientUser>; public user: If<Ready, ClientUser>;
public users: UserManager; public users: UserManager;
public voice: ClientVoiceManager; public voice: ClientVoiceManager;
public ws: WebSocketManager; public ws: WebSocketManager;
public destroy(): Promise<void>; public destroy(): Promise<void>;
public deleteWebhook(id: Snowflake, options?: WebhookDeleteOptions): Promise<void>; public deleteWebhook(id: Snowflake, options?: WebhookDeleteOptions): Promise<void>;
public fetchGuildPreview(guild: GuildResolvable): Promise<GuildPreview>; public fetchGuildPreview(guild: GuildResolvable): Promise<GuildPreview>;
@@ -1431,7 +1439,6 @@ export class Guild extends AnonymousGuild {
public get safetyAlertsChannel(): TextChannel | null; public get safetyAlertsChannel(): TextChannel | null;
public safetyAlertsChannelId: Snowflake | null; public safetyAlertsChannelId: Snowflake | null;
public scheduledEvents: GuildScheduledEventManager; public scheduledEvents: GuildScheduledEventManager;
public get shard(): WebSocketShard;
public shardId: number; public shardId: number;
public stageInstances: StageInstanceManager; public stageInstances: StageInstanceManager;
public stickers: GuildStickerManager; public stickers: GuildStickerManager;
@@ -3632,70 +3639,6 @@ export class WebhookClient extends BaseClient {
public send(options: string | MessagePayload | WebhookMessageCreateOptions): Promise<APIMessage>; public send(options: string | MessagePayload | WebhookMessageCreateOptions): Promise<APIMessage>;
} }
export class WebSocketManager extends EventEmitter {
private constructor(client: Client);
private readonly packetQueue: unknown[];
private destroyed: boolean;
public readonly client: Client;
public gateway: string | null;
public shards: Collection<number, WebSocketShard>;
public status: Status;
public get ping(): number;
public on(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this;
public once(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this;
private debug(messages: readonly string[], shardId?: number): void;
private connect(): Promise<void>;
private broadcast(packet: unknown): void;
private destroy(): Promise<void>;
private handlePacket(packet?: unknown, shard?: WebSocketShard): boolean;
private checkShardsReady(): void;
private triggerClientReady(): void;
}
export interface WebSocketShardEventTypes {
ready: [];
resumed: [];
invalidSession: [];
destroyed: [];
close: [event: CloseEvent];
allReady: [unavailableGuilds?: Set<Snowflake>];
}
export class WebSocketShard extends EventEmitter {
private constructor(manager: WebSocketManager, id: number);
private closeSequence: number;
private sessionInfo: SessionInfo | null;
public lastPingTimestamp: number;
private expectedGuilds: Set<Snowflake> | null;
private readyTimeout: NodeJS.Timeout | null;
public manager: WebSocketManager;
public id: number;
public status: Status;
public ping: number;
private debug(messages: readonly string[]): void;
private onReadyPacket(packet: unknown): void;
private gotGuild(guildId: Snowflake): void;
private checkReady(): void;
private emitClose(event?: CloseEvent): void;
public send(data: unknown, important?: boolean): void;
public on<Event extends keyof WebSocketShardEventTypes>(
event: Event,
listener: (...args: WebSocketShardEventTypes[Event]) => void,
): this;
public once<Event extends keyof WebSocketShardEventTypes>(
event: Event,
listener: (...args: WebSocketShardEventTypes[Event]) => void,
): this;
}
export class Widget extends Base { export class Widget extends Base {
private constructor(client: Client<true>, data: RawWidgetData); private constructor(client: Client<true>, data: RawWidgetData);
private _patch(data: RawWidgetData): void; private _patch(data: RawWidgetData): void;
@@ -5133,6 +5076,7 @@ export interface ClientEvents {
oldChannel: DMChannel | NonThreadGuildBasedChannel, oldChannel: DMChannel | NonThreadGuildBasedChannel,
newChannel: DMChannel | NonThreadGuildBasedChannel, newChannel: DMChannel | NonThreadGuildBasedChannel,
]; ];
clientReady: [client: Client<true>];
debug: [message: string]; debug: [message: string];
warn: [message: string]; warn: [message: string];
emojiCreate: [emoji: GuildEmoji]; emojiCreate: [emoji: GuildEmoji];
@@ -5186,7 +5130,6 @@ export interface ClientEvents {
newMessage: OmitPartialGroupDMChannel<Message>, newMessage: OmitPartialGroupDMChannel<Message>,
]; ];
presenceUpdate: [oldPresence: Presence | null, newPresence: Presence]; presenceUpdate: [oldPresence: Presence | null, newPresence: Presence];
ready: [client: Client<true>];
invalidated: []; invalidated: [];
roleCreate: [role: Role]; roleCreate: [role: Role];
roleDelete: [role: Role]; roleDelete: [role: Role];
@@ -5206,11 +5149,6 @@ export interface ClientEvents {
voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState];
webhooksUpdate: [channel: TextChannel | NewsChannel | VoiceChannel | ForumChannel | MediaChannel]; webhooksUpdate: [channel: TextChannel | NewsChannel | VoiceChannel | ForumChannel | MediaChannel];
interactionCreate: [interaction: Interaction]; interactionCreate: [interaction: Interaction];
shardDisconnect: [closeEvent: CloseEvent, shardId: number];
shardError: [error: Error, shardId: number];
shardReady: [shardId: number, unavailableGuilds: Set<Snowflake> | undefined];
shardReconnecting: [shardId: number];
shardResume: [shardId: number, replayedEvents: number];
stageInstanceCreate: [stageInstance: StageInstance]; stageInstanceCreate: [stageInstance: StageInstance];
stageInstanceUpdate: [oldStageInstance: StageInstance | null, newStageInstance: StageInstance]; stageInstanceUpdate: [oldStageInstance: StageInstance | null, newStageInstance: StageInstance];
stageInstanceDelete: [stageInstance: StageInstance]; stageInstanceDelete: [stageInstance: StageInstance];
@@ -5232,8 +5170,6 @@ export interface ClientFetchInviteOptions {
} }
export interface ClientOptions { export interface ClientOptions {
shards?: number | readonly number[] | 'auto';
shardCount?: number;
closeTimeout?: number; closeTimeout?: number;
makeCache?: CacheFactory; makeCache?: CacheFactory;
allowedMentions?: MessageMentionOptions; allowedMentions?: MessageMentionOptions;
@@ -5243,7 +5179,7 @@ export interface ClientOptions {
intents: BitFieldResolvable<GatewayIntentsString, number>; intents: BitFieldResolvable<GatewayIntentsString, number>;
waitGuildTimeout?: number; waitGuildTimeout?: number;
sweepers?: SweeperOptions; sweepers?: SweeperOptions;
ws?: WebSocketOptions; ws?: Partial<WebSocketManagerOptions>;
rest?: Partial<RESTOptions>; rest?: Partial<RESTOptions>;
jsonTransformer?: (obj: unknown) => unknown; jsonTransformer?: (obj: unknown) => unknown;
enforceNonce?: boolean; enforceNonce?: boolean;
@@ -5263,14 +5199,6 @@ export interface ClientUserEditOptions {
banner?: BufferResolvable | Base64Resolvable | null; banner?: BufferResolvable | Base64Resolvable | null;
} }
export interface CloseEvent {
/** @deprecated Not used anymore since using {@link @discordjs/ws#(WebSocketManager:class)} internally */
wasClean: boolean;
code: number;
/** @deprecated Not used anymore since using {@link @discordjs/ws#(WebSocketManager:class)} internally */
reason: string;
}
export type CollectorFilter<Arguments extends unknown[]> = (...args: Arguments) => Awaitable<boolean>; export type CollectorFilter<Arguments extends unknown[]> = (...args: Arguments) => Awaitable<boolean>;
export interface CollectorOptions<FilterArguments extends unknown[]> { export interface CollectorOptions<FilterArguments extends unknown[]> {
@@ -5364,7 +5292,7 @@ export enum Events {
AutoModerationRuleCreate = 'autoModerationRuleCreate', AutoModerationRuleCreate = 'autoModerationRuleCreate',
AutoModerationRuleDelete = 'autoModerationRuleDelete', AutoModerationRuleDelete = 'autoModerationRuleDelete',
AutoModerationRuleUpdate = 'autoModerationRuleUpdate', AutoModerationRuleUpdate = 'autoModerationRuleUpdate',
ClientReady = 'ready', ClientReady = 'clientReady',
EntitlementCreate = 'entitlementCreate', EntitlementCreate = 'entitlementCreate',
EntitlementDelete = 'entitlementDelete', EntitlementDelete = 'entitlementDelete',
EntitlementUpdate = 'entitlementUpdate', EntitlementUpdate = 'entitlementUpdate',
@@ -5452,15 +5380,6 @@ export enum ShardEvents {
Spawn = 'spawn', Spawn = 'spawn',
} }
export enum WebSocketShardEvents {
Close = 'close',
Destroyed = 'destroyed',
InvalidSession = 'invalidSession',
Ready = 'ready',
Resumed = 'resumed',
AllReady = 'allReady',
}
export enum Status { export enum Status {
Ready = 0, Ready = 0,
Connecting = 1, Connecting = 1,
@@ -6879,13 +6798,6 @@ export interface WebhookMessageCreateOptions extends Omit<MessageCreateOptions,
appliedTags?: readonly Snowflake[]; appliedTags?: readonly Snowflake[];
} }
export interface WebSocketOptions {
large_threshold?: number;
version?: number;
buildStrategy?(manager: WSWebSocketManager): IShardingStrategy;
buildIdentifyThrottler?(manager: WSWebSocketManager): Awaitable<IIdentifyThrottler>;
}
export interface WidgetActivity { export interface WidgetActivity {
name: string; name: string;
} }

View File

@@ -49,7 +49,6 @@ import {
Client, Client,
ClientApplication, ClientApplication,
ClientUser, ClientUser,
CloseEvent,
Collection, Collection,
ChatInputCommandInteraction, ChatInputCommandInteraction,
CommandInteractionOption, CommandInteractionOption,
@@ -100,7 +99,6 @@ import {
User, User,
VoiceChannel, VoiceChannel,
Shard, Shard,
WebSocketShard,
Collector, Collector,
GuildAuditLogsEntry, GuildAuditLogsEntry,
GuildAuditLogs, GuildAuditLogs,
@@ -112,7 +110,6 @@ import {
RepliableInteraction, RepliableInteraction,
ThreadChannelType, ThreadChannelType,
Events, Events,
WebSocketShardEvents,
Status, Status,
CategoryChannelChildManager, CategoryChannelChildManager,
ActionRowData, ActionRowData,
@@ -677,7 +674,7 @@ client.on('presenceUpdate', (oldPresence, { client }) => {
declare const slashCommandBuilder: SlashCommandBuilder; declare const slashCommandBuilder: SlashCommandBuilder;
declare const contextMenuCommandBuilder: ContextMenuCommandBuilder; declare const contextMenuCommandBuilder: ContextMenuCommandBuilder;
client.on('ready', async client => { client.on('clientReady', async client => {
expectType<Client<true>>(client); expectType<Client<true>>(client);
console.log(`Client is logged in as ${client.user.tag} and ready!`); console.log(`Client is logged in as ${client.user.tag} and ready!`);
@@ -1305,8 +1302,8 @@ client.on('guildCreate', async g => {
}); });
// EventEmitter static method overrides // EventEmitter static method overrides
expectType<Promise<[Client<true>]>>(Client.once(client, 'ready')); expectType<Promise<[Client<true>]>>(Client.once(client, 'clientReady'));
expectType<AsyncIterableIterator<[Client<true>]>>(Client.on(client, 'ready')); expectType<AsyncIterableIterator<[Client<true>]>>(Client.on(client, 'clientReady'));
client.login('absolutely-valid-token'); client.login('absolutely-valid-token');
@@ -1426,7 +1423,6 @@ reactionCollector.on('dispose', (...args) => {
// Make sure the properties are typed correctly, and that no backwards properties // Make sure the properties are typed correctly, and that no backwards properties
// (K -> V and V -> K) exist: // (K -> V and V -> K) exist:
expectAssignable<'messageCreate'>(Events.MessageCreate); expectAssignable<'messageCreate'>(Events.MessageCreate);
expectAssignable<'close'>(WebSocketShardEvents.Close);
expectAssignable<'death'>(ShardEvents.Death); expectAssignable<'death'>(ShardEvents.Death);
expectAssignable<1>(Status.Connecting); expectAssignable<1>(Status.Connecting);
@@ -2100,12 +2096,6 @@ shard.on('death', process => {
expectType<ChildProcess | Worker>(process); expectType<ChildProcess | Worker>(process);
}); });
declare const webSocketShard: WebSocketShard;
webSocketShard.on('close', event => {
expectType<CloseEvent>(event);
});
declare const collector: Collector<string, Interaction, string[]>; declare const collector: Collector<string, Interaction, string[]>;
collector.on('collect', (collected, ...other) => { collector.on('collect', (collected, ...other) => {

62
pnpm-lock.yaml generated
View File

@@ -932,8 +932,8 @@ importers:
specifier: workspace:^ specifier: workspace:^
version: link:../util version: link:../util
'@discordjs/ws': '@discordjs/ws':
specifier: 1.1.1 specifier: workspace:^
version: 1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) version: link:../ws
'@sapphire/snowflake': '@sapphire/snowflake':
specifier: 3.5.3 specifier: 3.5.3
version: 3.5.3 version: 3.5.3
@@ -2609,10 +2609,6 @@ packages:
resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==}
engines: {node: '>=16.11.0'} engines: {node: '>=16.11.0'}
'@discordjs/collection@2.1.0':
resolution: {integrity: sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==}
engines: {node: '>=18'}
'@discordjs/formatters@0.5.0': '@discordjs/formatters@0.5.0':
resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==} resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2625,22 +2621,10 @@ packages:
resolution: {integrity: sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==} resolution: {integrity: sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
'@discordjs/rest@2.3.0':
resolution: {integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==}
engines: {node: '>=16.11.0'}
'@discordjs/util@1.1.0':
resolution: {integrity: sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==}
engines: {node: '>=16.11.0'}
'@discordjs/util@1.1.1': '@discordjs/util@1.1.1':
resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@discordjs/ws@1.1.1':
resolution: {integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==}
engines: {node: '>=16.11.0'}
'@edge-runtime/format@2.2.1': '@edge-runtime/format@2.2.1':
resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -7631,9 +7615,6 @@ packages:
discord-api-types@0.37.101: discord-api-types@0.37.101:
resolution: {integrity: sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==} resolution: {integrity: sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==}
discord-api-types@0.37.83:
resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==}
discord-api-types@0.37.97: discord-api-types@0.37.97:
resolution: {integrity: sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==} resolution: {integrity: sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==}
@@ -13035,10 +13016,6 @@ packages:
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
engines: {node: '>=14.0'} engines: {node: '>=14.0'}
undici@6.13.0:
resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==}
engines: {node: '>=18.0'}
undici@6.19.8: undici@6.19.8:
resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==}
engines: {node: '>=18.17'} engines: {node: '>=18.17'}
@@ -14857,8 +14834,6 @@ snapshots:
'@discordjs/collection@1.5.3': {} '@discordjs/collection@1.5.3': {}
'@discordjs/collection@2.1.0': {}
'@discordjs/formatters@0.5.0': '@discordjs/formatters@0.5.0':
dependencies: dependencies:
discord-api-types: 0.37.97 discord-api-types: 0.37.97
@@ -14886,37 +14861,8 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
'@discordjs/rest@2.3.0':
dependencies:
'@discordjs/collection': 2.1.0
'@discordjs/util': 1.1.0
'@sapphire/async-queue': 1.5.3
'@sapphire/snowflake': 3.5.3
'@vladfrangu/async_event_emitter': 2.4.6
discord-api-types: 0.37.83
magic-bytes.js: 1.10.0
tslib: 2.6.3
undici: 6.13.0
'@discordjs/util@1.1.0': {}
'@discordjs/util@1.1.1': {} '@discordjs/util@1.1.1': {}
'@discordjs/ws@1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
dependencies:
'@discordjs/collection': 2.1.0
'@discordjs/rest': 2.3.0
'@discordjs/util': 1.1.0
'@sapphire/async-queue': 1.5.3
'@types/ws': 8.5.12
'@vladfrangu/async_event_emitter': 2.4.6
discord-api-types: 0.37.83
tslib: 2.6.3
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@edge-runtime/format@2.2.1': {} '@edge-runtime/format@2.2.1': {}
'@edge-runtime/node-utils@2.3.0': {} '@edge-runtime/node-utils@2.3.0': {}
@@ -21518,8 +21464,6 @@ snapshots:
discord-api-types@0.37.101: {} discord-api-types@0.37.101: {}
discord-api-types@0.37.83: {}
discord-api-types@0.37.97: {} discord-api-types@0.37.97: {}
dlv@1.1.3: {} dlv@1.1.3: {}
@@ -28791,8 +28735,6 @@ snapshots:
dependencies: dependencies:
'@fastify/busboy': 2.1.1 '@fastify/busboy': 2.1.1
undici@6.13.0: {}
undici@6.19.8: {} undici@6.19.8: {}
unicode-canonical-property-names-ecmascript@2.0.0: {} unicode-canonical-property-names-ecmascript@2.0.0: {}