Skip to content

Commit 5c5b545

Browse files
didinelekodiakhq[bot]
authored andcommitted
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>
1 parent eeeef2a commit 5c5b545

File tree

6 files changed

+67
-14
lines changed

6 files changed

+67
-14
lines changed

packages/core/src/client.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { clearTimeout, setTimeout } from 'node:timers';
22
import type { REST } from '@discordjs/rest';
3-
import { calculateShardId } from '@discordjs/util';
3+
import { calculateShardId, GatewayRateLimitError } from '@discordjs/util';
44
import { WebSocketShardEvents } from '@discordjs/ws';
55
import { DiscordSnowflake } from '@sapphire/snowflake';
66
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
@@ -57,6 +57,7 @@ import {
5757
type GatewayMessageUpdateDispatchData,
5858
type GatewayPresenceUpdateData,
5959
type GatewayPresenceUpdateDispatchData,
60+
type GatewayRateLimitedDispatchData,
6061
type GatewayReadyDispatchData,
6162
type GatewayRequestGuildMembersData,
6263
type GatewayStageInstanceCreateDispatchData,
@@ -150,6 +151,7 @@ export interface MappedEvents {
150151
[GatewayDispatchEvents.MessageReactionRemoveEmoji]: [ToEventProps<GatewayMessageReactionRemoveEmojiDispatchData>];
151152
[GatewayDispatchEvents.MessageUpdate]: [ToEventProps<GatewayMessageUpdateDispatchData>];
152153
[GatewayDispatchEvents.PresenceUpdate]: [ToEventProps<GatewayPresenceUpdateDispatchData>];
154+
[GatewayDispatchEvents.RateLimited]: [ToEventProps<GatewayRateLimitedDispatchData>];
153155
[GatewayDispatchEvents.Ready]: [ToEventProps<GatewayReadyDispatchData>];
154156
[GatewayDispatchEvents.Resumed]: [ToEventProps<never>];
155157
[GatewayDispatchEvents.StageInstanceCreate]: [ToEventProps<GatewayStageInstanceCreateDispatchData>];
@@ -182,6 +184,10 @@ export interface RequestGuildMembersResult {
182184
presences: NonNullable<GatewayGuildMembersChunkDispatchData['presences']>;
183185
}
184186

187+
function createTimer(controller: AbortController, timeout: number) {
188+
return setTimeout(() => controller.abort(), timeout);
189+
}
190+
185191
export class Client extends AsyncEventEmitter<MappedEvents> {
186192
public readonly rest: REST;
187193

@@ -220,13 +226,24 @@ export class Client extends AsyncEventEmitter<MappedEvents> {
220226

221227
const controller = new AbortController();
222228

223-
const createTimer = () =>
224-
setTimeout(() => {
225-
controller.abort();
226-
}, timeout);
229+
let timer: NodeJS.Timeout | undefined = createTimer(controller, timeout);
227230

228-
let timer: NodeJS.Timeout | undefined = createTimer();
231+
const onRatelimit = ({ data }: ToEventProps<GatewayRateLimitedDispatchData>) => {
232+
// We could verify meta.guild_id === options.guild_id as well, but really, the nonce check is enough
233+
if (data.meta.nonce === nonce) {
234+
controller.abort(new GatewayRateLimitError(data, options));
235+
}
236+
};
229237

238+
const cleanup = () => {
239+
if (timer) {
240+
clearTimeout(timer);
241+
}
242+
243+
this.off(GatewayDispatchEvents.RateLimited, onRatelimit);
244+
};
245+
246+
this.on(GatewayDispatchEvents.RateLimited, onRatelimit);
230247
await this.gateway.send(shardId, {
231248
op: GatewayOpcodes.RequestGuildMembers,
232249
// eslint-disable-next-line id-length
@@ -256,22 +273,23 @@ export class Client extends AsyncEventEmitter<MappedEvents> {
256273
chunkCount: data.chunk_count,
257274
};
258275

259-
if (data.chunk_index >= data.chunk_count - 1) {
260-
break;
261-
} else {
262-
timer = createTimer();
263-
}
276+
if (data.chunk_index >= data.chunk_count - 1) break;
277+
278+
// eslint-disable-next-line require-atomic-updates
279+
timer = createTimer(controller, timeout);
264280
}
265281
} catch (error) {
266282
if (error instanceof Error && error.name === 'AbortError') {
283+
if (error.cause instanceof GatewayRateLimitError) {
284+
throw error.cause;
285+
}
286+
267287
throw new Error('Request timed out');
268288
}
269289

270290
throw error;
271291
} finally {
272-
if (timer) {
273-
clearTimeout(timer);
274-
}
292+
cleanup();
275293
}
276294
}
277295

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export * from './util/index.js';
55

66
export * from 'discord-api-types/v10';
77

8+
export { GatewayRateLimitError } from '@discordjs/util';
9+
810
/**
911
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/core#readme | @discordjs/core} version
1012
* that you are currently using.

packages/util/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
},
6262
"homepage": "https://discord.js.org",
6363
"funding": "https://github.com/discordjs/discord.js?sponsor",
64+
"dependencies": {
65+
"discord-api-types": "^0.38.33"
66+
},
6467
"devDependencies": {
6568
"@discordjs/api-extractor": "workspace:^",
6669
"@discordjs/scripts": "workspace:^",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { GatewayOpcodeRateLimitMetadataMap, GatewayRateLimitedDispatchData } from 'discord-api-types/v10';
2+
3+
/**
4+
* Represents the error thrown when the gateway emits a `RATE_LIMITED` event after a certain request.
5+
*/
6+
export class GatewayRateLimitError extends Error {
7+
public override readonly name = GatewayRateLimitError.name;
8+
9+
public constructor(
10+
/**
11+
* The data associated with the rate limit event
12+
*/
13+
public readonly data: GatewayRateLimitedDispatchData<keyof GatewayOpcodeRateLimitMetadataMap>,
14+
/**
15+
* The payload data that lead to this rate limit
16+
*
17+
* @privateRemarks
18+
* Too complicated to type properly here (i.e. extract the ['data']
19+
* of event payloads that have t = keyof GatewayOpcodeRateLimitMetadataMap)
20+
*/
21+
public readonly payload: unknown,
22+
) {
23+
super(`Request with opcode ${data.opcode} was rate limited. Retry after ${data.retry_after} seconds.`);
24+
}
25+
}

packages/util/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './types.js';
22
export * from './functions/index.js';
33
export * from './JSONEncodable.js';
44
export * from './Equatable.js';
5+
export * from './gatewayRateLimitError.js';
56

67
/**
78
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/util#readme | @discordjs/util} version

pnpm-lock.yaml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)