diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js index 1f7a9da86127..b156334b00d3 100644 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ b/packages/discord.js/src/client/websocket/WebSocketShard.js @@ -142,6 +142,14 @@ class WebSocketShard extends EventEmitter { */ Object.defineProperty(this, 'helloTimeout', { value: null, writable: true }); + /** + * The RESUMED dispatch timeout + * @name WebSocketShard#resumedDispatchTimeout + * @type {?NodeJS.Timeout} + * @private + */ + Object.defineProperty(this, 'resumedDispatchTimeout', { value: null, writable: true }); + /** * The WebSocket timeout. * @name WebSocketShard#wsCloseTimeout @@ -174,6 +182,14 @@ class WebSocketShard extends EventEmitter { */ Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); + /** + * The READY dispatch event timeout + * @name WebSocketShard#readyDispatchTimeout + * @type {?NodeJS.Timeout} + * @private + */ + Object.defineProperty(this, 'readyDispatchTimeout', { value: null, writable: true }); + /** * Time when the WebSocket connection was opened * @name WebSocketShard#connectedAt @@ -181,6 +197,14 @@ class WebSocketShard extends EventEmitter { * @private */ Object.defineProperty(this, 'connectedAt', { value: 0, writable: true }); + + /** + * Time when the last replayed event was received + * @name WebSocketShard#lastReplayedAt + * @type {number} + * @private + */ + Object.defineProperty(this, 'lastReplayedAt', { value: 0, writable: true }); } /** @@ -428,6 +452,7 @@ class WebSocketShard extends EventEmitter { this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); this.status = Status.WaitingForGuilds; this.debug(`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`); + this.setReadyDispatchTimeout(-1); this.lastHeartbeatAcked = true; this.sendHeartbeat('ReadyHeartbeat'); break; @@ -441,10 +466,15 @@ class WebSocketShard extends EventEmitter { this.status = Status.Ready; const replayed = packet.s - this.closeSequence; this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`); + this.setResumedDispatchTimeout(-1); this.lastHeartbeatAcked = true; this.sendHeartbeat('ResumeHeartbeat'); break; } + default: { + if (this.status === Status.Resuming) this.lastReplayedAt = Date.now(); + break; + } } if (packet.s > this.sequence) this.sequence = packet.s; @@ -461,6 +491,12 @@ class WebSocketShard extends EventEmitter { break; case GatewayOpcodes.InvalidSession: this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); + + // Clear the timers + this.setReadyDispatchTimeout(-1); + this.setResumedDispatchTimeout(-1); + clearTimeout(this.readyTimeout); + // If we can resume the session, do so immediately if (packet.d) { this.identifyResume(); @@ -565,7 +601,54 @@ class WebSocketShard extends EventEmitter { this.helloTimeout = setTimeout(() => { this.debug('Did not receive HELLO in time. Destroying and connecting again.'); this.destroy({ reset: true, closeCode: 4009 }); - }, 20_000).unref(); + }, 20_000); + } + + /** + * Sets the RESUMED dispatch timeout. + * @param {number} [time] If set to -1, it will clear the resumed timeout + * @private + */ + setResumedDispatchTimeout(time) { + if (time === -1) { + if (this.resumedDispatchTimeout) { + this.debug('Clearing the RESUMED dispatch timeout.'); + clearTimeout(this.resumedDispatchTimeout); + this.resumedDispatchTimeout = null; + } + return; + } + this.debug('Setting a RESUMED dispatch timeout for 20s.'); + this.resumedDispatchTimeout = setTimeout(() => { + if ((Date.now() - this.lastReplayedAt) < 20_000) { + this.debug('Received a message within the last 20s. Delaying RESUMED timeout.'); + this.setResumedDispatchTimeout(); + return + } + this.debug('Did not receive RESUMED in time. Destroying and connecting again.'); + this.destroy({ reset: false, closeCode: 4009 }); + }, 20_000); + } + + /** + * Sets the READY dispatch timeout. + * @param {number} [time] If set to -1, it will clear the ready timeout + * @private + */ + setReadyDispatchTimeout(time) { + if (time === -1) { + if (this.readyDispatchTimeout) { + this.debug('Clearing the READY dispatch timeout.'); + clearTimeout(this.readyDispatchTimeout); + this.readyDispatchTimeout = null; + } + return; + } + this.debug('Setting a READY dispatch timeout for 20s.'); + this.readyDispatchTimeout = setTimeout(() => { + this.debug('Did not receive READY in time. Destroying and connecting again.'); + this.destroy({ reset: true, closeCode: 4009 }); + }, 20_000); } /** @@ -605,7 +688,7 @@ class WebSocketShard extends EventEmitter { this.emitClose(); // Setting the variable false to check for zombie connections. this.closeEmitted = false; - }, time).unref(); + }, time); } /** @@ -702,6 +785,8 @@ class WebSocketShard extends EventEmitter { this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: ${d.intents}`); this.send({ op: GatewayOpcodes.Identify, d }, true); + + this.setReadyDispatchTimeout(); } /** @@ -726,6 +811,8 @@ class WebSocketShard extends EventEmitter { }; this.send({ op: GatewayOpcodes.Resume, d }, true); + + this.setResumedDispatchTimeout(); } /** @@ -802,6 +889,9 @@ class WebSocketShard extends EventEmitter { // Step 0: Remove all timers this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); + this.setReadyDispatchTimeout(-1); + this.setResumedDispatchTimeout(-1); + clearTimeout(this.readyTimeout); this.debug( `[WebSocket] Destroy: Attempting to close the WebSocket. | WS State: ${ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 211c15268494..b8ca015f8070 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3277,11 +3277,14 @@ export class WebSocketShard extends EventEmitter { }; private connection: WebSocket | null; private helloTimeout: NodeJS.Timeout | null; + private resumedDispatchTimeout: NodeJS.Timeout | null; private eventsAttached: boolean; private expectedGuilds: Set | null; private readyTimeout: NodeJS.Timeout | null; + private readyDispatchTimeout: NodeJS.Timeout | null; private closeEmitted: boolean; private wsCloseTimeout: NodeJS.Timeout | null; + private lastReplayedAt: number; public manager: WebSocketManager; public id: number; @@ -3297,6 +3300,8 @@ export class WebSocketShard extends EventEmitter { private onPacket(packet: unknown): void; private checkReady(): void; private setHelloTimeout(time?: number): void; + private setResumedDispatchTimeout(time?: number): void; + private setReadyDispatchTimeout(time?: number): void; private setWsCloseTimeout(time?: number): void; private setHeartbeatTimer(time: number): void; private sendHeartbeat(): void;