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/rest": "workspace:^",
"@discordjs/util": "workspace:^",
"@discordjs/ws": "1.1.1",
"@discordjs/ws": "workspace:^",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.37.101",
"fast-deep-equal": "3.1.3",

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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')],
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')],
['READY', require('./READY')],
['RESUMED', require('./RESUMED')],
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
'use strict';
const { DefaultRestOptions, DefaultUserAgentAppendix } = require('@discordjs/rest');
const { DefaultWebSocketManagerOptions } = require('@discordjs/ws');
const { toSnakeCase } = require('./Transformers');
const { version } = require('../../package.json');
@@ -16,13 +17,8 @@ const { version } = require('../../package.json');
/**
* Options for a client.
* @typedef {Object} ClientOptions
* @property {number|number[]|string} [shards] The shard's id to run, or an array of shard ids. If not specified,
* the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the
* recommended amount of shards from Discord and spawn that amount
* @property {number} [closeTimeout=5_000] The amount of time in milliseconds to wait for the close frame to be received
* from the WebSocket. Don't have this too high/low. It's best to have it between 2_000-6_000 ms.
* @property {number} [shardCount=1] The total amount of shards used by all processes of this bot
* (e.g. recommended shard count, shard count of the ShardingManager)
* @property {CacheFactory} [makeCache] Function to create a cache.
* You can use your own function, or the {@link Options} class to customize the Collection used for the cache.
* <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
* important usage information, as partials require you to put checks in place when handling data.
* @property {boolean} [failIfNotExists=true] The default value for {@link MessageReplyOptions#failIfNotExists}
* @property {PresenceData} [presence={}] Presence data to use upon login
* @property {PresenceData} [presence] Presence data to use upon login
* @property {IntentsResolvable} intents Intents to enable for this connection
* @property {number} [waitGuildTimeout=15_000] Time in milliseconds that clients with the
* {@link GatewayIntentBits.Guilds} gateway intent should wait for missing guilds to be received before being ready.
* @property {SweeperOptions} [sweepers=this.DefaultSweeperSettings] Options for cache sweeping
* @property {WebsocketOptions} [ws] Options for the WebSocket
* @property {WebSocketManagerOptions} [ws] Options for the WebSocketManager
* @property {RESTOptions} [rest] Options for the REST manager
* @property {Function} [jsonTransformer] A function used to transform outgoing json data
* @property {boolean} [enforceNonce=false] The default value for {@link MessageReplyOptions#enforceNonce}
@@ -60,40 +56,6 @@ const { version } = require('../../package.json');
* <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.
*/
@@ -114,15 +76,14 @@ class Options extends null {
return {
closeTimeout: 5_000,
waitGuildTimeout: 15_000,
shardCount: 1,
makeCache: this.cacheWithLimits(this.DefaultMakeCacheSettings),
partials: [],
failIfNotExists: true,
enforceNonce: false,
presence: {},
sweepers: this.DefaultSweeperSettings,
ws: {
large_threshold: 50,
...DefaultWebSocketManagerOptions,
largeThreshold: 50,
version: 10,
},
rest: {
@@ -224,7 +185,7 @@ module.exports = Options;
*/
/**
* @external WSWebSocketManager
* @external WebSocketManager
* @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class}
*/

View File

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

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

View File

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

62
pnpm-lock.yaml generated
View File

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