diff --git a/src/bot.ts b/src/bot.ts index c88efddb7..b3f5a2600 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -45,8 +45,10 @@ import { Errors } from "./types/discordeno/errors.ts"; import { DiscordGatewayPayload, GatewayDispatchEventNames, GatewayPayload } from "./types/gateway/gatewayPayload.ts"; import { closeWS, + createGatewayManager, createShard, DiscordenoShard, + GatewayManager, handleOnMessage, heartbeat, identify, @@ -329,64 +331,6 @@ export interface HelperUtils { calculatePermissions: typeof calculatePermissions; } -export function createGatewayManager( - options: Partial & Pick, -): GatewayManager { - return { - cache: { - guildIds: new Set(), - loadingGuildIds: new Set(), - editedMessages: new Collection(), - }, - secretKey: options.secretKey ?? "", - url: options.url ?? "", - reshard: options.reshard ?? true, - reshardPercentage: options.reshardPercentage ?? 80, - spawnShardDelay: options.spawnShardDelay ?? 2600, - maxShards: options.maxShards ?? options.shardsRecommended ?? 0, - useOptimalLargeBotSharding: options.useOptimalLargeBotSharding ?? true, - shardsPerWorker: options.shardsPerWorker ?? 25, - maxWorkers: options.maxWorkers ?? 4, - firstShardId: options.firstShardId ?? 0, - lastShardId: options.lastShardId ?? options.maxShards ?? options.shardsRecommended ?? 1, - token: options.token ?? "", - compress: options.compress ?? false, - $os: options.$os ?? "linux", - $browser: options.$browser ?? "Discordeno", - $device: options.$device ?? "Discordeno", - intents: - (Array.isArray(options.intents) - ? options.intents.reduce((bits, next) => (bits |= GatewayIntents[next]), 0) - : options.intents) ?? 0, - shard: options.shard ?? [0, options.shardsRecommended ?? 1], - urlWSS: options.urlWSS ?? "wss://gateway.discord.gg/?v=9&encoding=json", - shardsRecommended: options.shardsRecommended ?? 1, - sessionStartLimitTotal: options.sessionStartLimitTotal ?? 1000, - sessionStartLimitRemaining: options.sessionStartLimitRemaining ?? 1000, - sessionStartLimitResetAfter: options.sessionStartLimitResetAfter ?? 0, - maxConcurrency: options.maxConcurrency ?? 1, - shards: options.shards ?? new Collection(), - loadingShards: options.loadingShards ?? new Collection(), - buckets: new Collection(), - utf8decoder: new TextDecoder(), - - prepareBuckets: options.prepareBuckets ?? prepareBuckets, - spawnShards: options.spawnShards ?? spawnShards, - createShard: options.createShard ?? createShard, - identify: options.identify ?? identify, - heartbeat: options.heartbeat ?? heartbeat, - tellWorkerToIdentify, - debug: options.debug || function () {}, - resharder: options.resharder ?? resharder, - handleOnMessage: options.handleOnMessage ?? handleOnMessage, - processGatewayQueue: options.processGatewayQueue ?? processGatewayQueue, - closeWS: options.closeWS ?? closeWS, - sendShardMessage: options.sendShardMessage ?? sendShardMessage, - resume: options.resume ?? resume, - handleDiscordPayload: options.handleDiscordPayload, - }; -} - export async function stopBot(bot: Bot) { // STOP WS bot.gateway.shards.forEach((shard) => { @@ -536,109 +480,6 @@ export function createTransformers(options: Partial) { export type RestManager = ReturnType; -export interface GatewayManager { - /** The secret key authorization header the bot will expect when sending payloads. */ - secretKey: string; - /** The url that all discord payloads for the dispatch type should be sent to. */ - url: string; - /** Whether or not to automatically reshard. */ - reshard: boolean; - /** The percentage at which resharding should occur. */ - reshardPercentage: number; - /** The delay in milliseconds to wait before spawning next shard. OPTIMAL IS ABOVE 2500. YOU DON"T WANT TO HIT THE RATE LIMIT!!! */ - spawnShardDelay: number; - /** The maximum shard Id number. Useful for zero-downtime updates or resharding. */ - maxShards: number; - /** Whether or not the resharder should automatically switch to LARGE BOT SHARDING when you are above 100K servers. */ - useOptimalLargeBotSharding: boolean; - /** The amount of shards to load per worker. */ - shardsPerWorker: number; - /** The maximum amount of workers to use for your bot. */ - maxWorkers: number; - /** The first shard Id to start spawning. */ - firstShardId: number; - /** The last shard Id for this worker. */ - lastShardId: number; - token: string; - compress: boolean; - $os: string; - $browser: string; - $device: string; - intents: number | (keyof typeof GatewayIntents)[]; - shard: [number, number]; - presence?: Omit; - - /** The WSS URL that can be used for connecting to the gateway. */ - urlWSS: string; - /** The recommended number of shards to use when connecting. */ - shardsRecommended: number; - /** The total number of session starts the current user is allowed. */ - sessionStartLimitTotal: number; - /** The remaining number of session starts the current user is allowed. */ - sessionStartLimitRemaining: number; - /** Milliseconds left until limit is reset. */ - sessionStartLimitResetAfter: number; - /** The number of identify requests allowed per 5 seconds. - * So, if you had a max concurrency of 16, and 16 shards for example, you could start them all up at the same time. - * Whereas if you had 32 shards, if you tried to start up shard 0 and 16 at the same time for example, it would not work. You can start shards 0-15 concurrently, then 16-31... - */ - maxConcurrency: number; - shards: Collection; - loadingShards: Collection< - number, - { - shardId: number; - resolve: (value: unknown) => void; - } - >; - /** Stored as bucketId: { workers: [workerId, [ShardIds]], createNextShard: boolean } */ - buckets: Collection< - number, - { - workers: number[][]; - createNextShard: (() => Promise)[]; - } - >; - utf8decoder: TextDecoder; - - cache: { - guildIds: Set; - loadingGuildIds: Set; - editedMessages: Collection; - }; - - // METHODS - - /** Prepares the buckets for identifying */ - prepareBuckets: typeof prepareBuckets; - /** The handler for spawning ALL the shards. */ - spawnShards: typeof spawnShards; - /** Create the websocket and adds the proper handlers to the websocket. */ - createShard: typeof createShard; - /** Begins identification of the shard to discord. */ - identify: typeof identify; - /** Begins heartbeating of the shard to keep it alive. */ - heartbeat: typeof heartbeat; - /** Sends the discord payload to another server. */ - handleDiscordPayload: (gateway: GatewayManager, data: GatewayPayload, shardId: number) => any; - /** Tell the worker to begin identifying this shard */ - tellWorkerToIdentify: typeof tellWorkerToIdentify; - /** Handle the different logs. Used for debugging. */ - debug: (text: string, ...args: any[]) => unknown; - /** Handles resharding the bot when necessary. */ - resharder: typeof resharder; - /** Handles the message events from websocket. */ - handleOnMessage: typeof handleOnMessage; - /** Handles processing queue of requests send to this shard. */ - processGatewayQueue: typeof processGatewayQueue; - /** Closes shard WebSocket connection properly. */ - closeWS: typeof closeWS; - /** Properly adds a message to the shards queue. */ - sendShardMessage: typeof sendShardMessage; - /** Properly resume an old shards session. */ - resume: typeof resume; -} - export interface EventHandlers { debug: (text: string, ...args: any[]) => unknown; threadCreate: (bot: Bot, thread: DiscordenoChannel) => unknown; diff --git a/src/util/calculateShardId.ts b/src/util/calculateShardId.ts index f6756eba4..376684770 100644 --- a/src/util/calculateShardId.ts +++ b/src/util/calculateShardId.ts @@ -1,4 +1,4 @@ -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "../ws/gateway_manager.ts"; export function calculateShardId(gateway: GatewayManager, guildId: bigint) { if (gateway.maxShards === 1) return 0; diff --git a/src/ws/createShard.ts b/src/ws/createShard.ts index 1b8c21fb6..145f6e55c 100644 --- a/src/ws/createShard.ts +++ b/src/ws/createShard.ts @@ -1,5 +1,5 @@ import { GatewayCloseEventCodes } from "../types/codes/gatewayCloseEventCodes.ts"; -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; export function createShard(gateway: GatewayManager, shardId: number) { const socket = new WebSocket(`${gateway.urlWSS}/?v=9&encoding=json`); diff --git a/src/ws/gateway_manager.ts b/src/ws/gateway_manager.ts new file mode 100644 index 000000000..6510bdd6c --- /dev/null +++ b/src/ws/gateway_manager.ts @@ -0,0 +1,181 @@ +import { GatewayIntents, GatewayPayload, StatusUpdate } from "../types/mod.ts"; +import { Collection } from "../util/collection.ts"; +import { closeWS } from "./closeWs.ts"; +import { createShard } from "./createShard.ts"; +import { handleOnMessage } from "./handleOnMessage.ts"; +import { heartbeat } from "./heartbeat.ts"; +import { identify } from "./identify.ts"; +import { processGatewayQueue } from "./processGatewayQueue.ts"; +import { resharder } from "./resharder.ts"; +import { resume } from "./resume.ts"; +import { sendShardMessage } from "./sendShardMessage.ts"; +import { prepareBuckets, spawnShards } from "./spawnShards.ts"; +import { tellWorkerToIdentify } from "./tellWorkerToIdentify.ts"; +import { DiscordenoShard } from "./ws.ts"; + +/** Create a new Gateway Manager. + * + * @param options: Customize every bit of the manager. If something is not + * provided, it will fallback to a default which should be suitable for most + * bots. + */ +export function createGatewayManager( + options: Partial & Pick, +): GatewayManager { + return { + cache: { + guildIds: new Set(), + loadingGuildIds: new Set(), + editedMessages: new Collection(), + }, + secretKey: options.secretKey ?? "", + url: options.url ?? "", + reshard: options.reshard ?? true, + reshardPercentage: options.reshardPercentage ?? 80, + spawnShardDelay: options.spawnShardDelay ?? 2600, + maxShards: options.maxShards ?? options.shardsRecommended ?? 0, + useOptimalLargeBotSharding: options.useOptimalLargeBotSharding ?? true, + shardsPerWorker: options.shardsPerWorker ?? 25, + maxWorkers: options.maxWorkers ?? 4, + firstShardId: options.firstShardId ?? 0, + lastShardId: options.lastShardId ?? options.maxShards ?? options.shardsRecommended ?? 1, + token: options.token ?? "", + compress: options.compress ?? false, + $os: options.$os ?? "linux", + $browser: options.$browser ?? "Discordeno", + $device: options.$device ?? "Discordeno", + intents: + (Array.isArray(options.intents) + ? options.intents.reduce((bits, next) => (bits |= GatewayIntents[next]), 0) + : options.intents) ?? 0, + shard: options.shard ?? [0, options.shardsRecommended ?? 1], + urlWSS: options.urlWSS ?? "wss://gateway.discord.gg/?v=9&encoding=json", + shardsRecommended: options.shardsRecommended ?? 1, + sessionStartLimitTotal: options.sessionStartLimitTotal ?? 1000, + sessionStartLimitRemaining: options.sessionStartLimitRemaining ?? 1000, + sessionStartLimitResetAfter: options.sessionStartLimitResetAfter ?? 0, + maxConcurrency: options.maxConcurrency ?? 1, + shards: options.shards ?? new Collection(), + loadingShards: options.loadingShards ?? new Collection(), + buckets: new Collection(), + utf8decoder: new TextDecoder(), + + prepareBuckets: options.prepareBuckets ?? prepareBuckets, + spawnShards: options.spawnShards ?? spawnShards, + createShard: options.createShard ?? createShard, + identify: options.identify ?? identify, + heartbeat: options.heartbeat ?? heartbeat, + tellWorkerToIdentify, + debug: options.debug || function () {}, + resharder: options.resharder ?? resharder, + handleOnMessage: options.handleOnMessage ?? handleOnMessage, + processGatewayQueue: options.processGatewayQueue ?? processGatewayQueue, + closeWS: options.closeWS ?? closeWS, + sendShardMessage: options.sendShardMessage ?? sendShardMessage, + resume: options.resume ?? resume, + handleDiscordPayload: options.handleDiscordPayload, + }; +} + +export interface GatewayManager { + /** The secret key authorization header the bot will expect when sending payloads. */ + secretKey: string; + /** The url that all discord payloads for the dispatch type should be sent to. */ + url: string; + /** Whether or not to automatically reshard. */ + reshard: boolean; + /** The percentage at which resharding should occur. */ + reshardPercentage: number; + /** The delay in milliseconds to wait before spawning next shard. OPTIMAL IS ABOVE 2500. YOU DON"T WANT TO HIT THE RATE LIMIT!!! */ + spawnShardDelay: number; + /** The maximum shard Id number. Useful for zero-downtime updates or resharding. */ + maxShards: number; + /** Whether or not the resharder should automatically switch to LARGE BOT SHARDING when you are above 100K servers. */ + useOptimalLargeBotSharding: boolean; + /** The amount of shards to load per worker. */ + shardsPerWorker: number; + /** The maximum amount of workers to use for your bot. */ + maxWorkers: number; + /** The first shard Id to start spawning. */ + firstShardId: number; + /** The last shard Id for this worker. */ + lastShardId: number; + token: string; + compress: boolean; + $os: string; + $browser: string; + $device: string; + intents: number | (keyof typeof GatewayIntents)[]; + shard: [number, number]; + presence?: Omit; + + /** The WSS URL that can be used for connecting to the gateway. */ + urlWSS: string; + /** The recommended number of shards to use when connecting. */ + shardsRecommended: number; + /** The total number of session starts the current user is allowed. */ + sessionStartLimitTotal: number; + /** The remaining number of session starts the current user is allowed. */ + sessionStartLimitRemaining: number; + /** Milliseconds left until limit is reset. */ + sessionStartLimitResetAfter: number; + /** The number of identify requests allowed per 5 seconds. + * So, if you had a max concurrency of 16, and 16 shards for example, you could start them all up at the same time. + * Whereas if you had 32 shards, if you tried to start up shard 0 and 16 at the same time for example, it would not work. You can start shards 0-15 concurrently, then 16-31... + */ + maxConcurrency: number; + shards: Collection; + loadingShards: Collection< + number, + { + shardId: number; + resolve: (value: unknown) => void; + } + >; + /** Stored as bucketId: { workers: [workerId, [ShardIds]], createNextShard: boolean } */ + buckets: Collection< + number, + { + workers: number[][]; + createNextShard: (() => Promise)[]; + } + >; + utf8decoder: TextDecoder; + + cache: { + guildIds: Set; + loadingGuildIds: Set; + editedMessages: Collection; + }; + + // METHODS + + /** Prepares the buckets for identifying */ + prepareBuckets: typeof prepareBuckets; + /** The handler for spawning ALL the shards. */ + spawnShards: typeof spawnShards; + /** Create the websocket and adds the proper handlers to the websocket. */ + createShard: typeof createShard; + /** Begins identification of the shard to discord. */ + identify: typeof identify; + /** Begins heartbeating of the shard to keep it alive. */ + heartbeat: typeof heartbeat; + /** Sends the discord payload to another server. */ + handleDiscordPayload: (gateway: GatewayManager, data: GatewayPayload, shardId: number) => any; + /** Tell the worker to begin identifying this shard */ + tellWorkerToIdentify: typeof tellWorkerToIdentify; + /** Handle the different logs. Used for debugging. */ + debug: (text: string, ...args: any[]) => unknown; + /** Handles resharding the bot when necessary. */ + resharder: typeof resharder; + /** Handles the message events from websocket. */ + handleOnMessage: typeof handleOnMessage; + /** Handles processing queue of requests send to this shard. */ + processGatewayQueue: typeof processGatewayQueue; + /** Closes shard WebSocket connection properly. */ + closeWS: typeof closeWS; + /** Properly adds a message to the shards queue. */ + sendShardMessage: typeof sendShardMessage; + /** Properly resume an old shards session. */ + resume: typeof resume; +} diff --git a/src/ws/handleOnMessage.ts b/src/ws/handleOnMessage.ts index 9a1984c2f..f0ab40905 100644 --- a/src/ws/handleOnMessage.ts +++ b/src/ws/handleOnMessage.ts @@ -1,4 +1,4 @@ -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; import { GatewayOpcodes } from "../types/codes/gatewayOpcodes.ts"; import type { DiscordGatewayPayload } from "../types/gateway/gatewayPayload.ts"; import type { DiscordHello } from "../types/gateway/hello.ts"; diff --git a/src/ws/heartbeat.ts b/src/ws/heartbeat.ts index 86e25c371..8d7f7576d 100644 --- a/src/ws/heartbeat.ts +++ b/src/ws/heartbeat.ts @@ -1,6 +1,6 @@ import { GatewayOpcodes } from "../types/codes/gatewayOpcodes.ts"; import { delay } from "../util/utils.ts"; -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; export async function heartbeat(gateway: GatewayManager, shardId: number, interval: number) { gateway.debug("GW HEARTBEATING_STARTED", { shardId, interval }); diff --git a/src/ws/identify.ts b/src/ws/identify.ts index 7b35e835c..2807a0cbe 100644 --- a/src/ws/identify.ts +++ b/src/ws/identify.ts @@ -1,5 +1,5 @@ import { GatewayOpcodes } from "../types/codes/gatewayOpcodes.ts"; -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; export function identify(gateway: GatewayManager, shardId: number, maxShards: number) { gateway.debug("GW IDENTIFYING", { shardId, maxShards }); diff --git a/src/ws/mod.ts b/src/ws/mod.ts index 2f80e518b..d5b3a6e7d 100644 --- a/src/ws/mod.ts +++ b/src/ws/mod.ts @@ -11,3 +11,4 @@ export * from "./sendShardMessage.ts"; export * from "./startGatewayOptions.ts"; export * from "./tellWorkerToIdentify.ts"; export * from "./ws.ts"; +export * from "./gateway_manager.ts"; diff --git a/src/ws/processGatewayQueue.ts b/src/ws/processGatewayQueue.ts index ff3bfbb83..9ada3bbb2 100644 --- a/src/ws/processGatewayQueue.ts +++ b/src/ws/processGatewayQueue.ts @@ -1,5 +1,5 @@ import { delay } from "../util/utils.ts"; -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; export async function processGatewayQueue(gateway: GatewayManager, id: number) { const shard = gateway.shards.get(id); diff --git a/src/ws/resharder.ts b/src/ws/resharder.ts index b242401bb..63444d4b9 100644 --- a/src/ws/resharder.ts +++ b/src/ws/resharder.ts @@ -1,5 +1,5 @@ -import { GatewayManager } from "../bot.ts"; import { GetGatewayBot } from "../types/gateway/getGatewayBot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; /** The handler to automatically reshard when necessary. */ export async function resharder(gateway: GatewayManager) { diff --git a/src/ws/resume.ts b/src/ws/resume.ts index 1b1cf842b..aea08b1a7 100644 --- a/src/ws/resume.ts +++ b/src/ws/resume.ts @@ -1,5 +1,5 @@ import { GatewayOpcodes } from "../types/codes/gatewayOpcodes.ts"; -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; export function resume(gateway: GatewayManager, shardId: number) { gateway.debug("GW RESUMING", { shardId }); diff --git a/src/ws/sendShardMessage.ts b/src/ws/sendShardMessage.ts index 401cf391a..39fa741b7 100644 --- a/src/ws/sendShardMessage.ts +++ b/src/ws/sendShardMessage.ts @@ -1,5 +1,5 @@ +import { GatewayManager } from "./gateway_manager.ts"; import { DiscordenoShard, WebSocketRequest } from "./ws.ts"; -import { GatewayManager } from "../bot.ts"; export function sendShardMessage( gateway: GatewayManager, diff --git a/src/ws/spawnShards.ts b/src/ws/spawnShards.ts index caf367dfd..a5d2225e5 100644 --- a/src/ws/spawnShards.ts +++ b/src/ws/spawnShards.ts @@ -1,5 +1,6 @@ /** Begin spawning shards. */ -import { GatewayManager } from "../bot.ts"; + +import { GatewayManager } from "./gateway_manager.ts"; export function prepareBuckets(gateway: GatewayManager, firstShardId: number, lastShardId: number) { /** Stored as bucketId: [workerId, [ShardIds]] */ diff --git a/src/ws/tellWorkerToIdentify.ts b/src/ws/tellWorkerToIdentify.ts index 4e7170f01..1c9c82509 100644 --- a/src/ws/tellWorkerToIdentify.ts +++ b/src/ws/tellWorkerToIdentify.ts @@ -1,4 +1,4 @@ -import { GatewayManager } from "../bot.ts"; +import { GatewayManager } from "./gateway_manager.ts"; /** Allows users to hook in and change to communicate to different workers across different servers or anything they like. For example using redis pubsub to talk to other servers. */ export async function tellWorkerToIdentify(