mirror of
https://github.com/discordjs/discord.js.git
synced 2026-09-18 01:07:26 +00:00
refactor(ws)!: take /gateway/bot data in WebSocketManager#connect (#11602)
BREAKING CHANGE: `WebSocketManager` no longer accepts `fetchGatewayInformation`, pass `gatewayInformation` to `connect` instead.
This commit is contained in:
@@ -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<RESTGetAPIGatewayBotResult>,
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+6
-24
@@ -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<RESTGetAPIGatewayBotResult>;
|
||||
},
|
||||
// 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<RESTGetAPIGatewayBotResult>;
|
||||
},
|
||||
});
|
||||
|
||||
// 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<RESTGetAPIGatewayBotResult>;
|
||||
},
|
||||
});
|
||||
|
||||
// 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<RESTGetAPIGatewayBotResult>;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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<RESTGetAPIGatewayBotResult>;
|
||||
},
|
||||
// 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<RESTGetAPIGatewayBotResult>;
|
||||
},
|
||||
buildStrategy: (manager) =>
|
||||
new WorkerShardingStrategy(manager, {
|
||||
shardsPerWorker: 2,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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] }) }),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<OptionalWebSocketManagerOptions, 'fetchGatewayInformation' | 'token'>;
|
||||
} as const satisfies Omit<OptionalWebSocketManagerOptions, 'token'>;
|
||||
|
||||
export const ImportantGatewayOpcodes = new Set([
|
||||
GatewayOpcodes.Heartbeat,
|
||||
|
||||
@@ -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<RESTGetAPIGatewayBotResult>;
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
fetchGatewayInformation(): Awaitable<RESTGetAPIGatewayBotResult>;
|
||||
/**
|
||||
* 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<RESTGetAPIGatewayBotResult>;
|
||||
* },
|
||||
* buildStrategy: (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 }),
|
||||
* });
|
||||
* ```
|
||||
@@ -207,6 +186,26 @@ export interface WebSocketManagerOptions extends OptionalWebSocketManagerOptions
|
||||
export interface CreateWebSocketManagerOptions
|
||||
extends Partial<OptionalWebSocketManagerOptions>, 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<ManagerShardEventsMap> implements AsyncDisposable {
|
||||
#token: string | null = null;
|
||||
|
||||
#gatewayInformation: RESTGetAPIGatewayBotResult | null = null;
|
||||
|
||||
/**
|
||||
* The options being used by this manager
|
||||
*/
|
||||
public readonly options: Omit<WebSocketManagerOptions, 'token'>;
|
||||
|
||||
/**
|
||||
* 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<ManagerShardEventsMap> 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<ManagerShardEventsMap> 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<ManagerShardEventsMap> 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<ManagerShardEventsMap> 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<number> {
|
||||
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<number[]> {
|
||||
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<ManagerShardEventsMap> 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<ManagerShardEventsMap> i
|
||||
* @remarks
|
||||
* To keep the shard(s) resumable, use the {@link CloseCodes.Resuming} code.
|
||||
*/
|
||||
public destroy(options?: Omit<WebSocketShardDestroyOptions, 'recover'>) {
|
||||
return this.strategy.destroy(options);
|
||||
public async destroy(options?: Omit<WebSocketShardDestroyOptions, 'recover'>) {
|
||||
await this.strategy.destroy(options);
|
||||
this.#gatewayInformation = null;
|
||||
}
|
||||
|
||||
public send(shardId: number, payload: GatewaySendPayload) {
|
||||
|
||||
Reference in New Issue
Block a user