fix(gateway): handle reconnects better way

This commit is contained in:
DjDeveloperr 2020-12-04 11:23:10 +05:30
parent 1ca8b4bc67
commit dfb99fb6b9
5 changed files with 67 additions and 32 deletions

11
.vscode/settings.json vendored
View File

@ -1,11 +0,0 @@
{
"deno.enable": true,
"deno.lint": false,
"deno.unstable": false,
"deepscan.enable": true,
"deno.import_intellisense_origins": {
"https://deno.land": true
},
"editor.tabSize": 2,
"editor.formatOnSave": true
}

14
mod.ts
View File

@ -55,7 +55,11 @@ export { Invite } from './src/structures/invite.ts'
export * from './src/structures/member.ts' export * from './src/structures/member.ts'
export { Message } from './src/structures/message.ts' export { Message } from './src/structures/message.ts'
export { MessageMentions } from './src/structures/messageMentions.ts' export { MessageMentions } from './src/structures/messageMentions.ts'
export { Presence, ClientPresence } from './src/structures/presence.ts' export {
Presence,
ClientPresence,
ActivityTypes
} from './src/structures/presence.ts'
export { Role } from './src/structures/role.ts' export { Role } from './src/structures/role.ts'
export { Snowflake } from './src/structures/snowflake.ts' export { Snowflake } from './src/structures/snowflake.ts'
export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts' export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts'
@ -67,4 +71,12 @@ export { Intents } from './src/utils/intents.ts'
// export { getBuildInfo } from './src/utils/buildInfo.ts' // export { getBuildInfo } from './src/utils/buildInfo.ts'
export * from './src/utils/permissions.ts' export * from './src/utils/permissions.ts'
export { UserFlagsManager } from './src/utils/userFlags.ts' export { UserFlagsManager } from './src/utils/userFlags.ts'
export type { EveryChannelTypes } from './src/utils/getChannelByType.ts'
export * from './src/utils/bitfield.ts' export * from './src/utils/bitfield.ts'
export type {
ActivityGame,
ClientActivity,
ClientStatus,
StatusType
} from './src/types/presence.ts'
export { ChannelTypes } from './src/types/channel.ts'

View File

@ -24,6 +24,8 @@ export interface RequestMembersOptions {
users?: string[] users?: string[]
} }
export const RECONNECT_REASON = 'harmony-reconnect'
/** /**
* Handles Discord gateway connection. * Handles Discord gateway connection.
* *
@ -43,6 +45,7 @@ class Gateway {
private heartbeatServerResponded = false private heartbeatServerResponded = false
client: Client client: Client
cache: GatewayCache cache: GatewayCache
private timedIdentify: number | null = null
constructor(client: Client, token: string, intents: GatewayIntents[]) { constructor(client: Client, token: string, intents: GatewayIntents[]) {
this.token = token this.token = token
@ -109,10 +112,20 @@ class Gateway {
case GatewayOpcodes.INVALID_SESSION: case GatewayOpcodes.INVALID_SESSION:
// Because we know this gonna be bool // Because we know this gonna be bool
this.debug(`Invalid Session! Identifying with forced new session`) this.debug(
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions `Invalid Session received! Resumeable? ${d === true ? 'Yes' : 'No'}`
// eslint-disable-next-line @typescript-eslint/promise-function-async )
setTimeout(() => this.sendIdentify(true), 3000) if (d !== true) {
this.debug(`Session was invalidated, deleting from cache`)
await this.cache.delete('session_id')
await this.cache.delete('seq')
this.sessionID = undefined
this.sequenceID = undefined
}
this.timedIdentify = setTimeout(async () => {
this.timedIdentify = null
await this.sendIdentify(!(d as boolean))
}, 5000)
break break
case GatewayOpcodes.DISPATCH: { case GatewayOpcodes.DISPATCH: {
@ -151,6 +164,7 @@ class Gateway {
} }
private async onclose(event: CloseEvent): Promise<void> { private async onclose(event: CloseEvent): Promise<void> {
if (event.reason === RECONNECT_REASON) return
this.debug(`Connection Closed with code: ${event.code}`) this.debug(`Connection Closed with code: ${event.code}`)
if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) { if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) {
@ -180,7 +194,7 @@ class Gateway {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect() this.reconnect()
} else if (event.code === GatewayCloseCodes.SHARDING_REQUIRED) { } else if (event.code === GatewayCloseCodes.SHARDING_REQUIRED) {
throw new Error("Couldn't connect. Sharding is requried!") throw new Error("Couldn't connect. Sharding is required!")
} else if (event.code === GatewayCloseCodes.INVALID_API_VERSION) { } else if (event.code === GatewayCloseCodes.INVALID_API_VERSION) {
throw new Error("Invalid API Version was used. This shouldn't happen!") throw new Error("Invalid API Version was used. This shouldn't happen!")
} else if (event.code === GatewayCloseCodes.INVALID_INTENTS) { } else if (event.code === GatewayCloseCodes.INVALID_INTENTS) {
@ -191,9 +205,12 @@ class Gateway {
this.debug( this.debug(
'Unknown Close code, probably connection error. Reconnecting in 5s.' 'Unknown Close code, probably connection error. Reconnecting in 5s.'
) )
if (this.timedIdentify !== null) {
clearTimeout(this.timedIdentify)
this.debug('Timed Identify found. Cleared timeout.')
}
await delay(5000) await delay(5000)
// eslint-disable-next-line @typescript-eslint/no-floating-promises await this.reconnect(true)
this.reconnect()
} }
} }
@ -216,13 +233,14 @@ class Gateway {
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}` `Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
) )
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
if (forceNewSession === undefined || !forceNewSession) { } else this.debug('Skipping /gateway/bot because bot: false')
const sessionIDCached = await this.cache.get('session_id')
if (sessionIDCached !== undefined) { if (forceNewSession === undefined || !forceNewSession) {
this.debug(`Found Cached SessionID: ${sessionIDCached}`) const sessionIDCached = await this.cache.get('session_id')
this.sessionID = sessionIDCached if (sessionIDCached !== undefined) {
return await this.sendResume() this.debug(`Found Cached SessionID: ${sessionIDCached}`)
} this.sessionID = sessionIDCached
return await this.sendResume()
} }
} }
@ -255,6 +273,7 @@ class Gateway {
} }
} }
this.debug('Sending Identify payload...')
this.send({ this.send({
op: GatewayOpcodes.IDENTIFY, op: GatewayOpcodes.IDENTIFY,
d: payload d: payload
@ -262,6 +281,10 @@ class Gateway {
} }
private async sendResume(): Promise<void> { private async sendResume(): Promise<void> {
if (this.sessionID === undefined) {
this.sessionID = await this.cache.get('session_id')
if (this.sessionID === undefined) return await this.sendIdentify()
}
this.debug(`Preparing to resume with Session: ${this.sessionID}`) this.debug(`Preparing to resume with Session: ${this.sessionID}`)
if (this.sequenceID === undefined) { if (this.sequenceID === undefined) {
const cached = await this.cache.get('seq') const cached = await this.cache.get('seq')
@ -276,6 +299,7 @@ class Gateway {
seq: this.sequenceID ?? null seq: this.sequenceID ?? null
} }
} }
this.debug('Sending Resume payload...')
this.send(resumePayload) this.send(resumePayload)
} }
@ -305,13 +329,16 @@ class Gateway {
async reconnect(forceNew?: boolean): Promise<void> { async reconnect(forceNew?: boolean): Promise<void> {
clearInterval(this.heartbeatIntervalID) clearInterval(this.heartbeatIntervalID)
if (forceNew === undefined || !forceNew) if (forceNew === true) {
await this.cache.delete('session_id') await this.cache.delete('session_id')
this.close() await this.cache.delete('seq')
}
this.close(1000, RECONNECT_REASON)
this.initWebsocket() this.initWebsocket()
} }
initWebsocket(): void { initWebsocket(): void {
this.debug('Initializing WebSocket...')
this.websocket = new WebSocket( this.websocket = new WebSocket(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`, `${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
@ -324,8 +351,8 @@ class Gateway {
this.websocket.onerror = this.onerror.bind(this) this.websocket.onerror = this.onerror.bind(this)
} }
close(): void { close(code: number = 1000, reason?: string): void {
this.websocket.close(1000) return this.websocket.close(code, reason)
} }
send(data: GatewayResponse): boolean { send(data: GatewayResponse): boolean {
@ -362,6 +389,7 @@ class Gateway {
if (this.heartbeatServerResponded) { if (this.heartbeatServerResponded) {
this.heartbeatServerResponded = false this.heartbeatServerResponded = false
} else { } else {
this.debug('Found dead connection, reconnecting...')
clearInterval(this.heartbeatIntervalID) clearInterval(this.heartbeatIntervalID)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect() this.reconnect()

View File

@ -11,7 +11,7 @@ import { Guild } from './guild.ts'
import { User } from './user.ts' import { User } from './user.ts'
import { Client } from '../models/client.ts' import { Client } from '../models/client.ts'
enum ActivityTypes { export enum ActivityTypes {
PLAYING = 0, PLAYING = 0,
STREAMING = 1, STREAMING = 1,
LISTENING = 2, LISTENING = 2,

View File

@ -92,4 +92,10 @@ client.on('messageCreate', async (msg: Message) => {
} }
}) })
client.connect(TOKEN, Intents.None) client.connect(TOKEN, Intents.All)
// OLD: Was a way to reproduce reconnect infinite loop
// setTimeout(() => {
// console.log('[DEBUG] Reconnect')
// client.gateway?.reconnect()
// }, 1000 * 4)