From c626815bff3c985b78c9932e0e8bfb34b85343f9 Mon Sep 17 00:00:00 2001 From: Denis-Adrian Cristea Date: Sat, 12 Sep 2026 23:38:20 +0300 Subject: [PATCH] refactor(ws)!: take /gateway/bot data in `WebSocketManager#connect` (#11602) BREAKING CHANGE: `WebSocketManager` no longer accepts `fetchGatewayInformation`, pass `gatewayInformation` to `connect` instead. --- packages/core/README.md | 5 +- packages/discord.js/src/client/Client.js | 5 +- .../discord.js/src/managers/GuildManager.js | 4 +- packages/ws/README.md | 30 +--- .../WorkerContextFetchingStrategy.test.ts | 12 +- .../strategy/WorkerShardingStrategy.test.ts | 5 +- .../ws/__tests__/ws/WebSocketManager.test.ts | 120 +++++++--------- .../context/IContextFetchingStrategy.ts | 4 +- packages/ws/src/utils/constants.ts | 8 +- packages/ws/src/ws/WebSocketManager.ts | 133 ++++++++---------- 10 files changed, 132 insertions(+), 194 deletions(-) diff --git a/packages/core/README.md b/packages/core/README.md index b635dd46d..0b7dcf375 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -55,7 +55,6 @@ const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); const gateway = new WebSocketManager({ token: process.env.DISCORD_TOKEN, intents: GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent, - fetchGatewayInformation: () => rest.get('/gateway/bot') as Promise, }); // Create a client to emit relevant events. @@ -75,7 +74,9 @@ client.on(GatewayDispatchEvents.InteractionCreate, async ({ data: interaction, a client.once(GatewayDispatchEvents.Ready, () => console.log('Ready!')); // Start the WebSocket connection. -gateway.connect(); +await gateway.connect({ + gatewayInformation: (await rest.get('/gateway/bot')) as RESTGetAPIGatewayBotResult, +}); ``` ## Independent REST API Usage diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index a3820c849..d7427d162 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -206,7 +206,6 @@ class Client extends AsyncEventEmitter { const wsOptions = { ...this.options.ws, intents: this.options.intents.bitfield, - fetchGatewayInformation: () => this.rest.get(Routes.gatewayBot()), // Explicitly nulled to always be set using `setToken` in `login` token: null, }; @@ -322,7 +321,7 @@ class Client extends AsyncEventEmitter { this.ws.setToken(this.token); try { - await this.ws.connect(); + await this.ws.connect({ gatewayInformation: await this.rest.get(Routes.gatewayBot()) }); return this.token; } catch (error) { await this.destroy(); @@ -434,7 +433,7 @@ class Client extends AsyncEventEmitter { * @private */ async _broadcast(packet) { - const shardIds = await this.ws.getShardIds(); + const shardIds = this.ws.getShardIds(); return Promise.all(shardIds.map(shardId => this.ws.send(shardId, packet))); } diff --git a/packages/discord.js/src/managers/GuildManager.js b/packages/discord.js/src/managers/GuildManager.js index 573429464..3df8b3c4d 100644 --- a/packages/discord.js/src/managers/GuildManager.js +++ b/packages/discord.js/src/managers/GuildManager.js @@ -137,7 +137,7 @@ class GuildManager extends CachedManager { const innerData = await this.client.rest.get(Routes.guild(id), { query: makeURLSearchParams({ with_counts: options.withCounts ?? true }), }); - innerData.shardId = ShardClientUtil.shardIdForGuildId(id, await this.client.ws.fetchShardCount()); + innerData.shardId = ShardClientUtil.shardIdForGuildId(id, this.client.ws.getShardCount()); return this._add(innerData, options.cache); } @@ -165,7 +165,7 @@ class GuildManager extends CachedManager { * console.log(soundboardSounds.get('123456789012345678')); */ async fetchSoundboardSounds({ guildIds, time = 10_000 }) { - const shardCount = await this.client.ws.getShardCount(); + const shardCount = this.client.ws.getShardCount(); const shardIds = Map.groupBy(guildIds, guildId => ShardClientUtil.shardIdForGuildId(guildId, shardCount)); for (const [shardId, shardGuildIds] of shardIds) { diff --git a/packages/ws/README.md b/packages/ws/README.md index 94fdf141a..434c4f582 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -46,16 +46,14 @@ The example uses [ES modules](https://nodejs.org/api/esm.html#enabling). ```ts import { WebSocketManager, WebSocketShardEvents, CompressionMethod } from '@discordjs/ws'; import { REST } from '@discordjs/rest'; -import type { RESTGetAPIGatewayBotResult } from 'discord-api-types/v10'; +import { Routes, type RESTGetAPIGatewayBotResult } from 'discord-api-types/v10'; const rest = new REST().setToken(process.env.DISCORD_TOKEN); + // This example will spawn Discord's recommended shard count, all under the current process. const manager = new WebSocketManager({ token: process.env.DISCORD_TOKEN, intents: 0, // for no intents - fetchGatewayInformation() { - return rest.get(Routes.gatewayBot()) as Promise; - }, // uncomment if you have zlib-sync installed and want to use compression // compression: CompressionMethod.ZlibSync, @@ -67,7 +65,10 @@ manager.on(WebSocketShardEvents.Dispatch, (event) => { // Process gateway events here. }); -await manager.connect(); +// The data from `/gateway/bot` is used as-is, so it's best fetched right before connecting. +await manager.connect({ + gatewayInformation: (await rest.get(Routes.gatewayBot())) as RESTGetAPIGatewayBotResult, +}); ``` ### Specify shards @@ -78,9 +79,6 @@ const manager = new WebSocketManager({ token: process.env.DISCORD_TOKEN, intents: 0, shardCount: 4, - fetchGatewayInformation() { - return rest.get(Routes.gatewayBot()) as Promise; - }, }); // The manager also supports being responsible for only a subset of your shards: @@ -92,9 +90,6 @@ const manager = new WebSocketManager({ intents: 0, shardCount: 8, shardIds: [0, 2, 4, 6], - fetchGatewayInformation() { - return rest.get(Routes.gatewayBot()) as Promise; - }, }); // Alternatively, if your shards are consecutive, you can pass in a range @@ -106,9 +101,6 @@ const manager = new WebSocketManager({ start: 0, end: 4, }, - fetchGatewayInformation() { - return rest.get(Routes.gatewayBot()) as Promise; - }, }); ``` @@ -118,16 +110,11 @@ You can also have the shards spawn in worker threads: ```ts import { WebSocketManager, WorkerShardingStrategy } from '@discordjs/ws'; -import { REST } from '@discordjs/rest'; -const rest = new REST().setToken(process.env.DISCORD_TOKEN); const manager = new WebSocketManager({ token: process.env.DISCORD_TOKEN, intents: 0, shardCount: 6, - fetchGatewayInformation() { - return rest.get(Routes.gatewayBot()) as Promise; - }, // This will cause 3 workers to spawn, 2 shards per each buildStrategy: (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 }), // Or maybe you want all your shards under a single worker @@ -139,15 +126,10 @@ const manager = new WebSocketManager({ ```ts import { WebSocketManager, WorkerShardingStrategy } from '@discordjs/ws'; -import { REST } from '@discordjs/rest'; -const rest = new REST().setToken(process.env.DISCORD_TOKEN); const manager = new WebSocketManager({ token: process.env.DISCORD_TOKEN, intents: 0, - fetchGatewayInformation() { - return rest.get(Routes.gatewayBot()) as Promise; - }, buildStrategy: (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2, diff --git a/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts b/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts index 52f306846..757369f09 100644 --- a/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts +++ b/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts @@ -46,11 +46,17 @@ test('session info', async () => { const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, - async fetchGatewayInformation() { - return mockGatewayInformation; - }, + buildStrategy: () => ({ + spawn: vi.fn(), + connect: vi.fn(), + destroy: vi.fn(), + send: vi.fn(), + fetchStatus: vi.fn(), + }), }); + await manager.connect({ gatewayInformation: mockGatewayInformation }); + const strategy = new WorkerContextFetchingStrategy(await managerToFetchingStrategyOptions(manager)); strategy.updateSessionInfo(0, session); diff --git a/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts b/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts index 65440639b..ac4f6e658 100644 --- a/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts +++ b/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts @@ -142,9 +142,6 @@ test('spawn, connect, send a message, session info, and destroy', async () => { const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, - async fetchGatewayInformation() { - return mockGatewayInformation; - }, shardIds: [0, 1], retrieveSessionInfo: mockRetrieveSessionInfo, updateSessionInfo: mockUpdateSessionInfo, @@ -153,7 +150,7 @@ test('spawn, connect, send a message, session info, and destroy', async () => { const managerEmitSpy = vi.spyOn(manager, 'emit'); - await manager.connect(); + await manager.connect({ gatewayInformation: mockGatewayInformation }); expect(mockConstructor).toHaveBeenCalledWith( expect.stringContaining('defaultWorker.js'), expect.objectContaining({ workerData: expect.objectContaining({ shardIds: [0, 1] }) }), diff --git a/packages/ws/__tests__/ws/WebSocketManager.test.ts b/packages/ws/__tests__/ws/WebSocketManager.test.ts index 2c2dad7c4..af1e74b26 100644 --- a/packages/ws/__tests__/ws/WebSocketManager.test.ts +++ b/packages/ws/__tests__/ws/WebSocketManager.test.ts @@ -4,136 +4,113 @@ import { describe, expect, test, vi } from 'vitest'; import { WebSocketManager, type IShardingStrategy } from '../../src/index.js'; import { mockGatewayInformation } from '../gateway.mock.js'; -vi.useFakeTimers(); +class MockStrategy implements IShardingStrategy { + public spawn = vi.fn(); -const NOW = vi.fn().mockReturnValue(Date.now()); -global.Date.now = NOW; + public connect = vi.fn(); -test('fetch gateway information', async () => { - const fetchGatewayInformation = vi.fn(async () => mockGatewayInformation); + public destroy = vi.fn(); + public send = vi.fn(); + + public fetchStatus = vi.fn(); +} + +test('connect requires gateway information', async () => { const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, - fetchGatewayInformation, }); - const initial = await manager.fetchGatewayInformation(); - expect(initial).toEqual(mockGatewayInformation); - expect(fetchGatewayInformation).toHaveBeenCalledOnce(); + // @ts-expect-error: Testing the runtime check for a missing gatewayInformation + await expect(manager.connect()).rejects.toThrow(TypeError); +}); - fetchGatewayInformation.mockClear(); +test('gateway information is not available before connecting', () => { + const manager = new WebSocketManager({ + token: 'A-Very-Fake-Token', + intents: 0, + }); - const cached = await manager.fetchGatewayInformation(); - expect(cached).toEqual(mockGatewayInformation); - expect(fetchGatewayInformation).not.toHaveBeenCalled(); - - fetchGatewayInformation.mockClear(); - - const forced = await manager.fetchGatewayInformation(true); - expect(forced).toEqual(mockGatewayInformation); - expect(fetchGatewayInformation).toHaveBeenCalledOnce(); - - fetchGatewayInformation.mockClear(); - - NOW.mockReturnValue(Number.POSITIVE_INFINITY); - const cacheExpired = await manager.fetchGatewayInformation(); - expect(cacheExpired).toEqual(mockGatewayInformation); - expect(fetchGatewayInformation).toHaveBeenCalledOnce(); + expect(() => manager.getGatewayInformation()).toThrow(Error); + expect(() => manager.getShardCount()).toThrow(Error); }); describe('get shard count', () => { - test('with shard count', async () => { + test('with no shard count or ids', async () => { + const manager = new WebSocketManager({ + token: 'A-Very-Fake-Token', + intents: 0, + buildStrategy: () => new MockStrategy(), + }); + + await manager.connect({ gatewayInformation: mockGatewayInformation }); + + expect(manager.getShardCount()).toBe(mockGatewayInformation.shards); + }); + + test('with shard count', () => { const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, shardCount: 2, - async fetchGatewayInformation() { - return mockGatewayInformation; - }, }); - expect(await manager.getShardCount()).toBe(2); + expect(manager.getShardCount()).toBe(2); }); - test('with shard ids array', async () => { + test('with shard ids array', () => { const shardIds = [5, 9]; const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, shardIds, - async fetchGatewayInformation() { - return mockGatewayInformation; - }, }); - expect(await manager.getShardCount()).toBe(shardIds.at(-1)! + 1); + expect(manager.getShardCount()).toBe(shardIds.at(-1)! + 1); }); - test('with shard id range', async () => { + test('with shard id range', () => { const shardIds = { start: 5, end: 9 }; const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, shardIds, - async fetchGatewayInformation() { - return mockGatewayInformation; - }, }); - expect(await manager.getShardCount()).toBe(shardIds.end + 1); + expect(manager.getShardCount()).toBe(shardIds.end + 1); }); }); test('update shard count', async () => { - const fetchGatewayInformation = vi.fn(async () => mockGatewayInformation); - const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, shardCount: 2, - fetchGatewayInformation, + buildStrategy: () => new MockStrategy(), }); - expect(await manager.getShardCount()).toBe(2); - expect(fetchGatewayInformation).not.toHaveBeenCalled(); - - fetchGatewayInformation.mockClear(); + expect(manager.getShardCount()).toBe(2); await manager.updateShardCount(3); - expect(await manager.getShardCount()).toBe(3); - expect(fetchGatewayInformation).toHaveBeenCalled(); + expect(manager.getShardCount()).toBe(3); + expect(manager.getShardIds()).toStrictEqual([0, 1, 2]); }); -test('it handles passing in both shardIds and shardCount', async () => { +test('it handles passing in both shardIds and shardCount', () => { const shardIds = { start: 2, end: 3 }; const manager = new WebSocketManager({ token: 'A-Very-Fake-Token', intents: 0, shardIds, shardCount: 4, - async fetchGatewayInformation() { - return mockGatewayInformation; - }, }); - expect(await manager.getShardCount()).toBe(4); - expect(await manager.getShardIds()).toStrictEqual([2, 3]); + expect(manager.getShardCount()).toBe(4); + expect(manager.getShardIds()).toStrictEqual([2, 3]); }); test('strategies', async () => { - class MockStrategy implements IShardingStrategy { - public spawn = vi.fn(); - - public connect = vi.fn(); - - public destroy = vi.fn(); - - public send = vi.fn(); - - public fetchStatus = vi.fn(); - } - const strategy = new MockStrategy(); const shardIds = [0, 1, 2]; @@ -142,19 +119,18 @@ test('strategies', async () => { token: 'A-Very-Fake-Token', intents: 0, shardIds, - async fetchGatewayInformation() { - return mockGatewayInformation; - }, buildStrategy: () => strategy, }); - await manager.connect(); + await manager.connect({ gatewayInformation: mockGatewayInformation }); + expect(manager.getGatewayInformation()).toBe(mockGatewayInformation); expect(strategy.spawn).toHaveBeenCalledWith(shardIds); expect(strategy.connect).toHaveBeenCalled(); const destroyOptions = { reason: ':3' }; await manager.destroy(destroyOptions); expect(strategy.destroy).toHaveBeenCalledWith(destroyOptions); + expect(() => manager.getGatewayInformation()).toThrow(Error); const send: GatewaySendPayload = { op: GatewayOpcodes.RequestGuildMembers, diff --git a/packages/ws/src/strategies/context/IContextFetchingStrategy.ts b/packages/ws/src/strategies/context/IContextFetchingStrategy.ts index 903d88da2..290e6883c 100644 --- a/packages/ws/src/strategies/context/IContextFetchingStrategy.ts +++ b/packages/ws/src/strategies/context/IContextFetchingStrategy.ts @@ -51,7 +51,7 @@ export async function managerToFetchingStrategyOptions(manager: WebSocketManager useIdentifyCompression: manager.options.useIdentifyCompression, version: manager.options.version, - gatewayInformation: await manager.fetchGatewayInformation(), - shardCount: await manager.getShardCount(), + gatewayInformation: manager.getGatewayInformation(), + shardCount: manager.getShardCount(), }; } diff --git a/packages/ws/src/utils/constants.ts b/packages/ws/src/utils/constants.ts index 35f3da732..a6d88f41b 100644 --- a/packages/ws/src/utils/constants.ts +++ b/packages/ws/src/utils/constants.ts @@ -37,10 +37,8 @@ export const CompressionParameterMap = { * Default options used by the manager */ export const DefaultWebSocketManagerOptions = { - async buildIdentifyThrottler(manager: WebSocketManager) { - const info = await manager.fetchGatewayInformation(); - return new SimpleIdentifyThrottler(info.session_start_limit.max_concurrency); - }, + buildIdentifyThrottler: (manager: WebSocketManager) => + new SimpleIdentifyThrottler(manager.getGatewayInformation().session_start_limit.max_concurrency), buildStrategy: (manager) => new SimpleShardingStrategy(manager), shardCount: null, shardIds: null, @@ -70,7 +68,7 @@ export const DefaultWebSocketManagerOptions = { handshakeTimeout: 30_000, helloTimeout: 60_000, readyTimeout: 15_000, -} as const satisfies Omit; +} as const satisfies Omit; export const ImportantGatewayOpcodes = new Set([ GatewayOpcodes.Heartbeat, diff --git a/packages/ws/src/ws/WebSocketManager.ts b/packages/ws/src/ws/WebSocketManager.ts index 0529a9e18..ff0b635db 100644 --- a/packages/ws/src/ws/WebSocketManager.ts +++ b/packages/ws/src/ws/WebSocketManager.ts @@ -2,7 +2,6 @@ import type { Collection } from '@discordjs/collection'; import { range, type Awaitable } from '@discordjs/util'; import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; import type { - APIGatewayBotInfo, GatewayIdentifyProperties, GatewayPresenceUpdateData, RESTGetAPIGatewayBotResult, @@ -54,22 +53,6 @@ export interface SessionInfo { * Required options for the WebSocketManager */ export interface RequiredWebSocketManagerOptions { - /** - * Function for retrieving the information returned by the `/gateway/bot` endpoint. - * We recommend using a REST client that respects Discord's rate limits, such as `@discordjs/rest`. - * - * @example - * ```ts - * const rest = new REST().setToken(process.env.DISCORD_TOKEN); - * const manager = new WebSocketManager({ - * token: process.env.DISCORD_TOKEN, - * fetchGatewayInformation() { - * return rest.get(Routes.gatewayBot()) as Promise; - * }, - * }); - * ``` - */ - fetchGatewayInformation(): Awaitable; /** * The intents to request */ @@ -89,13 +72,9 @@ export interface OptionalWebSocketManagerOptions { * * @example * ```ts - * const rest = new REST().setToken(process.env.DISCORD_TOKEN); * const manager = new WebSocketManager({ * token: process.env.DISCORD_TOKEN, * intents: 0, // for no intents - * fetchGatewayInformation() { - * return rest.get(Routes.gatewayBot()) as Promise; - * }, * buildStrategy: (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 }), * }); * ``` @@ -207,6 +186,26 @@ export interface WebSocketManagerOptions extends OptionalWebSocketManagerOptions export interface CreateWebSocketManagerOptions extends Partial, RequiredWebSocketManagerOptions {} +/** + * Options for {@link WebSocketManager.connect} + */ +export interface WebSocketManagerConnectOptions { + /** + * Information retrieved from the `/gateway/bot` endpoint, used as-is. + * We recommend using a REST client that respects Discord's rate limits, such as `@discordjs/rest`, + * and fetching this information right before connecting, as the session start limits it reports go stale. + * + * @example + * ```ts + * const rest = new REST().setToken(process.env.DISCORD_TOKEN); + * await manager.connect({ + * gatewayInformation: (await rest.get(Routes.gatewayBot())) as RESTGetAPIGatewayBotResult, + * }); + * ``` + */ + gatewayInformation: RESTGetAPIGatewayBotResult; +} + export interface ManagerShardEventsMap { [WebSocketShardEvents.Closed]: [code: number, shardId: number]; [WebSocketShardEvents.Debug]: [message: string, shardId: number]; @@ -225,24 +224,13 @@ export interface ManagerShardEventsMap { export class WebSocketManager extends AsyncEventEmitter implements AsyncDisposable { #token: string | null = null; + #gatewayInformation: RESTGetAPIGatewayBotResult | null = null; + /** * The options being used by this manager */ public readonly options: Omit; - /** - * Internal cache for a GET /gateway/bot result - */ - private gatewayInformation: { - data: APIGatewayBotInfo; - expiresAt: number; - } | null = null; - - /** - * Internal cache for the shard ids - */ - private shardIds: number[] | null = null; - /** * Strategy used to manage shards * @@ -266,10 +254,6 @@ export class WebSocketManager extends AsyncEventEmitter i } public constructor(options: CreateWebSocketManagerOptions) { - if (typeof options.fetchGatewayInformation !== 'function') { - throw new TypeError('fetchGatewayInformation is required'); - } - super(); this.options = { ...DefaultWebSocketManagerOptions, @@ -280,28 +264,19 @@ export class WebSocketManager extends AsyncEventEmitter i } /** - * Fetches the gateway information from Discord - or returns it from cache if available - * - * @param force - Whether to ignore the cache and force a fresh fetch + * The `/gateway/bot` information provided to {@link WebSocketManager.connect}. + * Throws if the method has not been invoked yet. */ - public async fetchGatewayInformation(force = false) { - if (this.gatewayInformation) { - if (this.gatewayInformation.expiresAt <= Date.now()) { - this.gatewayInformation = null; - } else if (!force) { - return this.gatewayInformation.data; - } + public getGatewayInformation(): RESTGetAPIGatewayBotResult { + if (!this.#gatewayInformation) { + throw new Error('Gateway information has not been set. Invoke `connect()` first.'); } - const data = await this.options.fetchGatewayInformation(); - - // For single sharded bots session_start_limit.reset_after will be 0, use 5 seconds as a minimum expiration time - this.gatewayInformation = { data, expiresAt: Date.now() + (data.session_start_limit.reset_after || 5_000) }; - return this.gatewayInformation.data; + return this.#gatewayInformation; } /** - * Updates your total shard count on-the-fly, spawning shards as needed + * Updates your total shard count on-the-fly, re-spawning all shards to the new amount * * @param shardCount - The new shard count to use */ @@ -309,7 +284,7 @@ export class WebSocketManager extends AsyncEventEmitter i await this.strategy.destroy({ reason: 'User is adjusting their shards' }); this.options.shardCount = shardCount; - const shardIds = await this.getShardIds(true); + const shardIds = this.getShardIds(); await this.strategy.spawn(shardIds); return this; @@ -317,24 +292,26 @@ export class WebSocketManager extends AsyncEventEmitter i /** * Yields the total number of shards across for your bot, accounting for Discord recommendations + * + * @remarks + * Throws if {@link WebSocketManager.connect} has not been invoked yet. */ - public async getShardCount(): Promise { + public getShardCount(): number { if (this.options.shardCount) { return this.options.shardCount; } - const shardIds = await this.getShardIds(); + const shardIds = this.getShardIds(); return Math.max(...shardIds) + 1; } /** * Yields the ids of the shards this manager should manage + * + * @remarks + * Throws if {@link WebSocketManager.connect} has not been invoked yet. */ - public async getShardIds(force = false): Promise { - if (this.shardIds && !force) { - return this.shardIds; - } - + public getShardIds(): number[] { let shardIds: number[]; if (this.options.shardIds) { if (Array.isArray(this.options.shardIds)) { @@ -344,30 +321,31 @@ export class WebSocketManager extends AsyncEventEmitter i shardIds = [...range({ start, end: end + 1 })]; } } else { - const data = await this.fetchGatewayInformation(); - shardIds = [...range(this.options.shardCount ?? data.shards)]; + shardIds = [...range(this.options.shardCount ?? this.getGatewayInformation().shards)]; } - this.shardIds = shardIds; return shardIds; } - public async connect() { - const shardCount = await this.getShardCount(); - // Spawn shards and adjust internal state - await this.updateShardCount(shardCount); + public async connect(options: WebSocketManagerConnectOptions) { + if (!options?.gatewayInformation) { + throw new TypeError('gatewayInformation is required'); + } - const shardIds = await this.getShardIds(); - const data = await this.fetchGatewayInformation(); - - if (data.session_start_limit.remaining < shardIds.length) { + this.#gatewayInformation = options.gatewayInformation; + const shardIds = this.getShardIds(); + if (options.gatewayInformation.session_start_limit.remaining < shardIds.length) { + this.#gatewayInformation = null; throw new Error( `Not enough sessions remaining to spawn ${shardIds.length} shards; only ${ - data.session_start_limit.remaining - } remaining; resets at ${new Date(Date.now() + data.session_start_limit.reset_after).toISOString()}`, + options.gatewayInformation.session_start_limit.remaining + } remaining; resets at ${new Date(Date.now() + options.gatewayInformation.session_start_limit.reset_after).toISOString()}`, ); } + // Spawn shards and adjust internal state + await this.updateShardCount(this.getShardCount()); + await this.strategy.connect(); } @@ -385,8 +363,9 @@ export class WebSocketManager extends AsyncEventEmitter i * @remarks * To keep the shard(s) resumable, use the {@link CloseCodes.Resuming} code. */ - public destroy(options?: Omit) { - return this.strategy.destroy(options); + public async destroy(options?: Omit) { + await this.strategy.destroy(options); + this.#gatewayInformation = null; } public send(shardId: number, payload: GatewaySendPayload) {