@ovencord/ws 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +262 -0
- package/package.json +57 -0
- package/src/index.ts +23 -0
- package/src/strategies/context/IContextFetchingStrategy.ts +57 -0
- package/src/strategies/context/SimpleContextFetchingStrategy.ts +40 -0
- package/src/strategies/context/WorkerContextFetchingStrategy.ts +95 -0
- package/src/strategies/sharding/IShardingStrategy.ts +30 -0
- package/src/strategies/sharding/SimpleShardingStrategy.ts +84 -0
- package/src/strategies/sharding/WorkerShardingStrategy.ts +255 -0
- package/src/strategies/sharding/defaultWorker.ts +4 -0
- package/src/throttling/IIdentifyThrottler.ts +11 -0
- package/src/throttling/SimpleIdentifyThrottler.ts +49 -0
- package/src/utils/BunCompression.ts +144 -0
- package/src/utils/WorkerBootstrapper.ts +65 -0
- package/src/utils/constants.ts +85 -0
- package/src/ws/WebSocketManager.ts +396 -0
- package/src/ws/WebSocketShard.ts +876 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WorkerSendPayloadOp,
|
|
3
|
+
} from '../strategies/sharding/WorkerShardingStrategy.js';
|
|
4
|
+
import { WebSocketManager } from '../ws/WebSocketManager.js';
|
|
5
|
+
import { SimpleShardingStrategy } from '../strategies/sharding/SimpleShardingStrategy.js';
|
|
6
|
+
|
|
7
|
+
// Define the global self explicitly for TypeScript as a Worker
|
|
8
|
+
declare const self: Worker;
|
|
9
|
+
|
|
10
|
+
export class WorkerBootstrapper {
|
|
11
|
+
private manager!: WebSocketManager;
|
|
12
|
+
|
|
13
|
+
public async bootstrap() {
|
|
14
|
+
// Listen for messages from the main thread
|
|
15
|
+
self.addEventListener('message', async (event: MessageEvent) => {
|
|
16
|
+
const message = event.data;
|
|
17
|
+
|
|
18
|
+
// Initial setup if we receive the custom 'initial_data' op
|
|
19
|
+
if (message && message.op === 'initial_data') {
|
|
20
|
+
const { token, intents, shardCount } = message.data;
|
|
21
|
+
|
|
22
|
+
this.manager = new WebSocketManager(
|
|
23
|
+
{
|
|
24
|
+
token,
|
|
25
|
+
intents,
|
|
26
|
+
buildStrategy: (manager: WebSocketManager) => new SimpleShardingStrategy(manager),
|
|
27
|
+
shardCount,
|
|
28
|
+
async fetchGatewayInformation() {
|
|
29
|
+
// In a worker, we might need to fetch this from the main thread too,
|
|
30
|
+
// but manager usually handles it.
|
|
31
|
+
// For now, satisfy the interface.
|
|
32
|
+
return message.data.gatewayInformation;
|
|
33
|
+
}
|
|
34
|
+
} as any,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await this.manager.connect();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Handle standard protocol messages
|
|
42
|
+
if (!message || typeof message.op === 'undefined') return;
|
|
43
|
+
|
|
44
|
+
switch (message.op) {
|
|
45
|
+
case WorkerSendPayloadOp.Connect: {
|
|
46
|
+
const { shardId: _shardId } = message.d;
|
|
47
|
+
await this.manager.connect();
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case WorkerSendPayloadOp.Destroy: {
|
|
52
|
+
const { options } = message.d;
|
|
53
|
+
await this.manager.destroy(options);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case WorkerSendPayloadOp.Send: {
|
|
58
|
+
const { shardId, payload } = message.d;
|
|
59
|
+
await this.manager.send(shardId, payload);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Collection } from '@ovencord/collection';
|
|
2
|
+
import { lazy } from '@ovencord/util';
|
|
3
|
+
import { APIVersion, GatewayOpcodes } from 'discord-api-types/v10';
|
|
4
|
+
import { SimpleShardingStrategy } from '../strategies/sharding/SimpleShardingStrategy.js';
|
|
5
|
+
import { SimpleIdentifyThrottler } from '../throttling/SimpleIdentifyThrottler.js';
|
|
6
|
+
import type { SessionInfo, OptionalWebSocketManagerOptions, WebSocketManager } from '../ws/WebSocketManager.js';
|
|
7
|
+
import type { SendRateLimitState } from '../ws/WebSocketShard.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Valid encoding types
|
|
11
|
+
*/
|
|
12
|
+
export enum Encoding {
|
|
13
|
+
JSON = 'json',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Valid compression methods
|
|
18
|
+
*/
|
|
19
|
+
export enum CompressionMethod {
|
|
20
|
+
ZlibNative,
|
|
21
|
+
ZlibSync,
|
|
22
|
+
ZstdNative,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DefaultDeviceProperty = `@ovencord/ws [VI]{{inject}}[/VI]` as `@ovencord/ws ${string}`;
|
|
26
|
+
|
|
27
|
+
const getDefaultSessionStore = lazy(() => new Collection<number, SessionInfo | null>());
|
|
28
|
+
|
|
29
|
+
export const CompressionParameterMap = {
|
|
30
|
+
[CompressionMethod.ZlibNative]: 'zlib-stream',
|
|
31
|
+
[CompressionMethod.ZlibSync]: 'zlib-stream',
|
|
32
|
+
[CompressionMethod.ZstdNative]: 'zstd-stream',
|
|
33
|
+
} as const satisfies Record<CompressionMethod, string>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default options used by the manager
|
|
37
|
+
*/
|
|
38
|
+
export const DefaultWebSocketManagerOptions = {
|
|
39
|
+
async buildIdentifyThrottler(manager: WebSocketManager) {
|
|
40
|
+
const info = await manager.fetchGatewayInformation();
|
|
41
|
+
return new SimpleIdentifyThrottler(info.session_start_limit.max_concurrency);
|
|
42
|
+
},
|
|
43
|
+
buildStrategy: (manager) => new SimpleShardingStrategy(manager),
|
|
44
|
+
shardCount: null,
|
|
45
|
+
shardIds: null,
|
|
46
|
+
largeThreshold: null,
|
|
47
|
+
initialPresence: null,
|
|
48
|
+
identifyProperties: {
|
|
49
|
+
browser: DefaultDeviceProperty,
|
|
50
|
+
device: DefaultDeviceProperty,
|
|
51
|
+
os: process.platform,
|
|
52
|
+
},
|
|
53
|
+
version: APIVersion,
|
|
54
|
+
encoding: Encoding.JSON,
|
|
55
|
+
compression: null,
|
|
56
|
+
useIdentifyCompression: false,
|
|
57
|
+
retrieveSessionInfo(shardId) {
|
|
58
|
+
const store = getDefaultSessionStore();
|
|
59
|
+
return store.get(shardId) ?? null;
|
|
60
|
+
},
|
|
61
|
+
updateSessionInfo(shardId: number, info: SessionInfo | null) {
|
|
62
|
+
const store = getDefaultSessionStore();
|
|
63
|
+
if (info) {
|
|
64
|
+
store.set(shardId, info);
|
|
65
|
+
} else {
|
|
66
|
+
store.delete(shardId);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
handshakeTimeout: 30_000,
|
|
70
|
+
helloTimeout: 60_000,
|
|
71
|
+
readyTimeout: 15_000,
|
|
72
|
+
} as const satisfies Omit<OptionalWebSocketManagerOptions, 'fetchGatewayInformation' | 'token'>;
|
|
73
|
+
|
|
74
|
+
export const ImportantGatewayOpcodes = new Set([
|
|
75
|
+
GatewayOpcodes.Heartbeat,
|
|
76
|
+
GatewayOpcodes.Identify,
|
|
77
|
+
GatewayOpcodes.Resume,
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
export function getInitialSendRateLimitState(): SendRateLimitState {
|
|
81
|
+
return {
|
|
82
|
+
sent: 0,
|
|
83
|
+
resetAt: Date.now() + 60_000,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import type { Collection } from '@ovencord/collection';
|
|
2
|
+
import { AsyncEventEmitter, range, type Awaitable } from '@ovencord/util';
|
|
3
|
+
import type {
|
|
4
|
+
APIGatewayBotInfo,
|
|
5
|
+
GatewayIdentifyProperties,
|
|
6
|
+
GatewayPresenceUpdateData,
|
|
7
|
+
RESTGetAPIGatewayBotResult,
|
|
8
|
+
GatewayIntentBits,
|
|
9
|
+
GatewaySendPayload,
|
|
10
|
+
GatewayDispatchPayload,
|
|
11
|
+
GatewayReadyDispatchData,
|
|
12
|
+
} from 'discord-api-types/v10';
|
|
13
|
+
import type { IShardingStrategy } from '../strategies/sharding/IShardingStrategy.js';
|
|
14
|
+
import type { IIdentifyThrottler } from '../throttling/IIdentifyThrottler.js';
|
|
15
|
+
import { DefaultWebSocketManagerOptions, type CompressionMethod, type Encoding } from '../utils/constants.js';
|
|
16
|
+
import type { WebSocketShardDestroyOptions, WebSocketShardEvents, WebSocketShardStatus } from './WebSocketShard.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a range of shard ids
|
|
20
|
+
*/
|
|
21
|
+
export interface ShardRange {
|
|
22
|
+
end: number;
|
|
23
|
+
start: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Session information for a given shard, used to resume a session
|
|
28
|
+
*/
|
|
29
|
+
export interface SessionInfo {
|
|
30
|
+
/**
|
|
31
|
+
* URL to use when resuming
|
|
32
|
+
*/
|
|
33
|
+
resumeURL: string;
|
|
34
|
+
/**
|
|
35
|
+
* The sequence number of the last message sent by the shard
|
|
36
|
+
*/
|
|
37
|
+
sequence: number;
|
|
38
|
+
/**
|
|
39
|
+
* Session id for this shard
|
|
40
|
+
*/
|
|
41
|
+
sessionId: string;
|
|
42
|
+
/**
|
|
43
|
+
* The total number of shards at the time of this shard identifying
|
|
44
|
+
*/
|
|
45
|
+
shardCount: number;
|
|
46
|
+
/**
|
|
47
|
+
* The id of the shard
|
|
48
|
+
*/
|
|
49
|
+
shardId: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Required options for the WebSocketManager
|
|
54
|
+
*/
|
|
55
|
+
export interface RequiredWebSocketManagerOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Function for retrieving the information returned by the `/gateway/bot` endpoint.
|
|
58
|
+
* We recommend using a REST client that respects Discord's rate limits, such as `@ovencord/rest`.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const rest = new REST().setToken(process.env.DISCORD_TOKEN);
|
|
63
|
+
* const manager = new WebSocketManager({
|
|
64
|
+
* token: process.env.DISCORD_TOKEN,
|
|
65
|
+
* fetchGatewayInformation() {
|
|
66
|
+
* return rest.get(Routes.gatewayBot()) as Promise<RESTGetAPIGatewayBotResult>;
|
|
67
|
+
* },
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
fetchGatewayInformation(): Awaitable<RESTGetAPIGatewayBotResult>;
|
|
72
|
+
/**
|
|
73
|
+
* The intents to request
|
|
74
|
+
*/
|
|
75
|
+
intents: GatewayIntentBits | 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Optional additional configuration for the WebSocketManager
|
|
80
|
+
*/
|
|
81
|
+
export interface OptionalWebSocketManagerOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Builds an identify throttler to use for this manager's shards
|
|
84
|
+
*/
|
|
85
|
+
buildIdentifyThrottler(manager: WebSocketManager): Awaitable<IIdentifyThrottler>;
|
|
86
|
+
/**
|
|
87
|
+
* Builds the strategy to use for sharding
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const rest = new REST().setToken(process.env.DISCORD_TOKEN);
|
|
92
|
+
* const manager = new WebSocketManager({
|
|
93
|
+
* token: process.env.DISCORD_TOKEN,
|
|
94
|
+
* intents: 0, // for no intents
|
|
95
|
+
* fetchGatewayInformation() {
|
|
96
|
+
* return rest.get(Routes.gatewayBot()) as Promise<RESTGetAPIGatewayBotResult>;
|
|
97
|
+
* },
|
|
98
|
+
* buildStrategy: (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 }),
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
buildStrategy(manager: WebSocketManager): IShardingStrategy;
|
|
103
|
+
/**
|
|
104
|
+
* The transport compression method to use - mutually exclusive with `useIdentifyCompression`
|
|
105
|
+
*
|
|
106
|
+
* @defaultValue `null` (no transport compression)
|
|
107
|
+
*/
|
|
108
|
+
compression: CompressionMethod | null;
|
|
109
|
+
/**
|
|
110
|
+
* The encoding to use
|
|
111
|
+
*
|
|
112
|
+
* @defaultValue `'json'`
|
|
113
|
+
*/
|
|
114
|
+
encoding: Encoding;
|
|
115
|
+
/**
|
|
116
|
+
* How long to wait for a shard to connect before giving up
|
|
117
|
+
*/
|
|
118
|
+
handshakeTimeout: number | null;
|
|
119
|
+
/**
|
|
120
|
+
* How long to wait for a shard's HELLO packet before giving up
|
|
121
|
+
*/
|
|
122
|
+
helloTimeout: number | null;
|
|
123
|
+
/**
|
|
124
|
+
* Properties to send to the gateway when identifying
|
|
125
|
+
*/
|
|
126
|
+
identifyProperties: GatewayIdentifyProperties;
|
|
127
|
+
/**
|
|
128
|
+
* Initial presence data to send to the gateway when identifying
|
|
129
|
+
*/
|
|
130
|
+
initialPresence: GatewayPresenceUpdateData | null;
|
|
131
|
+
/**
|
|
132
|
+
* Value between 50 and 250, total number of members where the gateway will stop sending offline members in the guild member list
|
|
133
|
+
*/
|
|
134
|
+
largeThreshold: number | null;
|
|
135
|
+
/**
|
|
136
|
+
* How long to wait for a shard's READY packet before giving up
|
|
137
|
+
*/
|
|
138
|
+
readyTimeout: number | null;
|
|
139
|
+
/**
|
|
140
|
+
* Function used to retrieve session information (and attempt to resume) for a given shard
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const manager = new WebSocketManager({
|
|
145
|
+
* async retrieveSessionInfo(shardId): Awaitable<SessionInfo | null> {
|
|
146
|
+
* // Fetch this info from redis or similar
|
|
147
|
+
* return { sessionId: string, sequence: number };
|
|
148
|
+
* // Return null if no information is found
|
|
149
|
+
* },
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
retrieveSessionInfo(shardId: number): Awaitable<SessionInfo | null>;
|
|
154
|
+
/**
|
|
155
|
+
* The total number of shards across all WebsocketManagers you intend to instantiate.
|
|
156
|
+
* Use `null` to use Discord's recommended shard count
|
|
157
|
+
*/
|
|
158
|
+
shardCount: number | null;
|
|
159
|
+
/**
|
|
160
|
+
* The ids of the shards this WebSocketManager should manage.
|
|
161
|
+
* Use `null` to simply spawn 0 through `shardCount - 1`
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const manager = new WebSocketManager({
|
|
166
|
+
* shardIds: [1, 3, 7], // spawns shard 1, 3, and 7, nothing else
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* const manager = new WebSocketManager({
|
|
172
|
+
* shardIds: {
|
|
173
|
+
* start: 3,
|
|
174
|
+
* end: 6,
|
|
175
|
+
* }, // spawns shards 3, 4, 5, and 6
|
|
176
|
+
* });
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
shardIds: number[] | ShardRange | null;
|
|
180
|
+
/**
|
|
181
|
+
* The token to use for identifying with the gateway
|
|
182
|
+
*
|
|
183
|
+
* If not provided, the token must be set using {@link WebSocketManager.setToken}
|
|
184
|
+
*/
|
|
185
|
+
token: string;
|
|
186
|
+
/**
|
|
187
|
+
* Function used to store session information for a given shard
|
|
188
|
+
*/
|
|
189
|
+
updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Whether to use the `compress` option when identifying
|
|
192
|
+
*
|
|
193
|
+
* @defaultValue `false`
|
|
194
|
+
*/
|
|
195
|
+
useIdentifyCompression: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* The gateway version to use
|
|
198
|
+
*
|
|
199
|
+
* @defaultValue `'10'`
|
|
200
|
+
*/
|
|
201
|
+
version: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface WebSocketManagerOptions extends OptionalWebSocketManagerOptions, RequiredWebSocketManagerOptions {}
|
|
205
|
+
|
|
206
|
+
export interface CreateWebSocketManagerOptions
|
|
207
|
+
extends Partial<OptionalWebSocketManagerOptions>, RequiredWebSocketManagerOptions {}
|
|
208
|
+
|
|
209
|
+
export interface ManagerShardEventsMap {
|
|
210
|
+
[WebSocketShardEvents.Closed]: [code: number, shardId: number];
|
|
211
|
+
[WebSocketShardEvents.Debug]: [message: string, shardId: number];
|
|
212
|
+
[WebSocketShardEvents.Dispatch]: [payload: GatewayDispatchPayload, shardId: number];
|
|
213
|
+
[WebSocketShardEvents.Error]: [error: Error, shardId: number];
|
|
214
|
+
[WebSocketShardEvents.Hello]: [shardId: number];
|
|
215
|
+
[WebSocketShardEvents.Ready]: [data: GatewayReadyDispatchData, shardId: number];
|
|
216
|
+
[WebSocketShardEvents.Resumed]: [shardId: number];
|
|
217
|
+
[WebSocketShardEvents.HeartbeatComplete]: [
|
|
218
|
+
stats: { ackAt: number; heartbeatAt: number; latency: number },
|
|
219
|
+
shardId: number,
|
|
220
|
+
];
|
|
221
|
+
[WebSocketShardEvents.SocketError]: [error: Error, shardId: number];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export class WebSocketManager extends AsyncEventEmitter<ManagerShardEventsMap> implements AsyncDisposable {
|
|
225
|
+
#token: string | null = null;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The options being used by this manager
|
|
229
|
+
*/
|
|
230
|
+
public readonly options: Omit<WebSocketManagerOptions, 'token'>;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Internal cache for a GET /gateway/bot result
|
|
234
|
+
*/
|
|
235
|
+
private gatewayInformation: {
|
|
236
|
+
data: APIGatewayBotInfo;
|
|
237
|
+
expiresAt: number;
|
|
238
|
+
} | null = null;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Internal cache for the shard ids
|
|
242
|
+
*/
|
|
243
|
+
private shardIds: number[] | null = null;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Strategy used to manage shards
|
|
247
|
+
*
|
|
248
|
+
* @defaultValue `SimpleShardingStrategy`
|
|
249
|
+
*/
|
|
250
|
+
private readonly strategy: IShardingStrategy;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Gets the token set for this manager. If no token is set, an error is thrown.
|
|
254
|
+
* To set the token, use {@link WebSocketManager.setToken} or pass it in the options.
|
|
255
|
+
*
|
|
256
|
+
* @remarks
|
|
257
|
+
* This getter is mostly used to pass the token to the sharding strategy internally, there's not much reason to use it.
|
|
258
|
+
*/
|
|
259
|
+
public get token(): string {
|
|
260
|
+
if (!this.#token) {
|
|
261
|
+
throw new Error('Token has not been set');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return this.#token;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public constructor(options: CreateWebSocketManagerOptions) {
|
|
268
|
+
if (typeof options.fetchGatewayInformation !== 'function') {
|
|
269
|
+
throw new TypeError('fetchGatewayInformation is required');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
super();
|
|
273
|
+
this.options = {
|
|
274
|
+
...DefaultWebSocketManagerOptions,
|
|
275
|
+
...options,
|
|
276
|
+
};
|
|
277
|
+
this.strategy = this.options.buildStrategy(this);
|
|
278
|
+
this.#token = options.token ?? null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Fetches the gateway information from Discord - or returns it from cache if available
|
|
283
|
+
*
|
|
284
|
+
* @param force - Whether to ignore the cache and force a fresh fetch
|
|
285
|
+
*/
|
|
286
|
+
public async fetchGatewayInformation(force = false) {
|
|
287
|
+
if (this.gatewayInformation) {
|
|
288
|
+
if (this.gatewayInformation.expiresAt <= Date.now()) {
|
|
289
|
+
this.gatewayInformation = null;
|
|
290
|
+
} else if (!force) {
|
|
291
|
+
return this.gatewayInformation.data;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const data = await this.options.fetchGatewayInformation();
|
|
296
|
+
|
|
297
|
+
// For single sharded bots session_start_limit.reset_after will be 0, use 5 seconds as a minimum expiration time
|
|
298
|
+
this.gatewayInformation = { data, expiresAt: Date.now() + (data.session_start_limit.reset_after || 5_000) };
|
|
299
|
+
return this.gatewayInformation.data;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Updates your total shard count on-the-fly, spawning shards as needed
|
|
304
|
+
*
|
|
305
|
+
* @param shardCount - The new shard count to use
|
|
306
|
+
*/
|
|
307
|
+
public async updateShardCount(shardCount: number | null) {
|
|
308
|
+
await this.strategy.destroy({ reason: 'User is adjusting their shards' });
|
|
309
|
+
this.options.shardCount = shardCount;
|
|
310
|
+
|
|
311
|
+
const shardIds = await this.getShardIds(true);
|
|
312
|
+
await this.strategy.spawn(shardIds);
|
|
313
|
+
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Yields the total number of shards across for your bot, accounting for Discord recommendations
|
|
319
|
+
*/
|
|
320
|
+
public async getShardCount(): Promise<number> {
|
|
321
|
+
if (this.options.shardCount) {
|
|
322
|
+
return this.options.shardCount;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const shardIds = await this.getShardIds();
|
|
326
|
+
return Math.max(...shardIds) + 1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Yields the ids of the shards this manager should manage
|
|
331
|
+
*/
|
|
332
|
+
public async getShardIds(force = false): Promise<number[]> {
|
|
333
|
+
if (this.shardIds && !force) {
|
|
334
|
+
return this.shardIds;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let shardIds: number[];
|
|
338
|
+
if (this.options.shardIds) {
|
|
339
|
+
if (Array.isArray(this.options.shardIds)) {
|
|
340
|
+
shardIds = this.options.shardIds;
|
|
341
|
+
} else {
|
|
342
|
+
const { start, end } = this.options.shardIds;
|
|
343
|
+
shardIds = [...range({ start, end: end + 1 })];
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
const data = await this.fetchGatewayInformation();
|
|
347
|
+
shardIds = [...range(this.options.shardCount ?? data.shards)];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.shardIds = shardIds;
|
|
351
|
+
return shardIds;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public async connect() {
|
|
355
|
+
const shardCount = await this.getShardCount();
|
|
356
|
+
// Spawn shards and adjust internal state
|
|
357
|
+
await this.updateShardCount(shardCount);
|
|
358
|
+
|
|
359
|
+
const shardIds = await this.getShardIds();
|
|
360
|
+
const data = await this.fetchGatewayInformation();
|
|
361
|
+
|
|
362
|
+
if (data.session_start_limit.remaining < shardIds.length) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Not enough sessions remaining to spawn ${shardIds.length} shards; only ${
|
|
365
|
+
data.session_start_limit.remaining
|
|
366
|
+
} remaining; resets at ${new Date(Date.now() + data.session_start_limit.reset_after).toISOString()}`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
await this.strategy.connect();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
public setToken(token: string): void {
|
|
374
|
+
if (this.#token) {
|
|
375
|
+
throw new Error('Token has already been set');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.#token = token;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
public destroy(options?: Omit<WebSocketShardDestroyOptions, 'recover'>) {
|
|
382
|
+
return this.strategy.destroy(options);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
public send(shardId: number, payload: GatewaySendPayload) {
|
|
386
|
+
return this.strategy.send(shardId, payload);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
public fetchStatus(): Awaitable<Collection<number, WebSocketShardStatus>> {
|
|
390
|
+
return this.strategy.fetchStatus();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
public async [Symbol.asyncDispose]() {
|
|
394
|
+
await this.destroy();
|
|
395
|
+
}
|
|
396
|
+
}
|