From bb19b0daef513d1ca04e40525dcd0e99c8295f00 Mon Sep 17 00:00:00 2001 From: H01001000 Date: Thu, 1 Dec 2022 15:03:22 +0800 Subject: [PATCH] fix: shard --- packages/gateway/src/{mod.ts => index.ts} | 0 .../src/shard/calculateSafeRequests.ts | 2 +- packages/gateway/src/shard/createShard.ts | 36 +++++++++--- packages/gateway/src/shard/deps.ts | 1 - packages/gateway/src/shard/handleClose.ts | 2 +- packages/gateway/src/shard/handleMessage.ts | 22 +++---- packages/gateway/src/shard/identify.ts | 6 +- packages/gateway/src/shard/resume.ts | 4 +- .../gateway/src/shard/startHeartbeating.ts | 58 ++++++++++--------- packages/gateway/src/shard/types.ts | 7 +-- .../gateway/src/utils/calculateShardId.ts | 4 +- 11 files changed, 78 insertions(+), 64 deletions(-) rename packages/gateway/src/{mod.ts => index.ts} (100%) delete mode 100644 packages/gateway/src/shard/deps.ts diff --git a/packages/gateway/src/mod.ts b/packages/gateway/src/index.ts similarity index 100% rename from packages/gateway/src/mod.ts rename to packages/gateway/src/index.ts diff --git a/packages/gateway/src/shard/calculateSafeRequests.ts b/packages/gateway/src/shard/calculateSafeRequests.ts index 00f2bc8d7..09184c1f4 100644 --- a/packages/gateway/src/shard/calculateSafeRequests.ts +++ b/packages/gateway/src/shard/calculateSafeRequests.ts @@ -1,6 +1,6 @@ import { Shard } from './types.js' -export function calculateSafeRequests (shard: Shard) { +export function calculateSafeRequests (shard: Shard): number { // * 2 adds extra safety layer for discords OP 1 requests that we need to respond to const safeRequests = shard.maxRequestsPerRateLimitTick - Math.ceil(shard.rateLimitResetInterval / shard.heart.interval) * 2 diff --git a/packages/gateway/src/shard/createShard.ts b/packages/gateway/src/shard/createShard.ts index aede0bf00..6a8264a9f 100644 --- a/packages/gateway/src/shard/createShard.ts +++ b/packages/gateway/src/shard/createShard.ts @@ -1,8 +1,5 @@ -import { StatusUpdate } from '../../helpers/misc/editShardStatus.js' -import { DiscordGatewayPayload } from '../../types/discord.js' -import { PickPartial } from '../../types/shared.js' -import { createLeakyBucket, LeakyBucket } from '../../util/bucket.js' -import { API_VERSION } from '../../util/constants.js' +import { ActivityTypes, DiscordGatewayPayload, PickPartial, PresenceStatus } from '@discordeno/types' +import { API_VERSION, createLeakyBucket, LeakyBucket } from '@discordeno/utils' import { calculateSafeRequests } from './calculateSafeRequests.js' import { close } from './close.js' import { connect } from './connect.js' @@ -32,6 +29,7 @@ import { // TODO: improve shard event resolving /** */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createShard ( options: CreateShard ) { @@ -55,11 +53,12 @@ export function createShard ( // ---------- /** The gateway configuration which is used to connect to Discord. */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions gatewayConfig: { compress: options.gatewayConfig.compress ?? false, intents: options.gatewayConfig.intents ?? 0, properties: { - os: options.gatewayConfig?.properties?.os ?? Deno.build.os, + os: options.gatewayConfig?.properties?.os ?? process.platform, browser: options.gatewayConfig?.properties?.browser ?? 'Discordeno', device: options.gatewayConfig?.properties?.device ?? 'Discordeno' }, @@ -68,6 +67,7 @@ export function createShard ( version: options.gatewayConfig.version ?? API_VERSION } as ShardGatewayConfig, /** This contains all the heartbeat information */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions heart: { acknowledged: false, interval: DEFAULT_HEARTBEAT_INTERVAL @@ -79,7 +79,7 @@ export function createShard ( */ maxRequestsPerRateLimitTick: MAX_GATEWAY_REQUESTS_PER_INTERVAL, /** The previous payload sequence number. */ - previousSequenceNumber: options.previousSequenceNumber || null, + previousSequenceNumber: options.previousSequenceNumber ?? null, /** In which interval (in milliseconds) the gateway resets it's rate limit. */ rateLimitResetInterval: GATEWAY_RATE_LIMIT_RESET_INTERVAL, /** Current session id of the shard if present. */ @@ -97,6 +97,7 @@ export function createShard ( // ---------- /** The shard related event handlers. */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions events: options.events ?? {} as ShardEvents, /** Calculate the amount of requests which can safely be made per rate limit interval, @@ -224,6 +225,25 @@ export function createShard ( } } +/** https://discord.com/developers/docs/topics/gateway-events#activity-object */ +export interface BotActivity { + name: string + type: ActivityTypes + url?: string +} + +/** https://discord.com/developers/docs/topics/gateway-events#update-presence */ +export interface BotStatusUpdate { + // /** Unix time (in milliseconds) of when the client went idle, or null if the client is not idle */ + since: number | null + /** The user's activities */ + activities: BotActivity[] + /** The user's new status */ + status: keyof typeof PresenceStatus + // /** Whether or not the client is afk */ + // afk: boolean; +} + export interface CreateShard { /** Id of the shard which should be created. */ id: number @@ -275,7 +295,7 @@ export interface CreateShard { isOpen?: typeof isOpen /** Function which can be overwritten in order to get the shards presence. */ - makePresence?: (shardId: number) => Promise | StatusUpdate + makePresence?: (shardId: number) => Promise | BotStatusUpdate /** The maximum of requests which can be send to discord per rate limit tick. * Typically this value should not be changed. diff --git a/packages/gateway/src/shard/deps.ts b/packages/gateway/src/shard/deps.ts deleted file mode 100644 index 8fb8a1b23..000000000 --- a/packages/gateway/src/shard/deps.ts +++ /dev/null @@ -1 +0,0 @@ -export { decompress_with as decompressWith } from 'https://unpkg.com/@evan/wasm@0.0.94/target/zlib/deno.js' diff --git a/packages/gateway/src/shard/handleClose.ts b/packages/gateway/src/shard/handleClose.ts index 2f40d5195..a8246ffe0 100644 --- a/packages/gateway/src/shard/handleClose.ts +++ b/packages/gateway/src/shard/handleClose.ts @@ -1,4 +1,4 @@ -import { GatewayCloseEventCodes } from '../../types/shared.js' +import { GatewayCloseEventCodes } from '@discordeno/types' import { Shard, ShardSocketCloseCodes, ShardState } from './types.js' export async function handleClose (shard: Shard, close: CloseEvent): Promise { diff --git a/packages/gateway/src/shard/handleMessage.ts b/packages/gateway/src/shard/handleMessage.ts index 0d190af11..28df2970e 100644 --- a/packages/gateway/src/shard/handleMessage.ts +++ b/packages/gateway/src/shard/handleMessage.ts @@ -1,23 +1,16 @@ -import { DiscordGatewayPayload, DiscordHello, DiscordReady } from '../../types/discord.js' -import { GatewayOpcodes } from '../../types/shared.js' -import { createLeakyBucket } from '../../util/bucket.js' -import { delay } from '../../util/utils.js' -import { decompressWith } from './deps.js' +import { DiscordGatewayPayload, DiscordHello, DiscordReady, GatewayOpcodes } from '@discordeno/types' +import { createLeakyBucket, delay } from '@discordeno/utils' +import { inflateSync } from 'node:zlib' import { GATEWAY_RATE_LIMIT_RESET_INTERVAL, Shard, ShardState } from './types.js' -const decoder = new TextDecoder() - export async function handleMessage (shard: Shard, message: MessageEvent): Promise { message = message.data // If message compression is enabled, // Discord might send zlib compressed payloads. if (shard.gatewayConfig.compress && message instanceof Blob) { - message = decompressWith( - new Uint8Array(await message.arrayBuffer()), - 0, - (slice: Uint8Array) => decoder.decode(slice) - ) + // @ts-expect-error + message = inflateSync(await message.arrayBuffer()).toString() } // Safeguard incase decompression failed to make a string. @@ -126,8 +119,9 @@ export async function handleMessage (shard: Shard, message: MessageEvent): shard.resolves.get('RESUMED')?.(messageData) shard.resolves.delete('RESUMED') - } // Important for future resumes. - else if (messageData.t === 'READY') { + } else if (messageData.t === 'READY') { + // Important for future resumes. + const payload = messageData.d as DiscordReady shard.resumeGatewayUrl = payload.resume_gateway_url diff --git a/packages/gateway/src/shard/identify.ts b/packages/gateway/src/shard/identify.ts index 8453ec3a1..951bf5c5a 100644 --- a/packages/gateway/src/shard/identify.ts +++ b/packages/gateway/src/shard/identify.ts @@ -1,11 +1,11 @@ -import { GatewayOpcodes } from '../../types/shared.js' +import { GatewayOpcodes } from '@discordeno/types' import { Shard, ShardSocketCloseCodes, ShardState } from './types.js' export async function identify (shard: Shard): Promise { // A new identify has been requested even though there is already a connection open. // Therefore we need to close the old connection and heartbeating before creating a new one. if (shard.isOpen()) { - console.log('CLOSING EXISTING SHARD: #' + shard.id) + console.log(`CLOSING EXISTING SHARD: #${shard.id}`) shard.close(ShardSocketCloseCodes.ReIdentifying, 'Re-identifying closure of old connection.') } @@ -22,7 +22,7 @@ export async function identify (shard: Shard): Promise { // Wait until an identify is free for this shard. await shard.requestIdentify() - shard.send({ + void shard.send({ op: GatewayOpcodes.Identify, d: { token: `Bot ${shard.gatewayConfig.token}`, diff --git a/packages/gateway/src/shard/resume.ts b/packages/gateway/src/shard/resume.ts index 5b2f291f1..de44a1c1b 100644 --- a/packages/gateway/src/shard/resume.ts +++ b/packages/gateway/src/shard/resume.ts @@ -1,4 +1,4 @@ -import { GatewayOpcodes } from '../../types/shared.js' +import { GatewayOpcodes } from '@discordeno/types' import { Shard, ShardSocketCloseCodes, ShardState } from './types.js' export async function resume (shard: Shard): Promise { @@ -26,7 +26,7 @@ export async function resume (shard: Shard): Promise { // Before we can resume, we need to create a new connection with Discord's gateway. await shard.connect() - shard.send({ + void shard.send({ op: GatewayOpcodes.Resume, d: { token: `Bot ${shard.gatewayConfig.token}`, diff --git a/packages/gateway/src/shard/startHeartbeating.ts b/packages/gateway/src/shard/startHeartbeating.ts index d2dc16e89..a785fbfc2 100644 --- a/packages/gateway/src/shard/startHeartbeating.ts +++ b/packages/gateway/src/shard/startHeartbeating.ts @@ -1,7 +1,7 @@ -import { GatewayOpcodes } from '../../types/shared.js' +import { GatewayOpcodes } from '@discordeno/types' import { Shard, ShardSocketCloseCodes, ShardState } from './types.js' -export function startHeartbeating (shard: Shard, interval: number) { +export function startHeartbeating (shard: Shard, interval: number): void { // gateway.debug("GW HEARTBEATING_STARTED", { shardId, interval }); shard.heart.interval = interval @@ -28,37 +28,39 @@ export function startHeartbeating (shard: Shard, interval: number) { shard.heart.acknowledged = false // After the random heartbeat jitter we can start a normal interval. - shard.heart.intervalId = setInterval(async () => { - // gateway.debug("GW DEBUG", `Running setInterval in heartbeat file. Shard: ${shardId}`); + shard.heart.intervalId = setInterval(() => { + void (async () => { + // gateway.debug("GW DEBUG", `Running setInterval in heartbeat file. Shard: ${shardId}`); - // gateway.debug("GW HEARTBEATING", { shardId, shard: currentShard }); + // gateway.debug("GW HEARTBEATING", { shardId, shard: currentShard }); - // The Shard did not receive a heartbeat ACK from Discord in time, - // therefore we have to assume that the connection has failed or got "zombied". - // The Shard needs to start a re-identify action accordingly. - // Reference: https://discord.com/developers/docs/topics/gateway#heartbeating-example-gateway-heartbeat-ack - if (!shard.heart.acknowledged) { - shard.close( - ShardSocketCloseCodes.ZombiedConnection, - 'Zombied connection, did not receive an heartbeat ACK in time.' + // The Shard did not receive a heartbeat ACK from Discord in time, + // therefore we have to assume that the connection has failed or got "zombied". + // The Shard needs to start a re-identify action accordingly. + // Reference: https://discord.com/developers/docs/topics/gateway#heartbeating-example-gateway-heartbeat-ack + if (!shard.heart.acknowledged) { + shard.close( + ShardSocketCloseCodes.ZombiedConnection, + 'Zombied connection, did not receive an heartbeat ACK in time.' + ) + + return await shard.identify() + } + + shard.heart.acknowledged = false + + // Using a direct socket.send call here because heartbeat requests are reserved by us. + shard.socket?.send( + JSON.stringify({ + op: GatewayOpcodes.Heartbeat, + d: shard.previousSequenceNumber + }) ) - return await shard.identify() - } + shard.heart.lastBeat = Date.now() - shard.heart.acknowledged = false - - // Using a direct socket.send call here because heartbeat requests are reserved by us. - shard.socket?.send( - JSON.stringify({ - op: GatewayOpcodes.Heartbeat, - d: shard.previousSequenceNumber - }) - ) - - shard.heart.lastBeat = Date.now() - - shard.events.heartbeat?.(shard) + shard.events.heartbeat?.(shard) + })() }, shard.heart.interval) }, jitter) } diff --git a/packages/gateway/src/shard/types.ts b/packages/gateway/src/shard/types.ts index 06ad015a0..94cd77637 100644 --- a/packages/gateway/src/shard/types.ts +++ b/packages/gateway/src/shard/types.ts @@ -1,5 +1,4 @@ -import { DiscordGatewayPayload } from '../../types/discord.js' -import { GatewayOpcodes } from '../../types/shared.js' +import { DiscordGatewayPayload, GatewayOpcodes } from '@discordeno/types' import { createShard } from './createShard.js' // TODO: think whether we also need an identifiedShard function @@ -80,7 +79,7 @@ export interface ShardHeart { /** Interval between heartbeats requested by Discord. */ interval: number /** Id of the interval, which is used for sending the heartbeats. */ - intervalId?: number + intervalId?: NodeJS.Timer /** Unix (in milliseconds) timestamp when the last heartbeat ACK was received from Discord. */ lastAck?: number /** Unix timestamp (in milliseconds) when the last heartbeat was sent. */ @@ -91,7 +90,7 @@ export interface ShardHeart { */ rtt?: number /** Id of the timeout which is used for sending the first heartbeat to Discord since it's "special". */ - timeoutId?: number + timeoutId?: NodeJS.Timeout } export interface ShardEvents { diff --git a/packages/gateway/src/utils/calculateShardId.ts b/packages/gateway/src/utils/calculateShardId.ts index f33cfc662..4703503d4 100644 --- a/packages/gateway/src/utils/calculateShardId.ts +++ b/packages/gateway/src/utils/calculateShardId.ts @@ -1,6 +1,6 @@ -import { GatewayManager } from '../gateway/manager/gatewayManager.js' +import { GatewayManager } from '../manager/gatewayManager.js' -export function calculateShardId (gateway: GatewayManager, guildId: bigint) { +export function calculateShardId (gateway: GatewayManager, guildId: bigint): number { if (gateway.manager.totalShards === 1) return 0 return Number((guildId >> 22n) % BigInt(gateway.manager.totalShards))