From dd4f501a6c03973d16558dd6f386c4d36de7f82f Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Wed, 9 Sep 2026 18:05:08 +0200 Subject: [PATCH] feat(WebSocketManager): support gateway capabilities --- .../types/WebSocketManager.test-d.ts | 3 ++ .../ws/__tests__/ws/WebSocketManager.test.ts | 29 ++++++++++++++++++- .../context/IContextFetchingStrategy.ts | 2 ++ packages/ws/src/utils/constants.ts | 1 + packages/ws/src/ws/WebSocketManager.ts | 8 +++++ packages/ws/src/ws/WebSocketShard.ts | 5 ++++ 6 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/ws/__tests__/types/WebSocketManager.test-d.ts b/packages/ws/__tests__/types/WebSocketManager.test-d.ts index 04dad8edb..821dd31f5 100644 --- a/packages/ws/__tests__/types/WebSocketManager.test-d.ts +++ b/packages/ws/__tests__/types/WebSocketManager.test-d.ts @@ -1,10 +1,13 @@ import type { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; +import type { GatewayCapabilityBits } from 'discord-api-types/v10'; import { expectTypeOf } from 'vitest'; import type { ManagerShardEventsMap, WebSocketShardEventsMap, WebSocketManager } from '../../src/index.js'; declare const manager: WebSocketManager; declare const eventMap: ManagerShardEventsMap; +expectTypeOf(manager.options.capabilities).toEqualTypeOf(); + type AugmentedShardEventsMap = { [K in keyof WebSocketShardEventsMap]: [...WebSocketShardEventsMap[K], shardId: number]; }; diff --git a/packages/ws/__tests__/ws/WebSocketManager.test.ts b/packages/ws/__tests__/ws/WebSocketManager.test.ts index 2c2dad7c4..9ec6a6d2b 100644 --- a/packages/ws/__tests__/ws/WebSocketManager.test.ts +++ b/packages/ws/__tests__/ws/WebSocketManager.test.ts @@ -1,5 +1,5 @@ import type { GatewaySendPayload } from 'discord-api-types/v10'; -import { GatewayOpcodes } from 'discord-api-types/v10'; +import { GatewayCapabilityBits, GatewayOpcodes } from 'discord-api-types/v10'; import { describe, expect, test, vi } from 'vitest'; import { WebSocketManager, type IShardingStrategy } from '../../src/index.js'; import { mockGatewayInformation } from '../gateway.mock.js'; @@ -121,6 +121,33 @@ test('it handles passing in both shardIds and shardCount', async () => { expect(await manager.getShardIds()).toStrictEqual([2, 3]); }); +describe('gateway capabilities', () => { + test('defaults to none', async () => { + const manager = new WebSocketManager({ + token: 'A-Very-Fake-Token', + intents: 0, + async fetchGatewayInformation() { + return mockGatewayInformation; + }, + }); + + expect(manager.options.capabilities).toBe(0); + }); + + test('with a provided bitfield', async () => { + const manager = new WebSocketManager({ + token: 'A-Very-Fake-Token', + intents: 0, + capabilities: GatewayCapabilityBits.ChannelObfuscation, + async fetchGatewayInformation() { + return mockGatewayInformation; + }, + }); + + expect(manager.options.capabilities).toBe(GatewayCapabilityBits.ChannelObfuscation); + }); +}); + test('strategies', async () => { class MockStrategy implements IShardingStrategy { public spawn = vi.fn(); diff --git a/packages/ws/src/strategies/context/IContextFetchingStrategy.ts b/packages/ws/src/strategies/context/IContextFetchingStrategy.ts index 903d88da2..8190076f8 100644 --- a/packages/ws/src/strategies/context/IContextFetchingStrategy.ts +++ b/packages/ws/src/strategies/context/IContextFetchingStrategy.ts @@ -4,6 +4,7 @@ import type { SessionInfo, WebSocketManager, WebSocketManagerOptions } from '../ export interface FetchingStrategyOptions extends Pick< WebSocketManagerOptions, + | 'capabilities' | 'compression' | 'encoding' | 'handshakeTimeout' @@ -38,6 +39,7 @@ export interface IContextFetchingStrategy { export async function managerToFetchingStrategyOptions(manager: WebSocketManager): Promise { return { + capabilities: manager.options.capabilities, compression: manager.options.compression, encoding: manager.options.encoding, handshakeTimeout: manager.options.handshakeTimeout, diff --git a/packages/ws/src/utils/constants.ts b/packages/ws/src/utils/constants.ts index 35f3da732..796319c1a 100644 --- a/packages/ws/src/utils/constants.ts +++ b/packages/ws/src/utils/constants.ts @@ -42,6 +42,7 @@ export const DefaultWebSocketManagerOptions = { return new SimpleIdentifyThrottler(info.session_start_limit.max_concurrency); }, buildStrategy: (manager) => new SimpleShardingStrategy(manager), + capabilities: 0, shardCount: null, shardIds: null, largeThreshold: null, diff --git a/packages/ws/src/ws/WebSocketManager.ts b/packages/ws/src/ws/WebSocketManager.ts index 0529a9e18..d592776ca 100644 --- a/packages/ws/src/ws/WebSocketManager.ts +++ b/packages/ws/src/ws/WebSocketManager.ts @@ -3,6 +3,7 @@ import { range, type Awaitable } from '@discordjs/util'; import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; import type { APIGatewayBotInfo, + GatewayCapabilityBits, GatewayIdentifyProperties, GatewayPresenceUpdateData, RESTGetAPIGatewayBotResult, @@ -101,6 +102,13 @@ export interface OptionalWebSocketManagerOptions { * ``` */ buildStrategy(manager: WebSocketManager): IShardingStrategy; + /** + * The gateway capabilities to opt into + * + * @defaultValue `0` + * @see {@link https://discord.com/developers/docs/events/gateway-events#identify-gateway-capabilities} + */ + capabilities: GatewayCapabilityBits | 0; /** * The transport compression method to use - mutually exclusive with `useIdentifyCompression` * diff --git a/packages/ws/src/ws/WebSocketShard.ts b/packages/ws/src/ws/WebSocketShard.ts index 5aa7a10d9..e1ae0e48a 100644 --- a/packages/ws/src/ws/WebSocketShard.ts +++ b/packages/ws/src/ws/WebSocketShard.ts @@ -552,6 +552,7 @@ export class WebSocketShard extends AsyncEventEmitter { `shard id: ${this.id.toString()}`, `shard count: ${this.strategy.options.shardCount}`, `intents: ${this.strategy.options.intents}`, + `capabilities: ${this.strategy.options.capabilities}`, `compression: ${this.transportCompressionEnabled ? CompressionParameterMap[this.strategy.options.compression!] : this.identifyCompressionEnabled ? 'identify' : 'none'}`, ]); @@ -567,6 +568,10 @@ export class WebSocketShard extends AsyncEventEmitter { data.large_threshold = this.strategy.options.largeThreshold; } + if (this.strategy.options.capabilities) { + data.capabilities = this.strategy.options.capabilities; + } + if (this.strategy.options.initialPresence) { data.presence = this.strategy.options.initialPresence; }