feat(core): handle request all guild members rate limit (#11251)

* feat(core): handle request all guild members rate limit

* fix: weird import update

* refactor: error class

* refactor: error class again

* refactor: requested changes

* chore: fix dep

* fix: suggested changes

---------

Co-Authored-By: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Denis-Adrian Cristea
2025-11-11 09:14:57 +02:00
committed by Jiralite
parent eeeef2ac50
commit 5c5b545c38
6 changed files with 67 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
import { clearTimeout, setTimeout } from 'node:timers';
import type { REST } from '@discordjs/rest';
import { calculateShardId } from '@discordjs/util';
import { calculateShardId, GatewayRateLimitError } from '@discordjs/util';
import { WebSocketShardEvents } from '@discordjs/ws';
import { DiscordSnowflake } from '@sapphire/snowflake';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
@@ -57,6 +57,7 @@ import {
type GatewayMessageUpdateDispatchData,
type GatewayPresenceUpdateData,
type GatewayPresenceUpdateDispatchData,
type GatewayRateLimitedDispatchData,
type GatewayReadyDispatchData,
type GatewayRequestGuildMembersData,
type GatewayStageInstanceCreateDispatchData,
@@ -150,6 +151,7 @@ export interface MappedEvents {
[GatewayDispatchEvents.MessageReactionRemoveEmoji]: [ToEventProps<GatewayMessageReactionRemoveEmojiDispatchData>];
[GatewayDispatchEvents.MessageUpdate]: [ToEventProps<GatewayMessageUpdateDispatchData>];
[GatewayDispatchEvents.PresenceUpdate]: [ToEventProps<GatewayPresenceUpdateDispatchData>];
[GatewayDispatchEvents.RateLimited]: [ToEventProps<GatewayRateLimitedDispatchData>];
[GatewayDispatchEvents.Ready]: [ToEventProps<GatewayReadyDispatchData>];
[GatewayDispatchEvents.Resumed]: [ToEventProps<never>];
[GatewayDispatchEvents.StageInstanceCreate]: [ToEventProps<GatewayStageInstanceCreateDispatchData>];
@@ -182,6 +184,10 @@ export interface RequestGuildMembersResult {
presences: NonNullable<GatewayGuildMembersChunkDispatchData['presences']>;
}
function createTimer(controller: AbortController, timeout: number) {
return setTimeout(() => controller.abort(), timeout);
}
export class Client extends AsyncEventEmitter<MappedEvents> {
public readonly rest: REST;
@@ -220,13 +226,24 @@ export class Client extends AsyncEventEmitter<MappedEvents> {
const controller = new AbortController();
const createTimer = () =>
setTimeout(() => {
controller.abort();
}, timeout);
let timer: NodeJS.Timeout | undefined = createTimer(controller, timeout);
let timer: NodeJS.Timeout | undefined = createTimer();
const onRatelimit = ({ data }: ToEventProps<GatewayRateLimitedDispatchData>) => {
// We could verify meta.guild_id === options.guild_id as well, but really, the nonce check is enough
if (data.meta.nonce === nonce) {
controller.abort(new GatewayRateLimitError(data, options));
}
};
const cleanup = () => {
if (timer) {
clearTimeout(timer);
}
this.off(GatewayDispatchEvents.RateLimited, onRatelimit);
};
this.on(GatewayDispatchEvents.RateLimited, onRatelimit);
await this.gateway.send(shardId, {
op: GatewayOpcodes.RequestGuildMembers,
// eslint-disable-next-line id-length
@@ -256,22 +273,23 @@ export class Client extends AsyncEventEmitter<MappedEvents> {
chunkCount: data.chunk_count,
};
if (data.chunk_index >= data.chunk_count - 1) {
break;
} else {
timer = createTimer();
}
if (data.chunk_index >= data.chunk_count - 1) break;
// eslint-disable-next-line require-atomic-updates
timer = createTimer(controller, timeout);
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
if (error.cause instanceof GatewayRateLimitError) {
throw error.cause;
}
throw new Error('Request timed out');
}
throw error;
} finally {
if (timer) {
clearTimeout(timer);
}
cleanup();
}
}

View File

@@ -5,6 +5,8 @@ export * from './util/index.js';
export * from 'discord-api-types/v10';
export { GatewayRateLimitError } from '@discordjs/util';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/core#readme | @discordjs/core} version
* that you are currently using.

View File

@@ -61,6 +61,9 @@
},
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",

View File

@@ -0,0 +1,25 @@
import type { GatewayOpcodeRateLimitMetadataMap, GatewayRateLimitedDispatchData } from 'discord-api-types/v10';
/**
* Represents the error thrown when the gateway emits a `RATE_LIMITED` event after a certain request.
*/
export class GatewayRateLimitError extends Error {
public override readonly name = GatewayRateLimitError.name;
public constructor(
/**
* The data associated with the rate limit event
*/
public readonly data: GatewayRateLimitedDispatchData<keyof GatewayOpcodeRateLimitMetadataMap>,
/**
* The payload data that lead to this rate limit
*
* @privateRemarks
* Too complicated to type properly here (i.e. extract the ['data']
* of event payloads that have t = keyof GatewayOpcodeRateLimitMetadataMap)
*/
public readonly payload: unknown,
) {
super(`Request with opcode ${data.opcode} was rate limited. Retry after ${data.retry_after} seconds.`);
}
}

View File

@@ -2,6 +2,7 @@ export * from './types.js';
export * from './functions/index.js';
export * from './JSONEncodable.js';
export * from './Equatable.js';
export * from './gatewayRateLimitError.js';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/util#readme | @discordjs/util} version

4
pnpm-lock.yaml generated
View File

@@ -1270,6 +1270,10 @@ importers:
version: 2.1.9(@edge-runtime/vm@3.2.0)(@types/node@18.19.130)(terser@5.44.1)
packages/util:
dependencies:
discord-api-types:
specifier: ^0.38.33
version: 0.38.33
devDependencies:
'@discordjs/api-extractor':
specifier: workspace:^