refactor: use eslint-config-neon for packages. (#8579)

Co-authored-by: Noel <buechler.noel@outlook.com>
This commit is contained in:
Suneet Tipirneni
2022-09-01 14:50:16 -04:00
committed by GitHub
parent 4bdb0593ae
commit edadb9fe5d
219 changed files with 2608 additions and 2053 deletions

View File

@@ -1,15 +1,16 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { REST } from '@discordjs/rest';
import { MockAgent, Interceptable } from 'undici';
import { MockAgent, type Interceptable } from 'undici';
import { beforeEach, test, vi, expect } from 'vitest';
import {
managerToFetchingStrategyOptions,
WorkerContextFetchingStrategy,
WorkerRecievePayload,
WorkerSendPayload,
WebSocketManager,
WorkerSendPayloadOp,
WorkerRecievePayloadOp,
} from '../../src';
type WorkerRecievePayload,
type WorkerSendPayload,
} from '../../src/index.js';
let mockAgent: MockAgent;
let mockPool: Interceptable;

View File

@@ -1,22 +1,24 @@
/* eslint-disable id-length */
import { setImmediate } from 'node:timers';
import { REST } from '@discordjs/rest';
import {
GatewayDispatchEvents,
GatewayDispatchPayload,
GatewayOpcodes,
GatewaySendPayload,
type GatewayDispatchPayload,
type GatewaySendPayload,
} from 'discord-api-types/v10';
import { MockAgent, Interceptable } from 'undici';
import { MockAgent, type Interceptable } from 'undici';
import { beforeEach, test, vi, expect, afterEach } from 'vitest';
import {
WorkerRecievePayload,
WorkerSendPayload,
WebSocketManager,
WorkerSendPayloadOp,
WorkerRecievePayloadOp,
WorkerShardingStrategy,
WebSocketShardEvents,
SessionInfo,
} from '../../src';
type WorkerRecievePayload,
type WorkerSendPayload,
type SessionInfo,
} from '../../src/index.js';
let mockAgent: MockAgent;
let mockPool: Interceptable;
@@ -43,6 +45,7 @@ const sessionInfo: SessionInfo = {
};
vi.mock('node:worker_threads', async () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const { EventEmitter }: typeof import('node:events') = await vi.importActual('node:events');
class MockWorker extends EventEmitter {
public constructor(...args: any[]) {
@@ -54,6 +57,7 @@ vi.mock('node:worker_threads', async () => {
}
public postMessage(message: WorkerSendPayload) {
// eslint-disable-next-line default-case
switch (message.op) {
case WorkerSendPayloadOp.Connect: {
const response: WorkerRecievePayload = {

View File

@@ -1,6 +1,6 @@
import { setTimeout as sleep } from 'node:timers/promises';
import { expect, Mock, test, vi } from 'vitest';
import { IdentifyThrottler, WebSocketManager } from '../../src';
import { expect, test, vi, type Mock } from 'vitest';
import { IdentifyThrottler, type WebSocketManager } from '../../src/index.js';
vi.mock('node:timers/promises', () => ({
setTimeout: vi.fn(),

View File

@@ -1,8 +1,8 @@
import { REST } from '@discordjs/rest';
import { APIGatewayBotInfo, GatewayOpcodes, GatewaySendPayload } from 'discord-api-types/v10';
import { MockAgent, Interceptable } from 'undici';
import { GatewayOpcodes, type APIGatewayBotInfo, type GatewaySendPayload } from 'discord-api-types/v10';
import { MockAgent, type Interceptable } from 'undici';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { IShardingStrategy, WebSocketManager } from '../../src';
import { WebSocketManager, type IShardingStrategy } from '../../src/index.js';
vi.useFakeTimers();
@@ -80,7 +80,7 @@ test('fetch gateway information', async () => {
})
.reply(fetch);
NOW.mockReturnValue(Infinity);
NOW.mockReturnValue(Number.POSITIVE_INFINITY);
const cacheExpired = await manager.fetchGatewayInformation();
expect(cacheExpired).toEqual(data);
expect(fetch).toHaveBeenCalledOnce();
@@ -171,8 +171,11 @@ test('it handles passing in both shardIds and shardCount', async () => {
test('strategies', async () => {
class MockStrategy implements IShardingStrategy {
public spawn = vi.fn();
public connect = vi.fn();
public destroy = vi.fn();
public send = vi.fn();
}
@@ -219,6 +222,7 @@ test('strategies', async () => {
await manager.destroy(destroyOptions);
expect(strategy.destroy).toHaveBeenCalledWith(destroyOptions);
// eslint-disable-next-line id-length
const send: GatewaySendPayload = { op: GatewayOpcodes.RequestGuildMembers, d: { guild_id: '1234', limit: 0 } };
await manager.send(0, send);
expect(strategy.send).toHaveBeenCalledWith(0, send);

View File

@@ -66,16 +66,10 @@
"@favware/cliff-jumper": "^1.8.7",
"@microsoft/api-extractor": "^7.29.5",
"@types/node": "^16.11.56",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"@vitest/coverage-c8": "^0.22.1",
"downlevel-dts": "^0.10.1",
"eslint": "^8.23.0",
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-tsdoc": "^0.2.16",
"eslint-config-neon": "^0.1.23",
"mock-socket": "^9.1.5",
"prettier": "^2.7.1",
"rollup-plugin-typescript2": "^0.33.0",

View File

@@ -1,14 +1,14 @@
export * from './strategies/context/IContextFetchingStrategy';
export * from './strategies/context/SimpleContextFetchingStrategy';
export * from './strategies/context/WorkerContextFetchingStrategy';
export * from './strategies/context/IContextFetchingStrategy.js';
export * from './strategies/context/SimpleContextFetchingStrategy.js';
export * from './strategies/context/WorkerContextFetchingStrategy.js';
export * from './strategies/sharding/IShardingStrategy';
export * from './strategies/sharding/SimpleShardingStrategy';
export * from './strategies/sharding/WorkerShardingStrategy';
export * from './strategies/sharding/IShardingStrategy.js';
export * from './strategies/sharding/SimpleShardingStrategy.js';
export * from './strategies/sharding/WorkerShardingStrategy.js';
export * from './utils/constants';
export * from './utils/IdentifyThrottler';
export * from './utils/utils';
export * from './utils/constants.js';
export * from './utils/IdentifyThrottler.js';
export * from './utils/utils.js';
export * from './ws/WebSocketManager';
export * from './ws/WebSocketShard';
export * from './ws/WebSocketManager.js';
export * from './ws/WebSocketShard.js';

View File

@@ -5,7 +5,7 @@ import type { SessionInfo, WebSocketManager, WebSocketManagerOptions } from '../
export interface FetchingStrategyOptions
extends Omit<
WebSocketManagerOptions,
'retrieveSessionInfo' | 'updateSessionInfo' | 'shardCount' | 'shardIds' | 'rest'
'rest' | 'retrieveSessionInfo' | 'shardCount' | 'shardIds' | 'updateSessionInfo'
> {
readonly gatewayInformation: APIGatewayBotInfo;
readonly shardCount: number;
@@ -16,11 +16,12 @@ export interface FetchingStrategyOptions
*/
export interface IContextFetchingStrategy {
readonly options: FetchingStrategyOptions;
retrieveSessionInfo: (shardId: number) => Awaitable<SessionInfo | null>;
updateSessionInfo: (shardId: number, sessionInfo: SessionInfo | null) => Awaitable<void>;
retrieveSessionInfo(shardId: number): Awaitable<SessionInfo | null>;
updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable<void>;
}
export async function managerToFetchingStrategyOptions(manager: WebSocketManager): Promise<FetchingStrategyOptions> {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { retrieveSessionInfo, updateSessionInfo, shardCount, shardIds, rest, ...managerOptions } = manager.options;
return {

View File

@@ -1,5 +1,5 @@
import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy';
import type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager';
import type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager.js';
import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy.js';
export class SimpleContextFetchingStrategy implements IContextFetchingStrategy {
public constructor(private readonly manager: WebSocketManager, public readonly options: FetchingStrategyOptions) {}

View File

@@ -1,13 +1,13 @@
import { isMainThread, parentPort } from 'node:worker_threads';
import { Collection } from '@discordjs/collection';
import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy';
import type { SessionInfo } from '../../ws/WebSocketManager';
import type { SessionInfo } from '../../ws/WebSocketManager.js';
import {
WorkerRecievePayload,
WorkerRecievePayloadOp,
WorkerSendPayload,
WorkerSendPayloadOp,
} from '../sharding/WorkerShardingStrategy';
type WorkerRecievePayload,
type WorkerSendPayload,
} from '../sharding/WorkerShardingStrategy.js';
import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy.js';
export class WorkerContextFetchingStrategy implements IContextFetchingStrategy {
private readonly sessionPromises = new Collection<number, (session: SessionInfo | null) => void>();
@@ -33,7 +33,9 @@ export class WorkerContextFetchingStrategy implements IContextFetchingStrategy {
shardId,
nonce,
};
// eslint-disable-next-line no-promise-executor-return
const promise = new Promise<SessionInfo | null>((resolve) => this.sessionPromises.set(nonce, resolve));
// eslint-disable-next-line unicorn/require-post-message-target-origin
parentPort!.postMessage(payload);
return promise;
}
@@ -44,6 +46,7 @@ export class WorkerContextFetchingStrategy implements IContextFetchingStrategy {
shardId,
session: sessionInfo,
};
// eslint-disable-next-line unicorn/require-post-message-target-origin
parentPort!.postMessage(payload);
}
}

View File

@@ -6,20 +6,20 @@ import type { WebSocketShardDestroyOptions } from '../../ws/WebSocketShard';
* Strategies responsible for spawning, initializing connections, destroying shards, and relaying events
*/
export interface IShardingStrategy {
/**
* Spawns all the shards
*/
spawn: (shardIds: number[]) => Awaitable<void>;
/**
* Initializes all the shards
*/
connect: () => Awaitable<void>;
connect(): Awaitable<void>;
/**
* Destroys all the shards
*/
destroy: (options?: Omit<WebSocketShardDestroyOptions, 'recover'>) => Awaitable<void>;
destroy(options?: Omit<WebSocketShardDestroyOptions, 'recover'>): Awaitable<void>;
/**
* Sends a payload to a shard
*/
send: (shardId: number, payload: GatewaySendPayload) => Awaitable<void>;
send(shardId: number, payload: GatewaySendPayload): Awaitable<void>;
/**
* Spawns all the shards
*/
spawn(shardIds: number[]): Awaitable<void>;
}

View File

@@ -1,17 +1,18 @@
import { Collection } from '@discordjs/collection';
import type { GatewaySendPayload } from 'discord-api-types/v10';
import type { IShardingStrategy } from './IShardingStrategy';
import { IdentifyThrottler } from '../../utils/IdentifyThrottler';
import { IdentifyThrottler } from '../../utils/IdentifyThrottler.js';
import type { WebSocketManager } from '../../ws/WebSocketManager';
import { WebSocketShard, WebSocketShardDestroyOptions, WebSocketShardEvents } from '../../ws/WebSocketShard';
import { managerToFetchingStrategyOptions } from '../context/IContextFetchingStrategy';
import { SimpleContextFetchingStrategy } from '../context/SimpleContextFetchingStrategy';
import { WebSocketShard, WebSocketShardEvents, type WebSocketShardDestroyOptions } from '../../ws/WebSocketShard.js';
import { managerToFetchingStrategyOptions } from '../context/IContextFetchingStrategy.js';
import { SimpleContextFetchingStrategy } from '../context/SimpleContextFetchingStrategy.js';
import type { IShardingStrategy } from './IShardingStrategy.js';
/**
* Simple strategy that just spawns shards in the current process
*/
export class SimpleShardingStrategy implements IShardingStrategy {
private readonly manager: WebSocketManager;
private readonly shards = new Collection<number, WebSocketShard>();
private readonly throttler: IdentifyThrottler;
@@ -30,9 +31,10 @@ export class SimpleShardingStrategy implements IShardingStrategy {
const strategy = new SimpleContextFetchingStrategy(this.manager, strategyOptions);
const shard = new WebSocketShard(strategy, shardId);
for (const event of Object.values(WebSocketShardEvents)) {
// @ts-expect-error
// @ts-expect-error: Intentional
shard.on(event, (payload) => this.manager.emit(event, { ...payload, shardId }));
}
this.shards.set(shardId, shard);
}
}
@@ -68,7 +70,7 @@ export class SimpleShardingStrategy implements IShardingStrategy {
/**
* {@inheritDoc IShardingStrategy.send}
*/
public send(shardId: number, payload: GatewaySendPayload) {
public async send(shardId: number, payload: GatewaySendPayload) {
const shard = this.shards.get(shardId);
if (!shard) throw new Error(`Shard ${shardId} not found`);
return shard.send(payload);

View File

@@ -3,11 +3,11 @@ import { join } from 'node:path';
import { Worker } from 'node:worker_threads';
import { Collection } from '@discordjs/collection';
import type { GatewaySendPayload } from 'discord-api-types/v10';
import type { IShardingStrategy } from './IShardingStrategy';
import { IdentifyThrottler } from '../../utils/IdentifyThrottler';
import { IdentifyThrottler } from '../../utils/IdentifyThrottler.js';
import type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager';
import type { WebSocketShardDestroyOptions, WebSocketShardEvents } from '../../ws/WebSocketShard';
import { FetchingStrategyOptions, managerToFetchingStrategyOptions } from '../context/IContextFetchingStrategy';
import { managerToFetchingStrategyOptions, type FetchingStrategyOptions } from '../context/IContextFetchingStrategy.js';
import type { IShardingStrategy } from './IShardingStrategy.js';
export interface WorkerData extends FetchingStrategyOptions {
shardIds: number[];
@@ -21,10 +21,10 @@ export enum WorkerSendPayloadOp {
}
export type WorkerSendPayload =
| { nonce: number; op: WorkerSendPayloadOp.SessionInfoResponse; session: SessionInfo | null }
| { op: WorkerSendPayloadOp.Connect; shardId: number }
| { op: WorkerSendPayloadOp.Destroy; shardId: number; options?: WebSocketShardDestroyOptions }
| { op: WorkerSendPayloadOp.Send; shardId: number; payload: GatewaySendPayload }
| { op: WorkerSendPayloadOp.SessionInfoResponse; nonce: number; session: SessionInfo | null };
| { op: WorkerSendPayloadOp.Destroy; options?: WebSocketShardDestroyOptions; shardId: number }
| { op: WorkerSendPayloadOp.Send; payload: GatewaySendPayload; shardId: number };
export enum WorkerRecievePayloadOp {
Connected,
@@ -35,12 +35,12 @@ export enum WorkerRecievePayloadOp {
}
export type WorkerRecievePayload =
// Can't seem to get a type-safe union based off of the event, so I'm sadly leaving data as any for now
| { data: any; event: WebSocketShardEvents; op: WorkerRecievePayloadOp.Event; shardId: number }
| { nonce: number; op: WorkerRecievePayloadOp.RetrieveSessionInfo; shardId: number }
| { op: WorkerRecievePayloadOp.Connected; shardId: number }
| { op: WorkerRecievePayloadOp.Destroyed; shardId: number }
// Can't seem to get a type-safe union based off of the event, so I'm sadly leaving data as any for now
| { op: WorkerRecievePayloadOp.Event; shardId: number; event: WebSocketShardEvents; data: any }
| { op: WorkerRecievePayloadOp.RetrieveSessionInfo; shardId: number; nonce: number }
| { op: WorkerRecievePayloadOp.UpdateSessionInfo; shardId: number; session: SessionInfo | null };
| { op: WorkerRecievePayloadOp.UpdateSessionInfo; session: SessionInfo | null; shardId: number };
/**
* Options for a {@link WorkerShardingStrategy}
@@ -57,12 +57,15 @@ export interface WorkerShardingStrategyOptions {
*/
export class WorkerShardingStrategy implements IShardingStrategy {
private readonly manager: WebSocketManager;
private readonly options: WorkerShardingStrategyOptions;
#workers: Worker[] = [];
readonly #workerByShardId = new Collection<number, Worker>();
private readonly connectPromises = new Collection<number, () => void>();
private readonly destroyPromises = new Collection<number, () => void>();
private readonly throttler: IdentifyThrottler;
@@ -98,7 +101,7 @@ export class WorkerShardingStrategy implements IShardingStrategy {
throw err;
})
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.on('message', (payload: WorkerRecievePayload) => this.onMessage(worker, payload));
.on('message', async (payload: WorkerRecievePayload) => this.onMessage(worker, payload));
this.#workers.push(worker);
for (const shardId of slice) {
@@ -123,7 +126,9 @@ export class WorkerShardingStrategy implements IShardingStrategy {
shardId,
};
// eslint-disable-next-line no-promise-executor-return
const promise = new Promise<void>((resolve) => this.connectPromises.set(shardId, resolve));
// eslint-disable-next-line unicorn/require-post-message-target-origin
worker.postMessage(payload);
promises.push(promise);
}
@@ -145,8 +150,10 @@ export class WorkerShardingStrategy implements IShardingStrategy {
};
promises.push(
new Promise<void>((resolve) => this.destroyPromises.set(shardId, resolve)).then(() => worker.terminate()),
// eslint-disable-next-line no-promise-executor-return, promise/prefer-await-to-then
new Promise<void>((resolve) => this.destroyPromises.set(shardId, resolve)).then(async () => worker.terminate()),
);
// eslint-disable-next-line unicorn/require-post-message-target-origin
worker.postMessage(payload);
}
@@ -170,10 +177,12 @@ export class WorkerShardingStrategy implements IShardingStrategy {
shardId,
payload: data,
};
// eslint-disable-next-line unicorn/require-post-message-target-origin
worker.postMessage(payload);
}
private async onMessage(worker: Worker, payload: WorkerRecievePayload) {
// eslint-disable-next-line default-case
switch (payload.op) {
case WorkerRecievePayloadOp.Connected: {
const resolve = this.connectPromises.get(payload.shardId)!;
@@ -202,6 +211,7 @@ export class WorkerShardingStrategy implements IShardingStrategy {
nonce: payload.nonce,
session,
};
// eslint-disable-next-line unicorn/require-post-message-target-origin
worker.postMessage(response);
break;
}

View File

@@ -1,14 +1,15 @@
/* eslint-disable unicorn/require-post-message-target-origin */
import { isMainThread, workerData, parentPort } from 'node:worker_threads';
import { Collection } from '@discordjs/collection';
import { WebSocketShard, WebSocketShardEvents, type WebSocketShardDestroyOptions } from '../../ws/WebSocketShard.js';
import { WorkerContextFetchingStrategy } from '../context/WorkerContextFetchingStrategy.js';
import {
WorkerData,
WorkerRecievePayload,
WorkerRecievePayloadOp,
WorkerSendPayload,
WorkerSendPayloadOp,
} from './WorkerShardingStrategy';
import { WebSocketShard, WebSocketShardDestroyOptions, WebSocketShardEvents } from '../../ws/WebSocketShard';
import { WorkerContextFetchingStrategy } from '../context/WorkerContextFetchingStrategy';
type WorkerData,
type WorkerRecievePayload,
type WorkerSendPayload,
} from './WorkerShardingStrategy.js';
if (isMainThread) {
throw new Error('Expected worker script to not be ran within the main thread');
@@ -22,6 +23,7 @@ async function connect(shardId: number) {
if (!shard) {
throw new Error(`Shard ${shardId} does not exist`);
}
await shard.connect();
}
@@ -30,13 +32,14 @@ async function destroy(shardId: number, options?: WebSocketShardDestroyOptions)
if (!shard) {
throw new Error(`Shard ${shardId} does not exist`);
}
await shard.destroy(options);
}
for (const shardId of data.shardIds) {
const shard = new WebSocketShard(new WorkerContextFetchingStrategy(data), shardId);
for (const event of Object.values(WebSocketShardEvents)) {
// @ts-expect-error
// @ts-expect-error: Event types incompatible
shard.on(event, (data) => {
const payload: WorkerRecievePayload = {
op: WorkerRecievePayloadOp.Event,
@@ -47,6 +50,7 @@ for (const shardId of data.shardIds) {
parentPort!.postMessage(payload);
});
}
shards.set(shardId, shard);
}
@@ -56,6 +60,7 @@ parentPort!
})
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.on('message', async (payload: WorkerSendPayload) => {
// eslint-disable-next-line default-case
switch (payload.op) {
case WorkerSendPayloadOp.Connect: {
await connect(payload.shardId);
@@ -73,6 +78,7 @@ parentPort!
op: WorkerRecievePayloadOp.Destroyed,
shardId: payload.shardId,
};
parentPort!.postMessage(response);
break;
}
@@ -82,6 +88,7 @@ parentPort!
if (!shard) {
throw new Error(`Shard ${payload.shardId} does not exist`);
}
await shard.send(payload.payload);
break;
}

View File

@@ -4,7 +4,7 @@ import type { WebSocketManager } from '../ws/WebSocketManager';
export class IdentifyThrottler {
private identifyState = {
remaining: 0,
resetsAt: Infinity,
resetsAt: Number.POSITIVE_INFINITY,
};
public constructor(private readonly manager: WebSocketManager) {}

View File

@@ -1,9 +1,10 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import process from 'node:process';
import { Collection } from '@discordjs/collection';
import { APIVersion, GatewayOpcodes } from 'discord-api-types/v10';
import { lazy } from './utils';
import type { OptionalWebSocketManagerOptions, SessionInfo } from '../ws/WebSocketManager';
import type { OptionalWebSocketManagerOptions, SessionInfo } from '../ws/WebSocketManager.js';
import { lazy } from './utils.js';
/**
* Valid encoding types

View File

@@ -1,6 +1,6 @@
import type { ShardRange } from '../ws/WebSocketManager';
export type Awaitable<T> = T | Promise<T>;
export type Awaitable<T> = Promise<T> | T;
/**
* Yields the numbers in the given range as an array
@@ -11,7 +11,7 @@ export type Awaitable<T> = T | Promise<T>;
* ```
*/
export function range({ start, end }: ShardRange): number[] {
return Array.from({ length: end - start + 1 }, (_, i) => i + start);
return Array.from({ length: end - start + 1 }, (_, index) => index + start);
}
/**

View File

@@ -1,26 +1,26 @@
import type { REST } from '@discordjs/rest';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
import {
APIGatewayBotInfo,
GatewayIdentifyProperties,
GatewayPresenceUpdateData,
RESTGetAPIGatewayBotResult,
GatewayIntentBits,
Routes,
GatewaySendPayload,
type APIGatewayBotInfo,
type GatewayIdentifyProperties,
type GatewayPresenceUpdateData,
type RESTGetAPIGatewayBotResult,
type GatewayIntentBits,
type GatewaySendPayload,
} from 'discord-api-types/v10';
import type { WebSocketShardDestroyOptions, WebSocketShardEventsMap } from './WebSocketShard';
import type { IShardingStrategy } from '../strategies/sharding/IShardingStrategy';
import { SimpleShardingStrategy } from '../strategies/sharding/SimpleShardingStrategy';
import { CompressionMethod, DefaultWebSocketManagerOptions, Encoding } from '../utils/constants';
import { Awaitable, range } from '../utils/utils';
import { SimpleShardingStrategy } from '../strategies/sharding/SimpleShardingStrategy.js';
import { DefaultWebSocketManagerOptions, type CompressionMethod, type Encoding } from '../utils/constants.js';
import { range, type Awaitable } from '../utils/utils.js';
import type { WebSocketShardDestroyOptions, WebSocketShardEventsMap } from './WebSocketShard.js';
/**
* Represents a range of shard ids
*/
export interface ShardRange {
start: number;
end: number;
start: number;
}
/**
@@ -28,35 +28,31 @@ export interface ShardRange {
*/
export interface SessionInfo {
/**
* Session id for this shard
* URL to use when resuming
*/
sessionId: string;
resumeURL: string;
/**
* The sequence number of the last message sent by the shard
*/
sequence: number;
/**
* The id of the shard
* Session id for this shard
*/
shardId: number;
sessionId: string;
/**
* The total number of shards at the time of this shard identifying
*/
shardCount: number;
/**
* URL to use when resuming
* The id of the shard
*/
resumeURL: string;
shardId: number;
}
/**
* Required options for the WebSocketManager
*/
export interface RequiredWebSocketManagerOptions {
/**
* The token to use for identifying with the gateway
*/
token: string;
/**
* The intents to request
*/
@@ -65,12 +61,67 @@ export interface RequiredWebSocketManagerOptions {
* The REST instance to use for fetching gateway information
*/
rest: REST;
/**
* The token to use for identifying with the gateway
*/
token: string;
}
/**
* Optional additional configuration for the WebSocketManager
*/
export interface OptionalWebSocketManagerOptions {
/**
* The compression method to use
*
* @defaultValue `null` (no compression)
*/
compression: CompressionMethod | null;
/**
* The encoding to use
*
* @defaultValue `'json'`
*/
encoding: Encoding;
/**
* How long to wait for a shard to connect before giving up
*/
handshakeTimeout: number | null;
/**
* How long to wait for a shard's HELLO packet before giving up
*/
helloTimeout: number | null;
/**
* Properties to send to the gateway when identifying
*/
identifyProperties: GatewayIdentifyProperties;
/**
* Initial presence data to send to the gateway when identifying
*/
initialPresence: GatewayPresenceUpdateData | null;
/**
* Value between 50 and 250, total number of members where the gateway will stop sending offline members in the guild member list
*/
largeThreshold: number | null;
/**
* How long to wait for a shard's READY packet before giving up
*/
readyTimeout: number | null;
/**
* Function used to retrieve session information (and attempt to resume) for a given shard
*
* @example
* ```ts
* const manager = new WebSocketManager({
* async retrieveSessionInfo(shardId): Awaitable<SessionInfo | null> {
* // Fetch this info from redis or similar
* return { sessionId: string, sequence: number };
* // Return null if no information is found
* },
* });
* ```
*/
retrieveSessionInfo(shardId: number): Awaitable<SessionInfo | null>;
/**
* The total number of shards across all WebsocketManagers you intend to instantiate.
* Use `null` to use Discord's recommended shard count
@@ -86,7 +137,6 @@ export interface OptionalWebSocketManagerOptions {
* shardIds: [1, 3, 7], // spawns shard 1, 3, and 7, nothing else
* });
* ```
*
* @example
* ```ts
* const manager = new WebSocketManager({
@@ -99,66 +149,18 @@ export interface OptionalWebSocketManagerOptions {
*/
shardIds: number[] | ShardRange | null;
/**
* Value between 50 and 250, total number of members where the gateway will stop sending offline members in the guild member list
* Function used to store session information for a given shard
*/
largeThreshold: number | null;
/**
* Initial presence data to send to the gateway when identifying
*/
initialPresence: GatewayPresenceUpdateData | null;
/**
* Properties to send to the gateway when identifying
*/
identifyProperties: GatewayIdentifyProperties;
updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable<void>;
/**
* The gateway version to use
*
* @defaultValue `'10'`
*/
version: string;
/**
* The encoding to use
* @defaultValue `'json'`
*/
encoding: Encoding;
/**
* The compression method to use
* @defaultValue `null` (no compression)
*/
compression: CompressionMethod | null;
/**
* Function used to retrieve session information (and attempt to resume) for a given shard
*
* @example
* ```ts
* const manager = new WebSocketManager({
* async retrieveSessionInfo(shardId): Awaitable<SessionInfo | null> {
* // Fetch this info from redis or similar
* return { sessionId: string, sequence: number };
* // Return null if no information is found
* },
* });
* ```
*/
retrieveSessionInfo: (shardId: number) => Awaitable<SessionInfo | null>;
/**
* Function used to store session information for a given shard
*/
updateSessionInfo: (shardId: number, sessionInfo: SessionInfo | null) => Awaitable<void>;
/**
* How long to wait for a shard to connect before giving up
*/
handshakeTimeout: number | null;
/**
* How long to wait for a shard's HELLO packet before giving up
*/
helloTimeout: number | null;
/**
* How long to wait for a shard's READY packet before giving up
*/
readyTimeout: number | null;
}
export type WebSocketManagerOptions = RequiredWebSocketManagerOptions & OptionalWebSocketManagerOptions;
export type WebSocketManagerOptions = OptionalWebSocketManagerOptions & RequiredWebSocketManagerOptions;
export type ManagerShardEventsMap = {
[K in keyof WebSocketShardEventsMap]: [
@@ -187,11 +189,12 @@ export class WebSocketManager extends AsyncEventEmitter<ManagerShardEventsMap> {
/**
* Strategy used to manage shards
*
* @defaultValue `SimpleManagerToShardStrategy`
*/
private strategy: IShardingStrategy = new SimpleShardingStrategy(this);
public constructor(options: RequiredWebSocketManagerOptions & Partial<OptionalWebSocketManagerOptions>) {
public constructor(options: Partial<OptionalWebSocketManagerOptions> & RequiredWebSocketManagerOptions) {
super();
this.options = { ...DefaultWebSocketManagerOptions, ...options };
}
@@ -203,6 +206,7 @@ export class WebSocketManager extends AsyncEventEmitter<ManagerShardEventsMap> {
/**
* 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
*/
public async fetchGatewayInformation(force = false) {
@@ -222,6 +226,7 @@ export class WebSocketManager extends AsyncEventEmitter<ManagerShardEventsMap> {
/**
* Updates your total shard count on-the-fly, spawning shards as needed
*
* @param shardCount - The new shard count to use
*/
public async updateShardCount(shardCount: number | null) {

View File

@@ -1,6 +1,9 @@
/* eslint-disable id-length */
import { Buffer } from 'node:buffer';
import { once } from 'node:events';
import { setTimeout } from 'node:timers';
import { setTimeout, clearInterval, clearTimeout, setInterval } from 'node:timers';
import { setTimeout as sleep } from 'node:timers/promises';
import { URLSearchParams } from 'node:url';
import { TextDecoder } from 'node:util';
import { inflate } from 'node:zlib';
import { Collection } from '@discordjs/collection';
@@ -9,27 +12,28 @@ import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
import {
GatewayCloseCodes,
GatewayDispatchEvents,
GatewayDispatchPayload,
GatewayIdentifyData,
GatewayOpcodes,
GatewayReceivePayload,
GatewaySendPayload,
type GatewayDispatchPayload,
type GatewayIdentifyData,
type GatewayReceivePayload,
type GatewaySendPayload,
} from 'discord-api-types/v10';
import { RawData, WebSocket } from 'ws';
import { WebSocket, type RawData } from 'ws';
import type { Inflate } from 'zlib-sync';
import type { SessionInfo } from './WebSocketManager';
import type { IContextFetchingStrategy } from '../strategies/context/IContextFetchingStrategy';
import { ImportantGatewayOpcodes } from '../utils/constants';
import { lazy } from '../utils/utils';
import { ImportantGatewayOpcodes } from '../utils/constants.js';
import { lazy } from '../utils/utils.js';
import type { SessionInfo } from './WebSocketManager.js';
const getZlibSync = lazy(() => import('zlib-sync').then((mod) => mod.default).catch(() => null));
// eslint-disable-next-line promise/prefer-await-to-then
const getZlibSync = lazy(async () => import('zlib-sync').then((mod) => mod.default).catch(() => null));
export enum WebSocketShardEvents {
Debug = 'debug',
Dispatch = 'dispatch',
Hello = 'hello',
Ready = 'ready',
Resumed = 'resumed',
Dispatch = 'dispatch',
}
export enum WebSocketShardStatus {
@@ -54,14 +58,14 @@ export type WebSocketShardEventsMap = {
};
export interface WebSocketShardDestroyOptions {
reason?: string;
code?: number;
reason?: string;
recover?: WebSocketShardDestroyRecovery;
}
export enum CloseCodes {
Normal = 1000,
Resuming = 4200,
Normal = 1_000,
Resuming = 4_200,
}
export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
@@ -72,6 +76,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
private useIdentifyCompress = false;
private inflate: Inflate | null = null;
private readonly textDecoder = new TextDecoder();
private status: WebSocketShardStatus = WebSocketShardStatus.Idle;
@@ -86,6 +91,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
};
private heartbeatInterval: NodeJS.Timer | null = null;
private lastHeartbeatAt = -1;
private session: SessionInfo | null = null;
@@ -114,7 +120,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
if (zlib) {
params.append('compress', compression);
this.inflate = new zlib.Inflate({
chunkSize: 65535,
chunkSize: 65_535,
to: 'string',
});
} else if (!this.useIdentifyCompress) {
@@ -173,6 +179,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.lastHeartbeatAt = -1;
// Clear session state if applicable
@@ -202,6 +209,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
this.status = WebSocketShardStatus.Idle;
if (options.recover !== undefined) {
// eslint-disable-next-line consistent-return
return this.connect();
}
}
@@ -213,6 +221,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
if (timeout) {
this.timeouts.set(event, timeout);
}
await once(this, event, { signal: controller.signal });
if (timeout) {
clearTimeout(timeout);
@@ -279,7 +288,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
this.status = WebSocketShardStatus.Ready;
}
private resume(session: SessionInfo) {
private async resume(session: SessionInfo) {
this.debug(['Resuming session']);
this.status = WebSocketShardStatus.Resuming;
this.replayedEvents = 0;
@@ -293,6 +302,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
});
}
// eslint-disable-next-line consistent-return
private async heartbeat(requested = false) {
if (!this.isAck && !requested) {
return this.destroy({ reason: 'Zombie connection', recover: WebSocketShardDestroyRecovery.Resume });
@@ -307,7 +317,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
this.isAck = false;
}
private async unpackMessage(data: Buffer | ArrayBuffer, isBinary: boolean): Promise<GatewayReceivePayload | null> {
private async unpackMessage(data: ArrayBuffer | Buffer, isBinary: boolean): Promise<GatewayReceivePayload | null> {
const decompressable = new Uint8Array(data);
// Deal with no compression
@@ -318,9 +328,10 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
// Deal with identify compress
if (this.useIdentifyCompress) {
return new Promise((resolve, reject) => {
inflate(decompressable, { chunkSize: 65535 }, (err, result) => {
inflate(decompressable, { chunkSize: 65_535 }, (err, result) => {
if (err) {
return reject(err);
reject(err);
return;
}
resolve(JSON.parse(this.textDecoder.decode(result)) as GatewayReceivePayload);
@@ -368,11 +379,12 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
}
private async onMessage(data: RawData, isBinary: boolean) {
const payload = await this.unpackMessage(data as Buffer | ArrayBuffer, isBinary);
const payload = await this.unpackMessage(data as ArrayBuffer | Buffer, isBinary);
if (!payload) {
return;
}
// eslint-disable-next-line default-case
switch (payload.op) {
case GatewayOpcodes.Dispatch: {
if (this.status === WebSocketShardStatus.Ready || this.status === WebSocketShardStatus.Resuming) {
@@ -411,11 +423,9 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
}
}
if (this.session) {
if (payload.s > this.session.sequence) {
this.session.sequence = payload.s;
await this.strategy.updateSessionInfo(this.id, this.session);
}
if (this.session && payload.s > this.session.sequence) {
this.session.sequence = payload.s;
await this.strategy.updateSessionInfo(this.id, this.session);
}
break;
@@ -447,6 +457,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
recover: WebSocketShardDestroyRecovery.Reconnect,
});
}
break;
}
@@ -469,6 +480,7 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
this.emit('error', err);
}
// eslint-disable-next-line consistent-return
private async onClose(code: number) {
switch (code) {
case CloseCodes.Normal: {