@discordjs/ws
Version:
Wrapper around Discord's gateway
1 lines • 108 kB
Source Map (JSON)
{"version":3,"sources":["../../../node_modules/.pnpm/tsup@8.2.4_@microsoft+api-extractor@7.43.0_@types+node@18.19.45__jiti@1.21.6_postcss@8.4.41_typescript@5.5.4_yaml@2.5.0/node_modules/tsup/assets/esm_shims.js","../src/strategies/context/IContextFetchingStrategy.ts","../src/strategies/context/SimpleContextFetchingStrategy.ts","../src/strategies/context/WorkerContextFetchingStrategy.ts","../src/strategies/sharding/WorkerShardingStrategy.ts","../src/strategies/sharding/SimpleShardingStrategy.ts","../src/ws/WebSocketShard.ts","../src/utils/constants.ts","../src/throttling/SimpleIdentifyThrottler.ts","../src/utils/WorkerBootstrapper.ts","../src/ws/WebSocketManager.ts","../src/index.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport { fileURLToPath } from 'url'\nimport path from 'path'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import type { Awaitable } from '@discordjs/util';\nimport type { APIGatewayBotInfo } from 'discord-api-types/v10';\nimport type { SessionInfo, WebSocketManager, WebSocketManagerOptions } from '../../ws/WebSocketManager.js';\n\nexport interface FetchingStrategyOptions\n\textends Omit<\n\t\tWebSocketManagerOptions,\n\t\t| 'buildIdentifyThrottler'\n\t\t| 'buildStrategy'\n\t\t| 'rest'\n\t\t| 'retrieveSessionInfo'\n\t\t| 'shardCount'\n\t\t| 'shardIds'\n\t\t| 'updateSessionInfo'\n\t> {\n\treadonly gatewayInformation: APIGatewayBotInfo;\n\treadonly shardCount: number;\n}\n\n/**\n * Strategies responsible solely for making manager information accessible\n */\nexport interface IContextFetchingStrategy {\n\treadonly options: FetchingStrategyOptions;\n\tretrieveSessionInfo(shardId: number): Awaitable<SessionInfo | null>;\n\tupdateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable<void>;\n\t/**\n\t * Resolves once the given shard should be allowed to identify\n\t * This should correctly handle the signal and reject with an abort error if the operation is aborted.\n\t * Other errors will cause the shard to reconnect.\n\t */\n\twaitForIdentify(shardId: number, signal: AbortSignal): Promise<void>;\n}\n\nexport async function managerToFetchingStrategyOptions(manager: WebSocketManager): Promise<FetchingStrategyOptions> {\n\tconst {\n\t\tbuildIdentifyThrottler,\n\t\tbuildStrategy,\n\t\tretrieveSessionInfo,\n\t\tupdateSessionInfo,\n\t\tshardCount,\n\t\tshardIds,\n\t\trest,\n\t\t...managerOptions\n\t} = manager.options;\n\n\treturn {\n\t\t...managerOptions,\n\t\ttoken: manager.token,\n\t\tgatewayInformation: await manager.fetchGatewayInformation(),\n\t\tshardCount: await manager.getShardCount(),\n\t};\n}\n","import type { IIdentifyThrottler } from '../../throttling/IIdentifyThrottler.js';\nimport type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager.js';\nimport type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy.js';\n\nexport class SimpleContextFetchingStrategy implements IContextFetchingStrategy {\n\t// This strategy assumes every shard is running under the same process - therefore we need a single\n\t// IdentifyThrottler per manager.\n\tprivate static throttlerCache = new WeakMap<WebSocketManager, IIdentifyThrottler>();\n\n\tprivate static async ensureThrottler(manager: WebSocketManager): Promise<IIdentifyThrottler> {\n\t\tconst throttler = SimpleContextFetchingStrategy.throttlerCache.get(manager);\n\t\tif (throttler) {\n\t\t\treturn throttler;\n\t\t}\n\n\t\tconst newThrottler = await manager.options.buildIdentifyThrottler(manager);\n\t\tSimpleContextFetchingStrategy.throttlerCache.set(manager, newThrottler);\n\n\t\treturn newThrottler;\n\t}\n\n\tpublic constructor(\n\t\tprivate readonly manager: WebSocketManager,\n\t\tpublic readonly options: FetchingStrategyOptions,\n\t) {}\n\n\tpublic async retrieveSessionInfo(shardId: number): Promise<SessionInfo | null> {\n\t\treturn this.manager.options.retrieveSessionInfo(shardId);\n\t}\n\n\tpublic updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null) {\n\t\treturn this.manager.options.updateSessionInfo(shardId, sessionInfo);\n\t}\n\n\tpublic async waitForIdentify(shardId: number, signal: AbortSignal): Promise<void> {\n\t\tconst throttler = await SimpleContextFetchingStrategy.ensureThrottler(this.manager);\n\t\tawait throttler.waitForIdentify(shardId, signal);\n\t}\n}\n","import { isMainThread, parentPort } from 'node:worker_threads';\nimport { Collection } from '@discordjs/collection';\nimport type { SessionInfo } from '../../ws/WebSocketManager.js';\nimport {\n\tWorkerReceivePayloadOp,\n\tWorkerSendPayloadOp,\n\ttype WorkerReceivePayload,\n\ttype WorkerSendPayload,\n} from '../sharding/WorkerShardingStrategy.js';\nimport type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy.js';\n\nexport class WorkerContextFetchingStrategy implements IContextFetchingStrategy {\n\tprivate readonly sessionPromises = new Collection<number, (session: SessionInfo | null) => void>();\n\n\tprivate readonly waitForIdentifyPromises = new Collection<\n\t\tnumber,\n\t\t{ reject(error: unknown): void; resolve(): void; signal: AbortSignal }\n\t>();\n\n\tpublic constructor(public readonly options: FetchingStrategyOptions) {\n\t\tif (isMainThread) {\n\t\t\tthrow new Error('Cannot instantiate WorkerContextFetchingStrategy on the main thread');\n\t\t}\n\n\t\tparentPort!.on('message', (payload: WorkerSendPayload) => {\n\t\t\tif (payload.op === WorkerSendPayloadOp.SessionInfoResponse) {\n\t\t\t\tthis.sessionPromises.get(payload.nonce)?.(payload.session);\n\t\t\t\tthis.sessionPromises.delete(payload.nonce);\n\t\t\t}\n\n\t\t\tif (payload.op === WorkerSendPayloadOp.ShardIdentifyResponse) {\n\t\t\t\tconst promise = this.waitForIdentifyPromises.get(payload.nonce);\n\t\t\t\tif (payload.ok) {\n\t\t\t\t\tpromise?.resolve();\n\t\t\t\t} else {\n\t\t\t\t\t// We need to make sure we reject with an abort error\n\t\t\t\t\tpromise?.reject(promise.signal.reason);\n\t\t\t\t}\n\n\t\t\t\tthis.waitForIdentifyPromises.delete(payload.nonce);\n\t\t\t}\n\t\t});\n\t}\n\n\tpublic async retrieveSessionInfo(shardId: number): Promise<SessionInfo | null> {\n\t\tconst nonce = Math.random();\n\t\tconst payload: WorkerReceivePayload = {\n\t\t\top: WorkerReceivePayloadOp.RetrieveSessionInfo,\n\t\t\tshardId,\n\t\t\tnonce,\n\t\t};\n\t\t// eslint-disable-next-line no-promise-executor-return\n\t\tconst promise = new Promise<SessionInfo | null>((resolve) => this.sessionPromises.set(nonce, resolve));\n\t\tparentPort!.postMessage(payload);\n\t\treturn promise;\n\t}\n\n\tpublic updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null) {\n\t\tconst payload: WorkerReceivePayload = {\n\t\t\top: WorkerReceivePayloadOp.UpdateSessionInfo,\n\t\t\tshardId,\n\t\t\tsession: sessionInfo,\n\t\t};\n\t\tparentPort!.postMessage(payload);\n\t}\n\n\tpublic async waitForIdentify(shardId: number, signal: AbortSignal): Promise<void> {\n\t\tconst nonce = Math.random();\n\n\t\tconst payload: WorkerReceivePayload = {\n\t\t\top: WorkerReceivePayloadOp.WaitForIdentify,\n\t\t\tnonce,\n\t\t\tshardId,\n\t\t};\n\t\tconst promise = new Promise<void>((resolve, reject) =>\n\t\t\t// eslint-disable-next-line no-promise-executor-return\n\t\t\tthis.waitForIdentifyPromises.set(nonce, { signal, resolve, reject }),\n\t\t);\n\n\t\tparentPort!.postMessage(payload);\n\n\t\tconst listener = () => {\n\t\t\tconst payload: WorkerReceivePayload = {\n\t\t\t\top: WorkerReceivePayloadOp.CancelIdentify,\n\t\t\t\tnonce,\n\t\t\t};\n\n\t\t\tparentPort!.postMessage(payload);\n\t\t};\n\n\t\tsignal.addEventListener('abort', listener);\n\n\t\ttry {\n\t\t\tawait promise;\n\t\t} finally {\n\t\t\tsignal.removeEventListener('abort', listener);\n\t\t}\n\t}\n}\n","import { once } from 'node:events';\nimport { join, isAbsolute, resolve } from 'node:path';\nimport { Worker } from 'node:worker_threads';\nimport { Collection } from '@discordjs/collection';\nimport type { GatewaySendPayload } from 'discord-api-types/v10';\nimport type { IIdentifyThrottler } from '../../throttling/IIdentifyThrottler.js';\nimport type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager.js';\nimport type {\n\tWebSocketShardDestroyOptions,\n\tWebSocketShardEvents,\n\tWebSocketShardStatus,\n} from '../../ws/WebSocketShard.js';\nimport { managerToFetchingStrategyOptions, type FetchingStrategyOptions } from '../context/IContextFetchingStrategy.js';\nimport type { IShardingStrategy } from './IShardingStrategy.js';\n\nexport interface WorkerData extends FetchingStrategyOptions {\n\tshardIds: number[];\n}\n\nexport enum WorkerSendPayloadOp {\n\tConnect,\n\tDestroy,\n\tSend,\n\tSessionInfoResponse,\n\tShardIdentifyResponse,\n\tFetchStatus,\n}\n\nexport type WorkerSendPayload =\n\t| { nonce: number; ok: boolean; op: WorkerSendPayloadOp.ShardIdentifyResponse }\n\t| { nonce: number; op: WorkerSendPayloadOp.FetchStatus; shardId: number }\n\t| { nonce: number; op: WorkerSendPayloadOp.SessionInfoResponse; session: SessionInfo | null }\n\t| { op: WorkerSendPayloadOp.Connect; shardId: number }\n\t| { op: WorkerSendPayloadOp.Destroy; options?: WebSocketShardDestroyOptions; shardId: number }\n\t| { op: WorkerSendPayloadOp.Send; payload: GatewaySendPayload; shardId: number };\n\nexport enum WorkerReceivePayloadOp {\n\tConnected,\n\tDestroyed,\n\tEvent,\n\tRetrieveSessionInfo,\n\tUpdateSessionInfo,\n\tWaitForIdentify,\n\tFetchStatusResponse,\n\tWorkerReady,\n\tCancelIdentify,\n}\n\nexport type WorkerReceivePayload =\n\t// Can't seem to get a type-safe union based off of the event, so I'm sadly leaving data as any for now\n\t| { data: any[]; event: WebSocketShardEvents; op: WorkerReceivePayloadOp.Event; shardId: number }\n\t| { nonce: number; op: WorkerReceivePayloadOp.CancelIdentify }\n\t| { nonce: number; op: WorkerReceivePayloadOp.FetchStatusResponse; status: WebSocketShardStatus }\n\t| { nonce: number; op: WorkerReceivePayloadOp.RetrieveSessionInfo; shardId: number }\n\t| { nonce: number; op: WorkerReceivePayloadOp.WaitForIdentify; shardId: number }\n\t| { op: WorkerReceivePayloadOp.Connected; shardId: number }\n\t| { op: WorkerReceivePayloadOp.Destroyed; shardId: number }\n\t| { op: WorkerReceivePayloadOp.UpdateSessionInfo; session: SessionInfo | null; shardId: number }\n\t| { op: WorkerReceivePayloadOp.WorkerReady };\n\n/**\n * Options for a {@link WorkerShardingStrategy}\n */\nexport interface WorkerShardingStrategyOptions {\n\t/**\n\t * Dictates how many shards should be spawned per worker thread.\n\t */\n\tshardsPerWorker: number | 'all';\n\t/**\n\t * Handles a payload not recognized by the handler.\n\t */\n\tunknownPayloadHandler?(payload: any): unknown;\n\t/**\n\t * Path to the worker file to use. The worker requires quite a bit of setup, it is recommended you leverage the {@link WorkerBootstrapper} class.\n\t */\n\tworkerPath?: string;\n}\n\n/**\n * Strategy used to spawn threads in worker_threads\n */\nexport class WorkerShardingStrategy implements IShardingStrategy {\n\tprivate readonly manager: WebSocketManager;\n\n\tprivate readonly options: WorkerShardingStrategyOptions;\n\n\t#workers: Worker[] = [];\n\n\treadonly #workerByShardId = new Collection<number, Worker>();\n\n\tprivate readonly connectPromises = new Collection<number, () => void>();\n\n\tprivate readonly destroyPromises = new Collection<number, () => void>();\n\n\tprivate readonly fetchStatusPromises = new Collection<number, (status: WebSocketShardStatus) => void>();\n\n\tprivate readonly waitForIdentifyControllers = new Collection<number, AbortController>();\n\n\tprivate throttler?: IIdentifyThrottler;\n\n\tpublic constructor(manager: WebSocketManager, options: WorkerShardingStrategyOptions) {\n\t\tthis.manager = manager;\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.spawn}\n\t */\n\tpublic async spawn(shardIds: number[]) {\n\t\tconst shardsPerWorker = this.options.shardsPerWorker === 'all' ? shardIds.length : this.options.shardsPerWorker;\n\t\tconst strategyOptions = await managerToFetchingStrategyOptions(this.manager);\n\n\t\tconst loops = Math.ceil(shardIds.length / shardsPerWorker);\n\t\tconst promises: Promise<void>[] = [];\n\n\t\tfor (let idx = 0; idx < loops; idx++) {\n\t\t\tconst slice = shardIds.slice(idx * shardsPerWorker, (idx + 1) * shardsPerWorker);\n\t\t\tconst workerData: WorkerData = {\n\t\t\t\t...strategyOptions,\n\t\t\t\tshardIds: slice,\n\t\t\t};\n\n\t\t\tpromises.push(this.setupWorker(workerData));\n\t\t}\n\n\t\tawait Promise.all(promises);\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.connect}\n\t */\n\tpublic async connect() {\n\t\tconst promises = [];\n\n\t\tfor (const [shardId, worker] of this.#workerByShardId.entries()) {\n\t\t\tconst payload: WorkerSendPayload = {\n\t\t\t\top: WorkerSendPayloadOp.Connect,\n\t\t\t\tshardId,\n\t\t\t};\n\n\t\t\t// eslint-disable-next-line no-promise-executor-return\n\t\t\tconst promise = new Promise<void>((resolve) => this.connectPromises.set(shardId, resolve));\n\t\t\tworker.postMessage(payload);\n\t\t\tpromises.push(promise);\n\t\t}\n\n\t\tawait Promise.all(promises);\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.destroy}\n\t */\n\tpublic async destroy(options: Omit<WebSocketShardDestroyOptions, 'recover'> = {}) {\n\t\tconst promises = [];\n\n\t\tfor (const [shardId, worker] of this.#workerByShardId.entries()) {\n\t\t\tconst payload: WorkerSendPayload = {\n\t\t\t\top: WorkerSendPayloadOp.Destroy,\n\t\t\t\tshardId,\n\t\t\t\toptions,\n\t\t\t};\n\n\t\t\tpromises.push(\n\t\t\t\t// eslint-disable-next-line no-promise-executor-return, promise/prefer-await-to-then\n\t\t\t\tnew Promise<void>((resolve) => this.destroyPromises.set(shardId, resolve)).then(async () => worker.terminate()),\n\t\t\t);\n\t\t\tworker.postMessage(payload);\n\t\t}\n\n\t\tthis.#workers = [];\n\t\tthis.#workerByShardId.clear();\n\n\t\tawait Promise.all(promises);\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.send}\n\t */\n\tpublic send(shardId: number, data: GatewaySendPayload) {\n\t\tconst worker = this.#workerByShardId.get(shardId);\n\t\tif (!worker) {\n\t\t\tthrow new Error(`No worker found for shard ${shardId}`);\n\t\t}\n\n\t\tconst payload: WorkerSendPayload = {\n\t\t\top: WorkerSendPayloadOp.Send,\n\t\t\tshardId,\n\t\t\tpayload: data,\n\t\t};\n\t\tworker.postMessage(payload);\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.fetchStatus}\n\t */\n\tpublic async fetchStatus() {\n\t\tconst statuses = new Collection<number, WebSocketShardStatus>();\n\n\t\tfor (const [shardId, worker] of this.#workerByShardId.entries()) {\n\t\t\tconst nonce = Math.random();\n\t\t\tconst payload: WorkerSendPayload = {\n\t\t\t\top: WorkerSendPayloadOp.FetchStatus,\n\t\t\t\tshardId,\n\t\t\t\tnonce,\n\t\t\t};\n\n\t\t\t// eslint-disable-next-line no-promise-executor-return\n\t\t\tconst promise = new Promise<WebSocketShardStatus>((resolve) => this.fetchStatusPromises.set(nonce, resolve));\n\t\t\tworker.postMessage(payload);\n\n\t\t\tconst status = await promise;\n\t\t\tstatuses.set(shardId, status);\n\t\t}\n\n\t\treturn statuses;\n\t}\n\n\tprivate async setupWorker(workerData: WorkerData) {\n\t\tconst worker = new Worker(this.resolveWorkerPath(), { workerData });\n\n\t\tawait once(worker, 'online');\n\t\t// We do this in case the user has any potentially long running code in their worker\n\t\tawait this.waitForWorkerReady(worker);\n\n\t\tworker\n\t\t\t.on('error', (err) => {\n\t\t\t\tthrow err;\n\t\t\t})\n\t\t\t.on('messageerror', (err) => {\n\t\t\t\tthrow err;\n\t\t\t})\n\t\t\t.on('message', async (payload: any) => {\n\t\t\t\tif ('op' in payload) {\n\t\t\t\t\tawait this.onMessage(worker, payload);\n\t\t\t\t} else {\n\t\t\t\t\tawait this.options.unknownPayloadHandler?.(payload);\n\t\t\t\t}\n\t\t\t});\n\n\t\tthis.#workers.push(worker);\n\t\tfor (const shardId of workerData.shardIds) {\n\t\t\tthis.#workerByShardId.set(shardId, worker);\n\t\t}\n\t}\n\n\tprivate resolveWorkerPath(): string {\n\t\tconst path = this.options.workerPath;\n\n\t\tif (!path) {\n\t\t\treturn join(__dirname, 'defaultWorker.js');\n\t\t}\n\n\t\tif (isAbsolute(path)) {\n\t\t\treturn path;\n\t\t}\n\n\t\tif (/^\\.\\.?[/\\\\]/.test(path)) {\n\t\t\treturn resolve(path);\n\t\t}\n\n\t\ttry {\n\t\t\treturn require.resolve(path);\n\t\t} catch {\n\t\t\treturn resolve(path);\n\t\t}\n\t}\n\n\tprivate async waitForWorkerReady(worker: Worker): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst handler = (payload: WorkerReceivePayload) => {\n\t\t\t\tif (payload.op === WorkerReceivePayloadOp.WorkerReady) {\n\t\t\t\t\tresolve();\n\t\t\t\t\tworker.off('message', handler);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tworker.on('message', handler);\n\t\t});\n\t}\n\n\tprivate async onMessage(worker: Worker, payload: WorkerReceivePayload) {\n\t\tswitch (payload.op) {\n\t\t\tcase WorkerReceivePayloadOp.Connected: {\n\t\t\t\tthis.connectPromises.get(payload.shardId)?.();\n\t\t\t\tthis.connectPromises.delete(payload.shardId);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.Destroyed: {\n\t\t\t\tthis.destroyPromises.get(payload.shardId)?.();\n\t\t\t\tthis.destroyPromises.delete(payload.shardId);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.Event: {\n\t\t\t\t// @ts-expect-error Event props can't be resolved properly, but they are correct\n\t\t\t\tthis.manager.emit(payload.event, ...payload.data, payload.shardId);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.RetrieveSessionInfo: {\n\t\t\t\tconst session = await this.manager.options.retrieveSessionInfo(payload.shardId);\n\t\t\t\tconst response: WorkerSendPayload = {\n\t\t\t\t\top: WorkerSendPayloadOp.SessionInfoResponse,\n\t\t\t\t\tnonce: payload.nonce,\n\t\t\t\t\tsession,\n\t\t\t\t};\n\t\t\t\tworker.postMessage(response);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.UpdateSessionInfo: {\n\t\t\t\tawait this.manager.options.updateSessionInfo(payload.shardId, payload.session);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.WaitForIdentify: {\n\t\t\t\tconst throttler = await this.ensureThrottler();\n\n\t\t\t\t// If this rejects it means we aborted, in which case we reply elsewhere.\n\t\t\t\ttry {\n\t\t\t\t\tconst controller = new AbortController();\n\t\t\t\t\tthis.waitForIdentifyControllers.set(payload.nonce, controller);\n\t\t\t\t\tawait throttler.waitForIdentify(payload.shardId, controller.signal);\n\t\t\t\t} catch {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst response: WorkerSendPayload = {\n\t\t\t\t\top: WorkerSendPayloadOp.ShardIdentifyResponse,\n\t\t\t\t\tnonce: payload.nonce,\n\t\t\t\t\tok: true,\n\t\t\t\t};\n\t\t\t\tworker.postMessage(response);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.FetchStatusResponse: {\n\t\t\t\tthis.fetchStatusPromises.get(payload.nonce)?.(payload.status);\n\t\t\t\tthis.fetchStatusPromises.delete(payload.nonce);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.WorkerReady: {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase WorkerReceivePayloadOp.CancelIdentify: {\n\t\t\t\tthis.waitForIdentifyControllers.get(payload.nonce)?.abort();\n\t\t\t\tthis.waitForIdentifyControllers.delete(payload.nonce);\n\n\t\t\t\tconst response: WorkerSendPayload = {\n\t\t\t\t\top: WorkerSendPayloadOp.ShardIdentifyResponse,\n\t\t\t\t\tnonce: payload.nonce,\n\t\t\t\t\tok: false,\n\t\t\t\t};\n\t\t\t\tworker.postMessage(response);\n\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tdefault: {\n\t\t\t\tawait this.options.unknownPayloadHandler?.(payload);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async ensureThrottler(): Promise<IIdentifyThrottler> {\n\t\tthis.throttler ??= await this.manager.options.buildIdentifyThrottler(this.manager);\n\t\treturn this.throttler;\n\t}\n}\n","import { Collection } from '@discordjs/collection';\nimport type { GatewaySendPayload } from 'discord-api-types/v10';\nimport type { WebSocketManager } from '../../ws/WebSocketManager.js';\nimport { WebSocketShard, WebSocketShardEvents, type WebSocketShardDestroyOptions } from '../../ws/WebSocketShard.js';\nimport { managerToFetchingStrategyOptions } from '../context/IContextFetchingStrategy.js';\nimport { SimpleContextFetchingStrategy } from '../context/SimpleContextFetchingStrategy.js';\nimport type { IShardingStrategy } from './IShardingStrategy.js';\n\n/**\n * Simple strategy that just spawns shards in the current process\n */\nexport class SimpleShardingStrategy implements IShardingStrategy {\n\tprivate readonly manager: WebSocketManager;\n\n\tprivate readonly shards = new Collection<number, WebSocketShard>();\n\n\tpublic constructor(manager: WebSocketManager) {\n\t\tthis.manager = manager;\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.spawn}\n\t */\n\tpublic async spawn(shardIds: number[]) {\n\t\tconst strategyOptions = await managerToFetchingStrategyOptions(this.manager);\n\n\t\tfor (const shardId of shardIds) {\n\t\t\tconst strategy = new SimpleContextFetchingStrategy(this.manager, strategyOptions);\n\t\t\tconst shard = new WebSocketShard(strategy, shardId);\n\n\t\t\tfor (const event of Object.values(WebSocketShardEvents)) {\n\t\t\t\t// @ts-expect-error Ugly casts are needed because when we try to collect all args, TS doesn't handle that nicely due to the strictness + lack of context\n\t\t\t\tshard.on(event, (...args) => this.manager.emit(event, ...args, shardId));\n\t\t\t}\n\n\t\t\tthis.shards.set(shardId, shard);\n\t\t}\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.connect}\n\t */\n\tpublic async connect() {\n\t\tconst promises = [];\n\n\t\tfor (const shard of this.shards.values()) {\n\t\t\tpromises.push(shard.connect());\n\t\t}\n\n\t\tawait Promise.all(promises);\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.destroy}\n\t */\n\tpublic async destroy(options?: Omit<WebSocketShardDestroyOptions, 'recover'>) {\n\t\tconst promises = [];\n\n\t\tfor (const shard of this.shards.values()) {\n\t\t\tpromises.push(shard.destroy(options));\n\t\t}\n\n\t\tawait Promise.all(promises);\n\t\tthis.shards.clear();\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.send}\n\t */\n\tpublic async send(shardId: number, payload: GatewaySendPayload) {\n\t\tconst shard = this.shards.get(shardId);\n\t\tif (!shard) {\n\t\t\tthrow new RangeError(`Shard ${shardId} not found`);\n\t\t}\n\n\t\treturn shard.send(payload);\n\t}\n\n\t/**\n\t * {@inheritDoc IShardingStrategy.fetchStatus}\n\t */\n\tpublic async fetchStatus() {\n\t\treturn this.shards.mapValues((shard) => shard.status);\n\t}\n}\n","import { Buffer } from 'node:buffer';\nimport { once } from 'node:events';\nimport { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { URLSearchParams } from 'node:url';\nimport { TextDecoder } from 'node:util';\nimport type * as nativeZlib from 'node:zlib';\nimport { Collection } from '@discordjs/collection';\nimport { lazy, shouldUseGlobalFetchAndWebSocket } from '@discordjs/util';\nimport { AsyncQueue } from '@sapphire/async-queue';\nimport { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\nimport {\n\tGatewayCloseCodes,\n\tGatewayDispatchEvents,\n\tGatewayOpcodes,\n\ttype GatewayDispatchPayload,\n\ttype GatewayIdentifyData,\n\ttype GatewayReadyDispatchData,\n\ttype GatewayReceivePayload,\n\ttype GatewaySendPayload,\n} from 'discord-api-types/v10';\nimport { WebSocket, type Data } from 'ws';\nimport type * as ZlibSync from 'zlib-sync';\nimport type { IContextFetchingStrategy } from '../strategies/context/IContextFetchingStrategy';\nimport {\n\tCompressionMethod,\n\tCompressionParameterMap,\n\tImportantGatewayOpcodes,\n\tgetInitialSendRateLimitState,\n} from '../utils/constants.js';\nimport type { SessionInfo } from './WebSocketManager.js';\n\n/* eslint-disable promise/prefer-await-to-then */\nconst getZlibSync = lazy(async () => import('zlib-sync').then((mod) => mod.default).catch(() => null));\nconst getNativeZlib = lazy(async () => import('node:zlib').then((mod) => mod).catch(() => null));\n/* eslint-enable promise/prefer-await-to-then */\n\nexport enum WebSocketShardEvents {\n\tClosed = 'closed',\n\tDebug = 'debug',\n\tDispatch = 'dispatch',\n\tError = 'error',\n\tHeartbeatComplete = 'heartbeat',\n\tHello = 'hello',\n\tReady = 'ready',\n\tResumed = 'resumed',\n\tSocketError = 'socketError',\n}\n\nexport enum WebSocketShardStatus {\n\tIdle,\n\tConnecting,\n\tResuming,\n\tReady,\n}\n\nexport enum WebSocketShardDestroyRecovery {\n\tReconnect,\n\tResume,\n}\n\nexport interface WebSocketShardEventsMap {\n\t[WebSocketShardEvents.Closed]: [code: number];\n\t[WebSocketShardEvents.Debug]: [message: string];\n\t[WebSocketShardEvents.Dispatch]: [payload: GatewayDispatchPayload];\n\t[WebSocketShardEvents.Error]: [error: Error];\n\t[WebSocketShardEvents.Hello]: [];\n\t[WebSocketShardEvents.Ready]: [payload: GatewayReadyDispatchData];\n\t[WebSocketShardEvents.Resumed]: [];\n\t[WebSocketShardEvents.HeartbeatComplete]: [stats: { ackAt: number; heartbeatAt: number; latency: number }];\n\t[WebSocketShardEvents.SocketError]: [error: Error];\n}\n\nexport interface WebSocketShardDestroyOptions {\n\tcode?: number;\n\treason?: string;\n\trecover?: WebSocketShardDestroyRecovery;\n}\n\nexport enum CloseCodes {\n\tNormal = 1_000,\n\tResuming = 4_200,\n}\n\nexport interface SendRateLimitState {\n\tresetAt: number;\n\tsent: number;\n}\n\nconst WebSocketConstructor: typeof WebSocket = shouldUseGlobalFetchAndWebSocket()\n\t? (globalThis as any).WebSocket\n\t: WebSocket;\n\nexport class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {\n\tprivate connection: WebSocket | null = null;\n\n\tprivate nativeInflate: nativeZlib.Inflate | null = null;\n\n\tprivate zLibSyncInflate: ZlibSync.Inflate | null = null;\n\n\t/**\n\t * @privateRemarks\n\t *\n\t * Used only for native zlib inflate, zlib-sync buffering is handled by the library itself.\n\t */\n\tprivate inflateBuffer: Buffer[] = [];\n\n\tprivate readonly textDecoder = new TextDecoder();\n\n\tprivate replayedEvents = 0;\n\n\tprivate isAck = true;\n\n\tprivate sendRateLimitState: SendRateLimitState = getInitialSendRateLimitState();\n\n\tprivate initialHeartbeatTimeoutController: AbortController | null = null;\n\n\tprivate heartbeatInterval: NodeJS.Timeout | null = null;\n\n\tprivate lastHeartbeatAt = -1;\n\n\t// Indicates whether the shard has already resolved its original connect() call\n\tprivate initialConnectResolved = false;\n\n\t// Indicates if we failed to connect to the ws url\n\tprivate failedToConnectDueToNetworkError = false;\n\n\tprivate readonly sendQueue = new AsyncQueue();\n\n\tprivate readonly timeoutAbortControllers = new Collection<WebSocketShardEvents, AbortController>();\n\n\tprivate readonly strategy: IContextFetchingStrategy;\n\n\tpublic readonly id: number;\n\n\t#status: WebSocketShardStatus = WebSocketShardStatus.Idle;\n\n\tprivate identifyCompressionEnabled = false;\n\n\t/**\n\t * @privateRemarks\n\t *\n\t * This is needed because `this.strategy.options.compression` is not an actual reflection of the compression method\n\t * used, but rather the compression method that the user wants to use. This is because the libraries could just be missing.\n\t */\n\tprivate get transportCompressionEnabled() {\n\t\treturn this.strategy.options.compression !== null && (this.nativeInflate ?? this.zLibSyncInflate) !== null;\n\t}\n\n\tpublic get status(): WebSocketShardStatus {\n\t\treturn this.#status;\n\t}\n\n\tpublic constructor(strategy: IContextFetchingStrategy, id: number) {\n\t\tsuper();\n\t\tthis.strategy = strategy;\n\t\tthis.id = id;\n\t}\n\n\tpublic async connect() {\n\t\tconst controller = new AbortController();\n\t\tlet promise;\n\n\t\tif (!this.initialConnectResolved) {\n\t\t\t// Sleep for the remaining time, but if the connection closes in the meantime, we shouldn't wait the remainder to avoid blocking the new conn\n\t\t\tpromise = Promise.race([\n\t\t\t\tonce(this, WebSocketShardEvents.Ready, { signal: controller.signal }),\n\t\t\t\tonce(this, WebSocketShardEvents.Resumed, { signal: controller.signal }),\n\t\t\t]);\n\t\t}\n\n\t\tvoid this.internalConnect();\n\n\t\ttry {\n\t\t\tawait promise;\n\t\t} catch ({ error }: any) {\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\t// cleanup hanging listeners\n\t\t\tcontroller.abort();\n\t\t}\n\n\t\tthis.initialConnectResolved = true;\n\t}\n\n\tprivate async internalConnect() {\n\t\tif (this.#status !== WebSocketShardStatus.Idle) {\n\t\t\tthrow new Error(\"Tried to connect a shard that wasn't idle\");\n\t\t}\n\n\t\tconst { version, encoding, compression, useIdentifyCompression } = this.strategy.options;\n\t\tthis.identifyCompressionEnabled = useIdentifyCompression;\n\n\t\t// eslint-disable-next-line id-length\n\t\tconst params = new URLSearchParams({ v: version, encoding });\n\t\tif (compression !== null) {\n\t\t\tif (useIdentifyCompression) {\n\t\t\t\tconsole.warn('WebSocketShard: transport compression is enabled, disabling identify compression');\n\t\t\t\tthis.identifyCompressionEnabled = false;\n\t\t\t}\n\n\t\t\tparams.append('compress', CompressionParameterMap[compression]);\n\n\t\t\tswitch (compression) {\n\t\t\t\tcase CompressionMethod.ZlibNative: {\n\t\t\t\t\tconst zlib = await getNativeZlib();\n\t\t\t\t\tif (zlib) {\n\t\t\t\t\t\tthis.inflateBuffer = [];\n\n\t\t\t\t\t\tconst inflate = zlib.createInflate({\n\t\t\t\t\t\t\tchunkSize: 65_535,\n\t\t\t\t\t\t\tflush: zlib.constants.Z_SYNC_FLUSH,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tinflate.on('data', (chunk) => {\n\t\t\t\t\t\t\tthis.inflateBuffer.push(chunk);\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tinflate.on('error', (error) => {\n\t\t\t\t\t\t\tthis.emit(WebSocketShardEvents.Error, error);\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tthis.nativeInflate = inflate;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconsole.warn('WebSocketShard: Compression is set to native but node:zlib is not available.');\n\t\t\t\t\t\tparams.delete('compress');\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase CompressionMethod.ZlibSync: {\n\t\t\t\t\tconst zlib = await getZlibSync();\n\t\t\t\t\tif (zlib) {\n\t\t\t\t\t\tthis.zLibSyncInflate = new zlib.Inflate({\n\t\t\t\t\t\t\tchunkSize: 65_535,\n\t\t\t\t\t\t\tto: 'string',\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconsole.warn('WebSocketShard: Compression is set to zlib-sync, but it is not installed.');\n\t\t\t\t\t\tparams.delete('compress');\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.identifyCompressionEnabled) {\n\t\t\tconst zlib = await getNativeZlib();\n\t\t\tif (!zlib) {\n\t\t\t\tconsole.warn('WebSocketShard: Identify compression is enabled, but node:zlib is not available.');\n\t\t\t\tthis.identifyCompressionEnabled = false;\n\t\t\t}\n\t\t}\n\n\t\tconst session = await this.strategy.retrieveSessionInfo(this.id);\n\n\t\tconst url = `${session?.resumeURL ?? this.strategy.options.gatewayInformation.url}?${params.toString()}`;\n\n\t\tthis.debug([`Connecting to ${url}`]);\n\n\t\tconst connection = new WebSocketConstructor(url, [], {\n\t\t\thandshakeTimeout: this.strategy.options.handshakeTimeout ?? undefined,\n\t\t});\n\n\t\tconnection.binaryType = 'arraybuffer';\n\n\t\tconnection.onmessage = (event) => {\n\t\t\tvoid this.onMessage(event.data, event.data instanceof ArrayBuffer);\n\t\t};\n\n\t\tconnection.onerror = (event) => {\n\t\t\tthis.onError(event.error);\n\t\t};\n\n\t\tconnection.onclose = (event) => {\n\t\t\tvoid this.onClose(event.code);\n\t\t};\n\n\t\tconnection.onopen = () => {\n\t\t\tthis.sendRateLimitState = getInitialSendRateLimitState();\n\t\t};\n\n\t\tthis.connection = connection;\n\n\t\tthis.#status = WebSocketShardStatus.Connecting;\n\n\t\tconst { ok } = await this.waitForEvent(WebSocketShardEvents.Hello, this.strategy.options.helloTimeout);\n\t\tif (!ok) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (session?.shardCount === this.strategy.options.shardCount) {\n\t\t\tawait this.resume(session);\n\t\t} else {\n\t\t\tawait this.identify();\n\t\t}\n\t}\n\n\tpublic async destroy(options: WebSocketShardDestroyOptions = {}) {\n\t\tif (this.#status === WebSocketShardStatus.Idle) {\n\t\t\tthis.debug(['Tried to destroy a shard that was idle']);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!options.code) {\n\t\t\toptions.code = options.recover === WebSocketShardDestroyRecovery.Resume ? CloseCodes.Resuming : CloseCodes.Normal;\n\t\t}\n\n\t\tthis.debug([\n\t\t\t'Destroying shard',\n\t\t\t`Reason: ${options.reason ?? 'none'}`,\n\t\t\t`Code: ${options.code}`,\n\t\t\t`Recover: ${options.recover === undefined ? 'none' : WebSocketShardDestroyRecovery[options.recover]!}`,\n\t\t]);\n\n\t\t// Reset state\n\t\tthis.isAck = true;\n\t\tif (this.heartbeatInterval) {\n\t\t\tclearInterval(this.heartbeatInterval);\n\t\t}\n\n\t\tif (this.initialHeartbeatTimeoutController) {\n\t\t\tthis.initialHeartbeatTimeoutController.abort();\n\t\t\tthis.initialHeartbeatTimeoutController = null;\n\t\t}\n\n\t\tthis.lastHeartbeatAt = -1;\n\n\t\tfor (const controller of this.timeoutAbortControllers.values()) {\n\t\t\tcontroller.abort();\n\t\t}\n\n\t\tthis.timeoutAbortControllers.clear();\n\n\t\tthis.failedToConnectDueToNetworkError = false;\n\n\t\t// Clear session state if applicable\n\t\tif (options.recover !== WebSocketShardDestroyRecovery.Resume) {\n\t\t\tawait this.strategy.updateSessionInfo(this.id, null);\n\t\t}\n\n\t\tif (this.connection) {\n\t\t\t// No longer need to listen to messages\n\t\t\tthis.connection.onmessage = null;\n\t\t\t// Prevent a reconnection loop by unbinding the main close event\n\t\t\tthis.connection.onclose = null;\n\n\t\t\tconst shouldClose = this.connection.readyState === WebSocket.OPEN;\n\n\t\t\tthis.debug([\n\t\t\t\t'Connection status during destroy',\n\t\t\t\t`Needs closing: ${shouldClose}`,\n\t\t\t\t`Ready state: ${this.connection.readyState}`,\n\t\t\t]);\n\n\t\t\tif (shouldClose) {\n\t\t\t\tlet outerResolve: () => void;\n\t\t\t\tconst promise = new Promise<void>((resolve) => {\n\t\t\t\t\touterResolve = resolve;\n\t\t\t\t});\n\n\t\t\t\tthis.connection.onclose = outerResolve!;\n\n\t\t\t\tthis.connection.close(options.code, options.reason);\n\n\t\t\t\tawait promise;\n\t\t\t\tthis.emit(WebSocketShardEvents.Closed, options.code);\n\t\t\t}\n\n\t\t\t// Lastly, remove the error event.\n\t\t\t// Doing this earlier would cause a hard crash in case an error event fired on our `close` call\n\t\t\tthis.connection.onerror = null;\n\t\t} else {\n\t\t\tthis.debug(['Destroying a shard that has no connection; please open an issue on GitHub']);\n\t\t}\n\n\t\tthis.#status = WebSocketShardStatus.Idle;\n\n\t\tif (options.recover !== undefined) {\n\t\t\t// There's cases (like no internet connection) where we immediately fail to connect,\n\t\t\t// causing a very fast and draining reconnection loop.\n\t\t\tawait sleep(500);\n\t\t\treturn this.internalConnect();\n\t\t}\n\t}\n\n\tprivate async waitForEvent(event: WebSocketShardEvents, timeoutDuration?: number | null): Promise<{ ok: boolean }> {\n\t\tthis.debug([`Waiting for event ${event} ${timeoutDuration ? `for ${timeoutDuration}ms` : 'indefinitely'}`]);\n\t\tconst timeoutController = new AbortController();\n\t\tconst timeout = timeoutDuration ? setTimeout(() => timeoutController.abort(), timeoutDuration).unref() : null;\n\n\t\tthis.timeoutAbortControllers.set(event, timeoutController);\n\n\t\tconst closeController = new AbortController();\n\n\t\ttry {\n\t\t\t// If the first promise resolves, all is well. If the 2nd promise resolves,\n\t\t\t// the shard has meanwhile closed. In that case, a destroy is already ongoing, so we just need to\n\t\t\t// return false. Meanwhile, if something rejects (error event) or the first controller is aborted,\n\t\t\t// we enter the catch block and trigger a destroy there.\n\t\t\tconst closed = await Promise.race<boolean>([\n\t\t\t\tonce(this, event, { signal: timeoutController.signal }).then(() => false),\n\t\t\t\tonce(this, WebSocketShardEvents.Closed, { signal: closeController.signal }).then(() => true),\n\t\t\t]);\n\n\t\t\treturn { ok: !closed };\n\t\t} catch {\n\t\t\t// If we're here because of other reasons, we need to destroy the shard\n\t\t\tvoid this.destroy({\n\t\t\t\tcode: CloseCodes.Normal,\n\t\t\t\treason: 'Something timed out or went wrong while waiting for an event',\n\t\t\t\trecover: WebSocketShardDestroyRecovery.Reconnect,\n\t\t\t});\n\n\t\t\treturn { ok: false };\n\t\t} finally {\n\t\t\tif (timeout) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t}\n\n\t\t\tthis.timeoutAbortControllers.delete(event);\n\n\t\t\t// Clean up the close listener to not leak memory\n\t\t\tif (!closeController.signal.aborted) {\n\t\t\t\tcloseController.abort();\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic async send(payload: GatewaySendPayload): Promise<void> {\n\t\tif (!this.connection) {\n\t\t\tthrow new Error(\"WebSocketShard wasn't connected\");\n\t\t}\n\n\t\t// Generally, the way we treat payloads is 115/60 seconds. The actual limit is 120/60, so we have a bit of leeway.\n\t\t// We use that leeway for those special payloads that we just fire with no checking, since there's no shot we ever\n\t\t// send more than 5 of those in a 60 second interval. This way we can avoid more complex queueing logic.\n\n\t\tif (ImportantGatewayOpcodes.has(payload.op)) {\n\t\t\tthis.connection.send(JSON.stringify(payload));\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.#status !== WebSocketShardStatus.Ready && !ImportantGatewayOpcodes.has(payload.op)) {\n\t\t\tthis.debug(['Tried to send a non-crucial payload before the shard was ready, waiting']);\n\t\t\t// This will throw if the shard throws an error event in the meantime, just requeue the payload\n\t\t\ttry {\n\t\t\t\tawait once(this, WebSocketShardEvents.Ready);\n\t\t\t} catch {\n\t\t\t\treturn this.send(payload);\n\t\t\t}\n\t\t}\n\n\t\tawait this.sendQueue.wait();\n\n\t\tconst now = Date.now();\n\t\tif (now >= this.sendRateLimitState.resetAt) {\n\t\t\tthis.sendRateLimitState = getInitialSendRateLimitState();\n\t\t}\n\n\t\tif (this.sendRateLimitState.sent + 1 >= 115) {\n\t\t\t// Sprinkle in a little randomness just in case.\n\t\t\tconst sleepFor = this.sendRateLimitState.resetAt - now + Math.random() * 1_500;\n\n\t\t\tthis.debug([`Was about to hit the send rate limit, sleeping for ${sleepFor}ms`]);\n\t\t\tconst controller = new AbortController();\n\n\t\t\t// Sleep for the remaining time, but if the connection closes in the meantime, we shouldn't wait the remainder to avoid blocking the new conn\n\t\t\tconst interrupted = await Promise.race([\n\t\t\t\tsleep(sleepFor).then(() => false),\n\t\t\t\tonce(this, WebSocketShardEvents.Closed, { signal: controller.signal }).then(() => true),\n\t\t\t]);\n\n\t\t\tif (interrupted) {\n\t\t\t\tthis.debug(['Connection closed while waiting for the send rate limit to reset, re-queueing payload']);\n\t\t\t\tthis.sendQueue.shift();\n\t\t\t\treturn this.send(payload);\n\t\t\t}\n\n\t\t\t// This is so the listener from the `once` call is removed\n\t\t\tcontroller.abort();\n\t\t}\n\n\t\tthis.sendRateLimitState.sent++;\n\n\t\tthis.sendQueue.shift();\n\t\tthis.connection.send(JSON.stringify(payload));\n\t}\n\n\tprivate async identify() {\n\t\tthis.debug(['Waiting for identify throttle']);\n\n\t\tconst controller = new AbortController();\n\t\tconst closeHandler = () => {\n\t\t\tcontroller.abort();\n\t\t};\n\n\t\tthis.on(WebSocketShardEvents.Closed, closeHandler);\n\n\t\ttry {\n\t\t\tawait this.strategy.waitForIdentify(this.id, controller.signal);\n\t\t} catch {\n\t\t\tif (controller.signal.aborted) {\n\t\t\t\tthis.debug(['Was waiting for an identify, but the shard closed in the meantime']);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.debug([\n\t\t\t\t'IContextFetchingStrategy#waitForIdentify threw an unknown error.',\n\t\t\t\t\"If you're using a custom strategy, this is probably nothing to worry about.\",\n\t\t\t\t\"If you're not, please open an issue on GitHub.\",\n\t\t\t]);\n\n\t\t\tawait this.destroy({\n\t\t\t\treason: 'Identify throttling logic failed',\n\t\t\t\trecover: WebSocketShardDestroyRecovery.Resume,\n\t\t\t});\n\t\t} finally {\n\t\t\tthis.off(WebSocketShardEvents.Closed, closeHandler);\n\t\t}\n\n\t\tthis.debug([\n\t\t\t'Identifying',\n\t\t\t`shard id: ${this.id.toString()}`,\n\t\t\t`shard count: ${this.strategy.options.shardCount}`,\n\t\t\t`intents: ${this.strategy.options.intents}`,\n\t\t\t`compression: ${this.transportCompressionEnabled ? CompressionParameterMap[this.strategy.options.compression!] : this.identifyCompressionEnabled ? 'identify' : 'none'}`,\n\t\t]);\n\n\t\tconst data: GatewayIdentifyData = {\n\t\t\ttoken: this.strategy.options.token,\n\t\t\tproperties: this.strategy.options.identifyProperties,\n\t\t\tintents: this.strategy.options.intents,\n\t\t\tcompress: this.identifyCompressionEnabled,\n\t\t\tshard: [this.id, this.strategy.options.shardCount],\n\t\t};\n\n\t\tif (this.strategy.options.largeThreshold) {\n\t\t\tdata.large_threshold = this.strategy.options.largeThreshold;\n\t\t}\n\n\t\tif (this.strategy.options.initialPresence) {\n\t\t\tdata.presence = this.strategy.options.initialPresence;\n\t\t}\n\n\t\tawait this.send({\n\t\t\top: GatewayOpcodes.Identify,\n\t\t\t// eslint-disable-next-line id-length\n\t\t\td: data,\n\t\t});\n\n\t\tawait this.waitForEvent(WebSocketShardEvents.Ready, this.strategy.options.readyTimeout);\n\t}\n\n\tprivate async resume(session: SessionInfo) {\n\t\tthis.debug([\n\t\t\t'Resuming session',\n\t\t\t`resume url: ${session.resumeURL}`,\n\t\t\t`sequence: ${session.sequence}`,\n\t\t\t`shard id: ${this.id.toString()}`,\n\t\t]);\n\n\t\tthis.#status = WebSocketShardStatus.Resuming;\n\t\tthis.replayedEvents = 0;\n\t\treturn this.send({\n\t\t\top: GatewayOpcodes.Resume,\n\t\t\t// eslint-disable-next-line id-length\n\t\t\td: {\n\t\t\t\ttoken: this.strategy.options.token,\n\t\t\t\tseq: session.sequence,\n\t\t\t\tsession_id: session.sessionId,\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async heartbeat(requested = false) {\n\t\tif (!this.isAck && !requested) {\n\t\t\treturn this.destroy({ reason: 'Zombie connection', recover: WebSocketShardDestroyRecovery.Resume });\n\t\t}\n\n\t\tconst session = await this.strategy.retrieveSessionInfo(this.id);\n\n\t\tawait this.send({\n\t\t\top: GatewayOpcodes.Heartbeat,\n\t\t\t// eslint-disable-next-line id-length\n\t\t\td: session?.sequence ?? null,\n\t\t});\n\n\t\tthis.lastHeartbeatAt = Date.now();\n\t\tthis.isAck = false;\n\t}\n\n\tprivate parseInflateResult(result: any): GatewayReceivePayload | null {\n\t\tif (!result) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn JSON.parse(typeof result === 'string' ? result : this.textDecoder.decode(result)) as GatewayReceivePayload;\n\t}\n\n\tprivate async unpackMessage(data: Data, isBinary: boolean): Promise<GatewayReceivePayload | null> {\n\t\t// Deal with no compression\n\t\tif (!isBinary) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(data as string) as GatewayReceivePayload;\n\t\t\t} catch {\n\t\t\t\t// This is a non-JSON payload / (at the time of writing this comment) emitted by bun wrongly interpreting custom close codes https://github.com/oven-sh/bun/issues/3392\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tconst decompressable = new Uint8Array(data as ArrayBuffer);\n\n\t\t// Deal with identify compress\n\t\tif (this.identifyCompressionEnabled) {\n\t\t\t// eslint-disable-next-line no-async-promise-executor\n\t\t\treturn new Promise(async (resolve, reject) => {\n\t\t\t\tconst zlib = (await getNativeZlib())!;\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-callbacks\n\t\t\t\tzlib.inflate(decompressable, { chunkSize: 65_535 }, (err, result) => {\n\t\t\t\t\tif (err) {\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve(JSON.parse(this.textDecoder.decode(result)) as GatewayReceivePayload);\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\n\t\t// Deal with transport compression\n\t\tif (this.transportCompressionEnabled) {\n\t\t\tconst flush =\n\t\t\t\tdecompressable.length >= 4 &&\n\t\t\t\tdecompressable.at(-4) === 0x00 &&\n\t\t\t\tdecompressable.at(-3) === 0x00 &&\n\t\t\t\tdecompressable.at(-2) === 0xff &&\n\t\t\t\tdecompressable.at(-1) === 0xff;\n\n\t\t\tif (this.nativeInflate) {\n\t\t\t\tconst doneWriting = new Promise<void>((resolve) => {\n\t\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-callbacks\n\t\t\t\t\tthis.nativeInflate!.write(decompressable, 'binary', (error) => {\n\t\t\t\t\t\tif (error) {\n\t\t\t\t\t\t\tthis.emit(WebSocketShardEvents.Error, error);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t});\n\t\t\t\t});\n\n\t\t\t\tif (!flush) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\t// This way we're ensuring the latest write has flushed and our buffer is ready\n\t\t\t\tawait doneWriting;\n\n\t\t\t\tconst result = this.parseInflateResult(Buffer.concat(this.inflateBuffer));\n\t\t\t\tthis.inflateBuffer = [];\n\n\t\t\t\treturn result;\n\t\t\t} else if (this.zLibSyncInflate) {\n\t\t\t\tconst zLibSync = (await getZlibSync())!;\n\t\t\t\tthis.zLibSyncInflate.push(Buffer.from(decompressable), flush ? zLibSync.Z_SYNC_FLUSH : zLibSync.Z_NO_FLUSH);\n\n\t\t\t\tif (this.zLibSyncInflate.err) {\n\t\t\t\t\tthis.emit(\n\t\t\t\t\t\tWebSocketShardEvents.Error,\n\t\t\t\t\t\tnew Error(`${this.zLibSyncInflate.err}${this.zLibSyncInflate.msg ? `: ${this.zLibSyncInflate.msg}` : ''}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (!flush) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst { result } = this.zLibSyncInflate;\n\t\t\t\treturn this.parseInflateResult(result);\n\t\t\t}\n\t\t}\n\n\t\tthis.debug([\n\t\t\t'Received a message we were unable to decompress',\n\t\t\t`isBinary: ${isBinary.toString()}`,\n\t\t\t`identifyCompressionEnabled: ${this.identifyCompressionEnabled.toString()}`,\n\t\t\t`inflate: ${this.transportCompressionEnabled ? CompressionMethod[this.strategy.options.compression!] : 'none'}`,\n\t\t]);\n\n\t\treturn null;\n\t}\n\n\tprivate async onMessage(data: Data, isBinary: boolean) {\n\t\tconst payload = await this.unpackMessage(data, isBinary);\n\t\tif (!payload) {\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (payload.op) {\n\t\t\tcase GatewayOpcodes.Dispatch: {\n\t\t\t\tif (this.#status === WebSocketShardStatus.Resuming) {\n\t\t\t\t\tthis.replayedEvents++;\n\t\t\t\t}\n\n\t\t\t\t// eslint-disable-next-line sonarjs/no-nested-switch\n\t\t\t\tswitch (payload.t) {\n\t\t\t\t\tcase GatewayDispatchEvents.Ready: {\n\t\t\t\t\t\tthis.#status = WebSocketShardStatus.Ready;\n\n\t\t\t\t\t\tconst session = {\n\t\t\t\t\t\t\tsequence: payload.s,\n\t\t\t\t\t\t\tsessionId: payload.d.session_id,\n\t\t\t\t\t\t\tshardId: this.id,\n\t\t\t\t\t\t\tshardCount: this.strategy.options.shardCount,\n\t\t\t\t\t\t\tresumeURL: payload.d.resume_gateway_url,\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tawait this.strategy.updateSessionInfo(this.id, session);\n\n\t\t\t\t\t\tthis.emit(WebSocketShardEvents.Ready, payload.d);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase GatewayDispatchEvents.Resumed: {\n\t\t\t\t\t\tthis.#status = WebSocketShardStatus.Ready;\n\t\t\t\t\t\tthis.debug([`Resumed and replayed ${this.replayedEvents} events`]);\n\t\t\t\t\t\tthis.emit(WebSocketShardEvents.Resumed);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst session = await this.strategy.retrieveSessionInfo(this.id);\n\t\t\t\tif (session) {\n\t\t\t\t\tif (payload.s > session.sequence) {\n\t\t\t\t\t\tawait this.strategy.updateSessionInfo(this.id, { ...session, sequence: payload.s });\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.debug([\n\t\t\t\t\t\t`Received a ${payload.t} event but no session is available. Session information cannot be re-constructed in this state without a full reconnect`,\n\t\t\t\t\t]);\n\t\t\t\t}\n\n\t\t\t\tthis.emit(WebSocketShardEvents.Dispatch, payload);\n\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase GatewayOpcodes.Heartbeat: {\n\t\t\t\tawait this.heartbeat(true);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase GatewayOpcodes.Reconnect: {\n\t\t\t\tawait this.destroy({\n\t\t\t\t\treason: 'Told to reconnect by Discord',\n\t\t\t\t\trecover: WebSocketShardDestroyRecovery.Resume,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase GatewayOpcodes.InvalidSession: {\n\t\t\t\tthis.debug([`Invalid session; will attempt to resume: ${payload.d.toString()}`]);\n\t\t\t\tconst session = await this.strategy.retrieveSessionInfo(this.id);\n\t\t\t\tif (payload.d && session) {\n\t\t\t\t\tawait this.resume(session);\n\t\t\t\t} else {\n\t\t\t\t\tawait this.destroy({\n\t\t\t\t\t\treason: 'Invalid session',\n\t\t\t\t\t\trecover: WebSocketShardDestroyRecovery.Reconnect,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase GatewayOpcodes.Hello: {\n\t\t\t\tthis.emit(WebSocketShardEvents.Hello);\n\t\t\t\tconst jitter = Math.random();\n\t\t\t\tconst firstWait = Math.floor(payload.d.heartbeat_interval * jitter);\n\t\t\t\tthis.debug([`Preparing first heartbeat of the connection with a jitter of ${jitter}; waiting ${firstWait}ms`]);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst controller = new AbortController();\n\t\t\t\t\tthis.initialHeartbeatTimeoutController = controller;\n\t\t\t\t\tawait sleep(firstWait, undefined, { signal: controller.signal });\n\t\t\t\t} catch {\n\t\t\t\t\tthis.debug(['Cancelled initial heartbeat due to #destroy being called']);\n\t\t\t\t\treturn;\n\t\t\t\t} finally {\n\t\t\t\t\tthis.initialHeartbeatTimeoutController = null;\n\t\t\t\t}\n\n\t\t\t\tawait this.heartbeat();\n\n\t\t\t\tthis.debug([`First heartbeat sent, starting to beat every ${payload.d.heartbeat_interval}ms`]);\n\t\t\t\tthis.heartbeatInterval = setInterval(() => void this.heartbeat(), payload.d.heartbeat_interval);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase GatewayOpcodes.HeartbeatAck: {\n\t\t\t\tthis.isAck = true;\n\n\t\t\t\tconst ackAt = Date.now();\n\t\t\t\tthis.emit(WebSocketShardEvents.HeartbeatComplete, {\n\t\t\t\t\tackAt,\n\t\t\t\t\theartbeatAt: this.lastHeartbeatAt,\n\t\t\t\t\tlatency: ackAt - this.lastHeartbeatAt,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate onError(error: Error) {\n\t\tthis.emit(WebSocketShardEvents.SocketError, error);\n\t\tthis.failedToConnectDueToNetworkError = true;\n\t}\n\n\tprivate async onClose(code: number) {\n\t\tthis.emit(WebSocketShardE