|
1 | 1 | import { clearTimeout, setTimeout } from 'node:timers'; |
2 | 2 | import type { REST } from '@discordjs/rest'; |
3 | | -import { calculateShardId } from '@discordjs/util'; |
| 3 | +import { calculateShardId, GatewayRateLimitError } from '@discordjs/util'; |
4 | 4 | import { WebSocketShardEvents } from '@discordjs/ws'; |
5 | 5 | import { DiscordSnowflake } from '@sapphire/snowflake'; |
6 | 6 | import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; |
@@ -57,6 +57,7 @@ import { |
57 | 57 | type GatewayMessageUpdateDispatchData, |
58 | 58 | type GatewayPresenceUpdateData, |
59 | 59 | type GatewayPresenceUpdateDispatchData, |
| 60 | + type GatewayRateLimitedDispatchData, |
60 | 61 | type GatewayReadyDispatchData, |
61 | 62 | type GatewayRequestGuildMembersData, |
62 | 63 | type GatewayStageInstanceCreateDispatchData, |
@@ -150,6 +151,7 @@ export interface MappedEvents { |
150 | 151 | [GatewayDispatchEvents.MessageReactionRemoveEmoji]: [ToEventProps<GatewayMessageReactionRemoveEmojiDispatchData>]; |
151 | 152 | [GatewayDispatchEvents.MessageUpdate]: [ToEventProps<GatewayMessageUpdateDispatchData>]; |
152 | 153 | [GatewayDispatchEvents.PresenceUpdate]: [ToEventProps<GatewayPresenceUpdateDispatchData>]; |
| 154 | + [GatewayDispatchEvents.RateLimited]: [ToEventProps<GatewayRateLimitedDispatchData>]; |
153 | 155 | [GatewayDispatchEvents.Ready]: [ToEventProps<GatewayReadyDispatchData>]; |
154 | 156 | [GatewayDispatchEvents.Resumed]: [ToEventProps<never>]; |
155 | 157 | [GatewayDispatchEvents.StageInstanceCreate]: [ToEventProps<GatewayStageInstanceCreateDispatchData>]; |
@@ -182,6 +184,10 @@ export interface RequestGuildMembersResult { |
182 | 184 | presences: NonNullable<GatewayGuildMembersChunkDispatchData['presences']>; |
183 | 185 | } |
184 | 186 |
|
| 187 | +function createTimer(controller: AbortController, timeout: number) { |
| 188 | + return setTimeout(() => controller.abort(), timeout); |
| 189 | +} |
| 190 | + |
185 | 191 | export class Client extends AsyncEventEmitter<MappedEvents> { |
186 | 192 | public readonly rest: REST; |
187 | 193 |
|
@@ -220,13 +226,24 @@ export class Client extends AsyncEventEmitter<MappedEvents> { |
220 | 226 |
|
221 | 227 | const controller = new AbortController(); |
222 | 228 |
|
223 | | - const createTimer = () => |
224 | | - setTimeout(() => { |
225 | | - controller.abort(); |
226 | | - }, timeout); |
| 229 | + let timer: NodeJS.Timeout | undefined = createTimer(controller, timeout); |
227 | 230 |
|
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 | + }; |
229 | 237 |
|
| 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); |
230 | 247 | await this.gateway.send(shardId, { |
231 | 248 | op: GatewayOpcodes.RequestGuildMembers, |
232 | 249 | // eslint-disable-next-line id-length |
@@ -256,22 +273,23 @@ export class Client extends AsyncEventEmitter<MappedEvents> { |
256 | 273 | chunkCount: data.chunk_count, |
257 | 274 | }; |
258 | 275 |
|
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); |
264 | 280 | } |
265 | 281 | } catch (error) { |
266 | 282 | if (error instanceof Error && error.name === 'AbortError') { |
| 283 | + if (error.cause instanceof GatewayRateLimitError) { |
| 284 | + throw error.cause; |
| 285 | + } |
| 286 | + |
267 | 287 | throw new Error('Request timed out'); |
268 | 288 | } |
269 | 289 |
|
270 | 290 | throw error; |
271 | 291 | } finally { |
272 | | - if (timer) { |
273 | | - clearTimeout(timer); |
274 | | - } |
| 292 | + cleanup(); |
275 | 293 | } |
276 | 294 | } |
277 | 295 |
|
|
0 commit comments