diff --git a/src/bot.ts b/src/bot.ts index ae0dc92ff..e04296576 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -31,6 +31,9 @@ export async function startBot(config: BotConfig) { // Initial API connection to get info about bots connection ws.botGatewayData = await getGatewayBot(); ws.maxShards = ws.maxShards || ws.botGatewayData.shards; + ws.lastShardId = ws.lastShardId === 1 + ? ws.botGatewayData.shards - 1 + : ws.lastShardId; // Explicitly append gateway version and encoding ws.botGatewayData.url += `?v=${GATEWAY_VERSION}&encoding=json`; @@ -94,7 +97,7 @@ export async function startBigBrainBot(options: BigBrainBotConfig) { // Initial API connection to get info about bots connection ws.botGatewayData = await getGatewayBot(); - ws.maxShards = ws.maxShards || + ws.maxShards = options.lastShardId || ws.maxShards || ws.botGatewayData.shards; ws.lastShardId = options.lastShardId || ws.botGatewayData.shards; // Explicitly append gateway version and encoding diff --git a/src/handlers/guilds/GUILD_CREATE.ts b/src/handlers/guilds/GUILD_CREATE.ts index 3c62e57ea..fcc43412b 100644 --- a/src/handlers/guilds/GUILD_CREATE.ts +++ b/src/handlers/guilds/GUILD_CREATE.ts @@ -24,8 +24,8 @@ export async function handleGuildCreate( if (shard?.unavailableGuildIds.has(guild.id)) { await cacheHandlers.delete("unavailableGuilds", guild.id); - shard.unavailableGuildIds.delete(guild.id); + shard.lastAvailable = Date.now(); return eventHandlers.guildAvailable?.(guild); } diff --git a/src/handlers/guilds/GUILD_DELETE.ts b/src/handlers/guilds/GUILD_DELETE.ts index ea30c220b..d7144e4cf 100644 --- a/src/handlers/guilds/GUILD_DELETE.ts +++ b/src/handlers/guilds/GUILD_DELETE.ts @@ -22,7 +22,6 @@ export async function handleGuildDelete( if (payload.unavailable) { const shard = ws.shards.get(shardId); if (shard) shard.unavailableGuildIds.add(guild.id); - await cacheHandlers.set("unavailableGuilds", guild.id, Date.now()); eventHandlers.guildUnavailable?.(guild); diff --git a/src/handlers/misc/READY.ts b/src/handlers/misc/READY.ts index 82098f0ac..1f2c2ab74 100644 --- a/src/handlers/misc/READY.ts +++ b/src/handlers/misc/READY.ts @@ -3,33 +3,33 @@ import { cache } from "../../cache.ts"; import type { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts"; import type { Ready } from "../../types/gateway/ready.ts"; import { snowflakeToBigint } from "../../util/bigint.ts"; -import { ws } from "../../ws/ws.ts"; +import { DiscordenoShard, ws } from "../../ws/ws.ts"; export function handleReady( data: DiscordGatewayPayload, shardId: number, ) { + // Triggered on each shard + eventHandlers.shardReady?.(shardId); + // The bot has already started, the last shard is resumed, however. if (cache.isReady) return; + const shard = ws.shards.get(shardId); + if (!shard) return; + const payload = data.d as Ready; setBotId(payload.user.id); setApplicationId(payload.application.id); - // Triggered on each shard - eventHandlers.shardReady?.(shardId); - // Save when the READY event was received to prevent infinite load loops - const now = Date.now(); - - const shard = ws.shards.get(shardId); - if (!shard) return; - // Set ready to false just to go sure shard.ready = false; // All guilds are unavailable at first shard.unavailableGuildIds = new Set( payload.guilds.map((g) => snowflakeToBigint(g.id)), ); + // Set the last available to now + shard.lastAvailable = Date.now(); // Start ready check in 2 seconds setTimeout(() => { @@ -37,58 +37,51 @@ export function handleReady( "loop", `1. Running setTimeout in READY file.`, ); - checkReady(payload, shardId, now); + checkReady(payload, shard); }, 2000); } -// Don't pass the shard itself because unavailableGuilds won't be updated by the GUILD_CREATE event /** This function checks if the shard is fully loaded */ -function checkReady(payload: Ready, shardId: number, now: number) { - const shard = ws.shards.get(shardId); - if (!shard) return; - +function checkReady(payload: Ready, shard: DiscordenoShard) { // Check if all guilds were loaded - if (shard.unavailableGuildIds.size) { - if (Date.now() - now > 10000) { - eventHandlers.shardFailedToLoad?.(shardId, shard.unavailableGuildIds); - // Force execute the loaded function to prevent infinite loop - loaded(shardId); - } else { - // Not all guilds were loaded but 10 seconds haven't passed so check again - setTimeout(() => { - eventHandlers.debug?.( - "loop", - `2. Running setTimeout in READY file.`, - ); - checkReady(payload, shardId, now); - }, 2000); - } - } else { - // All guilds were loaded - loaded(shardId); + if (!shard.unavailableGuildIds.size) return loaded(shard); + + // If the last GUILD_CREATE has been received before 5 seconds if so most likely the remaining guilds are unavailable + if (shard.lastAvailable + 5000 < Date.now()) { + eventHandlers.shardFailedToLoad?.(shard.id, shard.unavailableGuildIds); + // Force execute the loaded function to prevent infinite loop + return loaded(shard); } + + // Not all guilds were loaded but 5 seconds haven't passed so check again + setTimeout(() => { + eventHandlers.debug?.( + "loop", + `2. Running setTimeout in READY file.`, + ); + checkReady(payload, shard); + }, 2000); } -function loaded(shardId: number) { - const shard = ws.shards.get(shardId); - if (!shard) return; - +function loaded(shard: DiscordenoShard) { shard.ready = true; - // If it is the last shard we can go full ready - if (shardId === ws.lastShardId) { - // Still some shards are loading so wait another 2 seconds for them - if (ws.shards.some((shard) => !shard.ready)) { - setTimeout(() => { - eventHandlers.debug?.( - "loop", - `3. Running setTimeout in READY file.`, - ); - loaded(shardId); - }, 2000); - } else { - cache.isReady = true; - eventHandlers.ready?.(); - } + // If it is not the last shard we can't go full ready + if (shard.id !== ws.lastShardId) return; + + // Still some shards are loading so wait another 2 seconds for them + if (ws.shards.some((shard) => !shard.ready)) { + setTimeout(() => { + eventHandlers.debug?.( + "loop", + `3. Running setTimeout in READY file.`, + ); + loaded(shard); + }, 2000); + + return; } + + cache.isReady = true; + eventHandlers.ready?.(); } diff --git a/src/types/discordeno/eventHandlers.ts b/src/types/discordeno/eventHandlers.ts index c456c91ee..72137e1ce 100644 --- a/src/types/discordeno/eventHandlers.ts +++ b/src/types/discordeno/eventHandlers.ts @@ -4,6 +4,7 @@ import { DiscordenoMember } from "../../structures/member.ts"; import { DiscordenoMessage } from "../../structures/message.ts"; import { DiscordenoRole } from "../../structures/role.ts"; import { Collection } from "../../util/collection.ts"; +import { DiscordenoShard } from "../../ws/ws.ts"; import { ThreadMember } from "../channels/threads/thread_member.ts"; import { ThreadMembersUpdate } from "../channels/threads/thread_members_update.ts"; import { IntegrationCreateUpdate } from "../integrations/integration_create_update.ts"; diff --git a/src/ws/identify.ts b/src/ws/identify.ts index c645df9d1..68fc669cf 100644 --- a/src/ws/identify.ts +++ b/src/ws/identify.ts @@ -24,6 +24,7 @@ export async function identify(shardId: number, maxShards: number) { resuming: false, ready: false, unavailableGuildIds: new Set(), + lastAvailable: 0, heartbeat: { lastSentAt: 0, lastReceivedAt: 0, diff --git a/src/ws/resume.ts b/src/ws/resume.ts index d1efa4e7b..ce47f63ff 100644 --- a/src/ws/resume.ts +++ b/src/ws/resume.ts @@ -30,6 +30,7 @@ export async function resume(shardId: number) { resuming: false, ready: false, unavailableGuildIds: new Set(), + lastAvailable: 0, heartbeat: { lastSentAt: 0, lastReceivedAt: 0, diff --git a/src/ws/ws.ts b/src/ws/ws.ts index b9611681a..34f060029 100644 --- a/src/ws/ws.ts +++ b/src/ws/ws.ts @@ -17,7 +17,7 @@ import { tellClusterToIdentify } from "./tell_cluster_to_identify.ts"; // CONTROLLER LIKE INTERFACE FOR WS HANDLING export const ws = { - /** The secret key authorization header the bot will expect when sending payloads */ + /** The secret key authorization header the bot will expect when sending payloads. */ secretKey: "", /** The url that all discord payloads for the dispatch type should be sent to. */ url: "", @@ -29,7 +29,7 @@ export const ws = { maxShards: 0, /** Whether or not the resharder should automatically switch to LARGE BOT SHARDING when you are above 100K servers. */ useOptimalLargeBotSharding: true, - /** The amount of shards to load per cluster */ + /** The amount of shards to load per cluster. */ shardsPerCluster: 25, /** The maximum amount of clusters to use for your bot. */ maxClusters: 4, @@ -96,7 +96,7 @@ export const ws = { createShard, /** Begins identification of the shard to discord. */ identify, - /** Begins heartbeating of the shard to keep it alive */ + /** Begins heartbeating of the shard to keep it alive. */ heartbeat, /** Sends the discord payload to another server. */ handleDiscordPayload, @@ -114,16 +114,16 @@ export const ws = { processQueue, /** Closes shard WebSocket connection properly. */ closeWS, - /** Properly adds a message to the shards queue */ + /** Properly adds a message to the shards queue. */ sendShardMessage, }; export interface DiscordenoShard { - /** The shard id number */ + /** The shard id number. */ id: number; - /** The websocket for this shard */ + /** The websocket for this shard. */ ws: WebSocket; - /** The amount of milliseconds to wait between heartbeats */ + /** The amount of milliseconds to wait between heartbeats. */ resumeInterval: number; /** The session id important for resuming connections. */ sessionId: string; @@ -131,12 +131,14 @@ export interface DiscordenoShard { previousSequenceNumber: number | null; /** Whether the shard is currently resuming. */ resuming: boolean; - /** Whether the shard has received the ready event */ + /** Whether the shard has received the ready event. */ ready: boolean; /** The list of guild ids that are currently unavailable due to an outage. */ unavailableGuildIds: Set; + /** Last time when a GUILD_CREATE event has been received for an unavailable guild. This is used to prevent infinite loops in the READY event handler. */ + lastAvailable: number; heartbeat: { - /** The exact timestamp the last heartbeat was sent */ + /** The exact timestamp the last heartbeat was sent. */ lastSentAt: number; /** The timestamp the last heartbeat ACK was received from discord. */ lastReceivedAt: number; @@ -155,7 +157,7 @@ export interface DiscordenoShard { processingQueue: boolean; /** When the first request for this minute has been sent. */ queueStartedAt: number; - /** The request counter of the queue */ + /** The request counter of the queue. */ queueCounter: number; }