UNPKG

@discordjs/ws

Version:

Wrapper around Discord's gateway

1,126 lines (1,111 loc) • 38.5 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/utils/WorkerBootstrapper.ts import { isMainThread as isMainThread2, parentPort as parentPort2, workerData } from "node:worker_threads"; import { Collection as Collection7 } from "@discordjs/collection"; // src/strategies/context/WorkerContextFetchingStrategy.ts import { isMainThread, parentPort } from "node:worker_threads"; import { Collection as Collection2 } from "@discordjs/collection"; // src/strategies/sharding/WorkerShardingStrategy.ts import { once } from "node:events"; import { join, isAbsolute, resolve } from "node:path"; import { Worker } from "node:worker_threads"; import { Collection } from "@discordjs/collection"; // src/strategies/context/IContextFetchingStrategy.ts async function managerToFetchingStrategyOptions(manager) { const { buildIdentifyThrottler, buildStrategy, retrieveSessionInfo, updateSessionInfo, shardCount, shardIds, rest, ...managerOptions } = manager.options; return { ...managerOptions, token: manager.token, gatewayInformation: await manager.fetchGatewayInformation(), shardCount: await manager.getShardCount() }; } __name(managerToFetchingStrategyOptions, "managerToFetchingStrategyOptions"); // src/strategies/context/WorkerContextFetchingStrategy.ts var WorkerContextFetchingStrategy = class { constructor(options) { this.options = options; if (isMainThread) { throw new Error("Cannot instantiate WorkerContextFetchingStrategy on the main thread"); } parentPort.on("message", (payload) => { if (payload.op === 3 /* SessionInfoResponse */) { this.sessionPromises.get(payload.nonce)?.(payload.session); this.sessionPromises.delete(payload.nonce); } if (payload.op === 4 /* ShardIdentifyResponse */) { const promise = this.waitForIdentifyPromises.get(payload.nonce); if (payload.ok) { promise?.resolve(); } else { promise?.reject(promise.signal.reason); } this.waitForIdentifyPromises.delete(payload.nonce); } }); } static { __name(this, "WorkerContextFetchingStrategy"); } sessionPromises = new Collection2(); waitForIdentifyPromises = new Collection2(); async retrieveSessionInfo(shardId) { const nonce = Math.random(); const payload = { op: 3 /* RetrieveSessionInfo */, shardId, nonce }; const promise = new Promise((resolve2) => this.sessionPromises.set(nonce, resolve2)); parentPort.postMessage(payload); return promise; } updateSessionInfo(shardId, sessionInfo) { const payload = { op: 4 /* UpdateSessionInfo */, shardId, session: sessionInfo }; parentPort.postMessage(payload); } async waitForIdentify(shardId, signal) { const nonce = Math.random(); const payload = { op: 5 /* WaitForIdentify */, nonce, shardId }; const promise = new Promise( (resolve2, reject) => ( // eslint-disable-next-line no-promise-executor-return this.waitForIdentifyPromises.set(nonce, { signal, resolve: resolve2, reject }) ) ); parentPort.postMessage(payload); const listener = /* @__PURE__ */ __name(() => { const payload2 = { op: 8 /* CancelIdentify */, nonce }; parentPort.postMessage(payload2); }, "listener"); signal.addEventListener("abort", listener); try { await promise; } finally { signal.removeEventListener("abort", listener); } } }; // src/ws/WebSocketShard.ts import { Buffer as Buffer2 } from "node:buffer"; import { once as once2 } from "node:events"; import { clearInterval, clearTimeout, setInterval, setTimeout } from "node:timers"; import { setTimeout as sleep2 } from "node:timers/promises"; import { URLSearchParams } from "node:url"; import { TextDecoder } from "node:util"; import { Collection as Collection6 } from "@discordjs/collection"; import { lazy as lazy2, shouldUseGlobalFetchAndWebSocket } from "@discordjs/util"; import { AsyncQueue as AsyncQueue2 } from "@sapphire/async-queue"; import { AsyncEventEmitter } from "@vladfrangu/async_event_emitter"; import { GatewayCloseCodes, GatewayDispatchEvents, GatewayOpcodes as GatewayOpcodes2 } from "discord-api-types/v10"; import { WebSocket } from "ws"; // src/utils/constants.ts import process from "node:process"; import { Collection as Collection5 } from "@discordjs/collection"; import { lazy } from "@discordjs/util"; import { APIVersion, GatewayOpcodes } from "discord-api-types/v10"; // src/strategies/sharding/SimpleShardingStrategy.ts import { Collection as Collection3 } from "@discordjs/collection"; // src/strategies/context/SimpleContextFetchingStrategy.ts var SimpleContextFetchingStrategy = class _SimpleContextFetchingStrategy { constructor(manager, options) { this.manager = manager; this.options = options; } static { __name(this, "SimpleContextFetchingStrategy"); } // This strategy assumes every shard is running under the same process - therefore we need a single // IdentifyThrottler per manager. static throttlerCache = /* @__PURE__ */ new WeakMap(); static async ensureThrottler(manager) { const throttler = _SimpleContextFetchingStrategy.throttlerCache.get(manager); if (throttler) { return throttler; } const newThrottler = await manager.options.buildIdentifyThrottler(manager); _SimpleContextFetchingStrategy.throttlerCache.set(manager, newThrottler); return newThrottler; } async retrieveSessionInfo(shardId) { return this.manager.options.retrieveSessionInfo(shardId); } updateSessionInfo(shardId, sessionInfo) { return this.manager.options.updateSessionInfo(shardId, sessionInfo); } async waitForIdentify(shardId, signal) { const throttler = await _SimpleContextFetchingStrategy.ensureThrottler(this.manager); await throttler.waitForIdentify(shardId, signal); } }; // src/strategies/sharding/SimpleShardingStrategy.ts var SimpleShardingStrategy = class { static { __name(this, "SimpleShardingStrategy"); } manager; shards = new Collection3(); constructor(manager) { this.manager = manager; } /** * {@inheritDoc IShardingStrategy.spawn} */ async spawn(shardIds) { const strategyOptions = await managerToFetchingStrategyOptions(this.manager); for (const shardId of shardIds) { const strategy = new SimpleContextFetchingStrategy(this.manager, strategyOptions); const shard = new WebSocketShard(strategy, shardId); for (const event of Object.values(WebSocketShardEvents)) { shard.on(event, (...args) => this.manager.emit(event, ...args, shardId)); } this.shards.set(shardId, shard); } } /** * {@inheritDoc IShardingStrategy.connect} */ async connect() { const promises = []; for (const shard of this.shards.values()) { promises.push(shard.connect()); } await Promise.all(promises); } /** * {@inheritDoc IShardingStrategy.destroy} */ async destroy(options) { const promises = []; for (const shard of this.shards.values()) { promises.push(shard.destroy(options)); } await Promise.all(promises); this.shards.clear(); } /** * {@inheritDoc IShardingStrategy.send} */ async send(shardId, payload) { const shard = this.shards.get(shardId); if (!shard) { throw new RangeError(`Shard ${shardId} not found`); } return shard.send(payload); } /** * {@inheritDoc IShardingStrategy.fetchStatus} */ async fetchStatus() { return this.shards.mapValues((shard) => shard.status); } }; // src/throttling/SimpleIdentifyThrottler.ts import { setTimeout as sleep } from "node:timers/promises"; import { Collection as Collection4 } from "@discordjs/collection"; import { AsyncQueue } from "@sapphire/async-queue"; var SimpleIdentifyThrottler = class { constructor(maxConcurrency) { this.maxConcurrency = maxConcurrency; } static { __name(this, "SimpleIdentifyThrottler"); } states = new Collection4(); /** * {@inheritDoc IIdentifyThrottler.waitForIdentify} */ async waitForIdentify(shardId, signal) { const key = shardId % this.maxConcurrency; const state = this.states.ensure(key, () => { return { queue: new AsyncQueue(), resetsAt: Number.POSITIVE_INFINITY }; }); await state.queue.wait({ signal }); try { const diff = state.resetsAt - Date.now(); if (diff <= 5e3) { const time = diff + Math.random() * 1500; await sleep(time); } state.resetsAt = Date.now() + 5e3; } finally { state.queue.shift(); } } }; // src/utils/constants.ts var CompressionMethod = /* @__PURE__ */ ((CompressionMethod2) => { CompressionMethod2[CompressionMethod2["ZlibNative"] = 0] = "ZlibNative"; CompressionMethod2[CompressionMethod2["ZlibSync"] = 1] = "ZlibSync"; return CompressionMethod2; })(CompressionMethod || {}); var DefaultDeviceProperty = `@discordjs/ws [VI]{{inject}}[/VI]`; var getDefaultSessionStore = lazy(() => new Collection5()); var CompressionParameterMap = { [0 /* ZlibNative */]: "zlib-stream", [1 /* ZlibSync */]: "zlib-stream" }; var DefaultWebSocketManagerOptions = { async buildIdentifyThrottler(manager) { const info = await manager.fetchGatewayInformation(); return new SimpleIdentifyThrottler(info.session_start_limit.max_concurrency); }, buildStrategy: /* @__PURE__ */ __name((manager) => new SimpleShardingStrategy(manager), "buildStrategy"), shardCount: null, shardIds: null, largeThreshold: null, initialPresence: null, identifyProperties: { browser: DefaultDeviceProperty, device: DefaultDeviceProperty, os: process.platform }, version: APIVersion, encoding: "json" /* JSON */, compression: null, useIdentifyCompression: false, retrieveSessionInfo(shardId) { const store = getDefaultSessionStore(); return store.get(shardId) ?? null; }, updateSessionInfo(shardId, info) { const store = getDefaultSessionStore(); if (info) { store.set(shardId, info); } else { store.delete(shardId); } }, handshakeTimeout: 3e4, helloTimeout: 6e4, readyTimeout: 15e3 }; var ImportantGatewayOpcodes = /* @__PURE__ */ new Set([ GatewayOpcodes.Heartbeat, GatewayOpcodes.Identify, GatewayOpcodes.Resume ]); function getInitialSendRateLimitState() { return { sent: 0, resetAt: Date.now() + 6e4 }; } __name(getInitialSendRateLimitState, "getInitialSendRateLimitState"); // src/ws/WebSocketShard.ts var getZlibSync = lazy2(async () => import("zlib-sync").then((mod) => mod.default).catch(() => null)); var getNativeZlib = lazy2(async () => import("node:zlib").then((mod) => mod).catch(() => null)); var WebSocketShardEvents = /* @__PURE__ */ ((WebSocketShardEvents2) => { WebSocketShardEvents2["Closed"] = "closed"; WebSocketShardEvents2["Debug"] = "debug"; WebSocketShardEvents2["Dispatch"] = "dispatch"; WebSocketShardEvents2["Error"] = "error"; WebSocketShardEvents2["HeartbeatComplete"] = "heartbeat"; WebSocketShardEvents2["Hello"] = "hello"; WebSocketShardEvents2["Ready"] = "ready"; WebSocketShardEvents2["Resumed"] = "resumed"; WebSocketShardEvents2["SocketError"] = "socketError"; return WebSocketShardEvents2; })(WebSocketShardEvents || {}); var WebSocketShardDestroyRecovery = /* @__PURE__ */ ((WebSocketShardDestroyRecovery2) => { WebSocketShardDestroyRecovery2[WebSocketShardDestroyRecovery2["Reconnect"] = 0] = "Reconnect"; WebSocketShardDestroyRecovery2[WebSocketShardDestroyRecovery2["Resume"] = 1] = "Resume"; return WebSocketShardDestroyRecovery2; })(WebSocketShardDestroyRecovery || {}); var WebSocketConstructor = shouldUseGlobalFetchAndWebSocket() ? globalThis.WebSocket : WebSocket; var WebSocketShard = class extends AsyncEventEmitter { static { __name(this, "WebSocketShard"); } connection = null; nativeInflate = null; zLibSyncInflate = null; /** * @privateRemarks * * Used only for native zlib inflate, zlib-sync buffering is handled by the library itself. */ inflateBuffer = []; textDecoder = new TextDecoder(); replayedEvents = 0; isAck = true; sendRateLimitState = getInitialSendRateLimitState(); initialHeartbeatTimeoutController = null; heartbeatInterval = null; lastHeartbeatAt = -1; // Indicates whether the shard has already resolved its original connect() call initialConnectResolved = false; // Indicates if we failed to connect to the ws url failedToConnectDueToNetworkError = false; sendQueue = new AsyncQueue2(); timeoutAbortControllers = new Collection6(); strategy; id; #status = 0 /* Idle */; identifyCompressionEnabled = false; /** * @privateRemarks * * This is needed because `this.strategy.options.compression` is not an actual reflection of the compression method * used, but rather the compression method that the user wants to use. This is because the libraries could just be missing. */ get transportCompressionEnabled() { return this.strategy.options.compression !== null && (this.nativeInflate ?? this.zLibSyncInflate) !== null; } get status() { return this.#status; } constructor(strategy, id) { super(); this.strategy = strategy; this.id = id; } async connect() { const controller = new AbortController(); let promise; if (!this.initialConnectResolved) { promise = Promise.race([ once2(this, "ready" /* Ready */, { signal: controller.signal }), once2(this, "resumed" /* Resumed */, { signal: controller.signal }) ]); } void this.internalConnect(); try { await promise; } catch ({ error }) { throw error; } finally { controller.abort(); } this.initialConnectResolved = true; } async internalConnect() { if (this.#status !== 0 /* Idle */) { throw new Error("Tried to connect a shard that wasn't idle"); } const { version, encoding, compression, useIdentifyCompression } = this.strategy.options; this.identifyCompressionEnabled = useIdentifyCompression; const params = new URLSearchParams({ v: version, encoding }); if (compression !== null) { if (useIdentifyCompression) { console.warn("WebSocketShard: transport compression is enabled, disabling identify compression"); this.identifyCompressionEnabled = false; } params.append("compress", CompressionParameterMap[compression]); switch (compression) { case 0 /* ZlibNative */: { const zlib = await getNativeZlib(); if (zlib) { this.inflateBuffer = []; const inflate = zlib.createInflate({ chunkSize: 65535, flush: zlib.constants.Z_SYNC_FLUSH }); inflate.on("data", (chunk) => { this.inflateBuffer.push(chunk); }); inflate.on("error", (error) => { this.emit("error" /* Error */, error); }); this.nativeInflate = inflate; } else { console.warn("WebSocketShard: Compression is set to native but node:zlib is not available."); params.delete("compress"); } break; } case 1 /* ZlibSync */: { const zlib = await getZlibSync(); if (zlib) { this.zLibSyncInflate = new zlib.Inflate({ chunkSize: 65535, to: "string" }); } else { console.warn("WebSocketShard: Compression is set to zlib-sync, but it is not installed."); params.delete("compress"); } break; } } } if (this.identifyCompressionEnabled) { const zlib = await getNativeZlib(); if (!zlib) { console.warn("WebSocketShard: Identify compression is enabled, but node:zlib is not available."); this.identifyCompressionEnabled = false; } } const session = await this.strategy.retrieveSessionInfo(this.id); const url = `${session?.resumeURL ?? this.strategy.options.gatewayInformation.url}?${params.toString()}`; this.debug([`Connecting to ${url}`]); const connection = new WebSocketConstructor(url, [], { handshakeTimeout: this.strategy.options.handshakeTimeout ?? void 0 }); connection.binaryType = "arraybuffer"; connection.onmessage = (event) => { void this.onMessage(event.data, event.data instanceof ArrayBuffer); }; connection.onerror = (event) => { this.onError(event.error); }; connection.onclose = (event) => { void this.onClose(event.code); }; connection.onopen = () => { this.sendRateLimitState = getInitialSendRateLimitState(); }; this.connection = connection; this.#status = 1 /* Connecting */; const { ok } = await this.waitForEvent("hello" /* Hello */, this.strategy.options.helloTimeout); if (!ok) { return; } if (session?.shardCount === this.strategy.options.shardCount) { await this.resume(session); } else { await this.identify(); } } async destroy(options = {}) { if (this.#status === 0 /* Idle */) { this.debug(["Tried to destroy a shard that was idle"]); return; } if (!options.code) { options.code = options.recover === 1 /* Resume */ ? 4200 /* Resuming */ : 1e3 /* Normal */; } this.debug([ "Destroying shard", `Reason: ${options.reason ?? "none"}`, `Code: ${options.code}`, `Recover: ${options.recover === void 0 ? "none" : WebSocketShardDestroyRecovery[options.recover]}` ]); this.isAck = true; if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } if (this.initialHeartbeatTimeoutController) { this.initialHeartbeatTimeoutController.abort(); this.initialHeartbeatTimeoutController = null; } this.lastHeartbeatAt = -1; for (const controller of this.timeoutAbortControllers.values()) { controller.abort(); } this.timeoutAbortControllers.clear(); this.failedToConnectDueToNetworkError = false; if (options.recover !== 1 /* Resume */) { await this.strategy.updateSessionInfo(this.id, null); } if (this.connection) { this.connection.onmessage = null; this.connection.onclose = null; const shouldClose = this.connection.readyState === WebSocket.OPEN; this.debug([ "Connection status during destroy", `Needs closing: ${shouldClose}`, `Ready state: ${this.connection.readyState}` ]); if (shouldClose) { let outerResolve; const promise = new Promise((resolve2) => { outerResolve = resolve2; }); this.connection.onclose = outerResolve; this.connection.close(options.code, options.reason); await promise; this.emit("closed" /* Closed */, options.code); } this.connection.onerror = null; } else { this.debug(["Destroying a shard that has no connection; please open an issue on GitHub"]); } this.#status = 0 /* Idle */; if (options.recover !== void 0) { await sleep2(500); return this.internalConnect(); } } async waitForEvent(event, timeoutDuration) { this.debug([`Waiting for event ${event} ${timeoutDuration ? `for ${timeoutDuration}ms` : "indefinitely"}`]); const timeoutController = new AbortController(); const timeout = timeoutDuration ? setTimeout(() => timeoutController.abort(), timeoutDuration).unref() : null; this.timeoutAbortControllers.set(event, timeoutController); const closeController = new AbortController(); try { const closed = await Promise.race([ once2(this, event, { signal: timeoutController.signal }).then(() => false), once2(this, "closed" /* Closed */, { signal: closeController.signal }).then(() => true) ]); return { ok: !closed }; } catch { void this.destroy({ code: 1e3 /* Normal */, reason: "Something timed out or went wrong while waiting for an event", recover: 0 /* Reconnect */ }); return { ok: false }; } finally { if (timeout) { clearTimeout(timeout); } this.timeoutAbortControllers.delete(event); if (!closeController.signal.aborted) { closeController.abort(); } } } async send(payload) { if (!this.connection) { throw new Error("WebSocketShard wasn't connected"); } if (ImportantGatewayOpcodes.has(payload.op)) { this.connection.send(JSON.stringify(payload)); return; } if (this.#status !== 3 /* Ready */ && !ImportantGatewayOpcodes.has(payload.op)) { this.debug(["Tried to send a non-crucial payload before the shard was ready, waiting"]); try { await once2(this, "ready" /* Ready */); } catch { return this.send(payload); } } await this.sendQueue.wait(); const now = Date.now(); if (now >= this.sendRateLimitState.resetAt) { this.sendRateLimitState = getInitialSendRateLimitState(); } if (this.sendRateLimitState.sent + 1 >= 115) { const sleepFor = this.sendRateLimitState.resetAt - now + Math.random() * 1500; this.debug([`Was about to hit the send rate limit, sleeping for ${sleepFor}ms`]); const controller = new AbortController(); const interrupted = await Promise.race([ sleep2(sleepFor).then(() => false), once2(this, "closed" /* Closed */, { signal: controller.signal }).then(() => true) ]); if (interrupted) { this.debug(["Connection closed while waiting for the send rate limit to reset, re-queueing payload"]); this.sendQueue.shift(); return this.send(payload); } controller.abort(); } this.sendRateLimitState.sent++; this.sendQueue.shift(); this.connection.send(JSON.stringify(payload)); } async identify() { this.debug(["Waiting for identify throttle"]); const controller = new AbortController(); const closeHandler = /* @__PURE__ */ __name(() => { controller.abort(); }, "closeHandler"); this.on("closed" /* Closed */, closeHandler); try { await this.strategy.waitForIdentify(this.id, controller.signal); } catch { if (controller.signal.aborted) { this.debug(["Was waiting for an identify, but the shard closed in the meantime"]); return; } this.debug([ "IContextFetchingStrategy#waitForIdentify threw an unknown error.", "If you're using a custom strategy, this is probably nothing to worry about.", "If you're not, please open an issue on GitHub." ]); await this.destroy({ reason: "Identify throttling logic failed", recover: 1 /* Resume */ }); } finally { this.off("closed" /* Closed */, closeHandler); } this.debug([ "Identifying", `shard id: ${this.id.toString()}`, `shard count: ${this.strategy.options.shardCount}`, `intents: ${this.strategy.options.intents}`, `compression: ${this.transportCompressionEnabled ? CompressionParameterMap[this.strategy.options.compression] : this.identifyCompressionEnabled ? "identify" : "none"}` ]); const data = { token: this.strategy.options.token, properties: this.strategy.options.identifyProperties, intents: this.strategy.options.intents, compress: this.identifyCompressionEnabled, shard: [this.id, this.strategy.options.shardCount] }; if (this.strategy.options.largeThreshold) { data.large_threshold = this.strategy.options.largeThreshold; } if (this.strategy.options.initialPresence) { data.presence = this.strategy.options.initialPresence; } await this.send({ op: GatewayOpcodes2.Identify, // eslint-disable-next-line id-length d: data }); await this.waitForEvent("ready" /* Ready */, this.strategy.options.readyTimeout); } async resume(session) { this.debug([ "Resuming session", `resume url: ${session.resumeURL}`, `sequence: ${session.sequence}`, `shard id: ${this.id.toString()}` ]); this.#status = 2 /* Resuming */; this.replayedEvents = 0; return this.send({ op: GatewayOpcodes2.Resume, // eslint-disable-next-line id-length d: { token: this.strategy.options.token, seq: session.sequence, session_id: session.sessionId } }); } async heartbeat(requested = false) { if (!this.isAck && !requested) { return this.destroy({ reason: "Zombie connection", recover: 1 /* Resume */ }); } const session = await this.strategy.retrieveSessionInfo(this.id); await this.send({ op: GatewayOpcodes2.Heartbeat, // eslint-disable-next-line id-length d: session?.sequence ?? null }); this.lastHeartbeatAt = Date.now(); this.isAck = false; } parseInflateResult(result) { if (!result) { return null; } return JSON.parse(typeof result === "string" ? result : this.textDecoder.decode(result)); } async unpackMessage(data, isBinary) { if (!isBinary) { try { return JSON.parse(data); } catch { return null; } } const decompressable = new Uint8Array(data); if (this.identifyCompressionEnabled) { return new Promise(async (resolve2, reject) => { const zlib = await getNativeZlib(); zlib.inflate(decompressable, { chunkSize: 65535 }, (err, result) => { if (err) { reject(err); return; } resolve2(JSON.parse(this.textDecoder.decode(result))); }); }); } if (this.transportCompressionEnabled) { const flush = decompressable.length >= 4 && decompressable.at(-4) === 0 && decompressable.at(-3) === 0 && decompressable.at(-2) === 255 && decompressable.at(-1) === 255; if (this.nativeInflate) { const doneWriting = new Promise((resolve2) => { this.nativeInflate.write(decompressable, "binary", (error) => { if (error) { this.emit("error" /* Error */, error); } resolve2(); }); }); if (!flush) { return null; } await doneWriting; const result = this.parseInflateResult(Buffer2.concat(this.inflateBuffer)); this.inflateBuffer = []; return result; } else if (this.zLibSyncInflate) { const zLibSync = await getZlibSync(); this.zLibSyncInflate.push(Buffer2.from(decompressable), flush ? zLibSync.Z_SYNC_FLUSH : zLibSync.Z_NO_FLUSH); if (this.zLibSyncInflate.err) { this.emit( "error" /* Error */, new Error(`${this.zLibSyncInflate.err}${this.zLibSyncInflate.msg ? `: ${this.zLibSyncInflate.msg}` : ""}`) ); } if (!flush) { return null; } const { result } = this.zLibSyncInflate; return this.parseInflateResult(result); } } this.debug([ "Received a message we were unable to decompress", `isBinary: ${isBinary.toString()}`, `identifyCompressionEnabled: ${this.identifyCompressionEnabled.toString()}`, `inflate: ${this.transportCompressionEnabled ? CompressionMethod[this.strategy.options.compression] : "none"}` ]); return null; } async onMessage(data, isBinary) { const payload = await this.unpackMessage(data, isBinary); if (!payload) { return; } switch (payload.op) { case GatewayOpcodes2.Dispatch: { if (this.#status === 2 /* Resuming */) { this.replayedEvents++; } switch (payload.t) { case GatewayDispatchEvents.Ready: { this.#status = 3 /* Ready */; const session2 = { sequence: payload.s, sessionId: payload.d.session_id, shardId: this.id, shardCount: this.strategy.options.shardCount, resumeURL: payload.d.resume_gateway_url }; await this.strategy.updateSessionInfo(this.id, session2); this.emit("ready" /* Ready */, payload.d); break; } case GatewayDispatchEvents.Resumed: { this.#status = 3 /* Ready */; this.debug([`Resumed and replayed ${this.replayedEvents} events`]); this.emit("resumed" /* Resumed */); break; } default: { break; } } const session = await this.strategy.retrieveSessionInfo(this.id); if (session) { if (payload.s > session.sequence) { await this.strategy.updateSessionInfo(this.id, { ...session, sequence: payload.s }); } } else { this.debug([ `Received a ${payload.t} event but no session is available. Session information cannot be re-constructed in this state without a full reconnect` ]); } this.emit("dispatch" /* Dispatch */, payload); break; } case GatewayOpcodes2.Heartbeat: { await this.heartbeat(true); break; } case GatewayOpcodes2.Reconnect: { await this.destroy({ reason: "Told to reconnect by Discord", recover: 1 /* Resume */ }); break; } case GatewayOpcodes2.InvalidSession: { this.debug([`Invalid session; will attempt to resume: ${payload.d.toString()}`]); const session = await this.strategy.retrieveSessionInfo(this.id); if (payload.d && session) { await this.resume(session); } else { await this.destroy({ reason: "Invalid session", recover: 0 /* Reconnect */ }); } break; } case GatewayOpcodes2.Hello: { this.emit("hello" /* Hello */); const jitter = Math.random(); const firstWait = Math.floor(payload.d.heartbeat_interval * jitter); this.debug([`Preparing first heartbeat of the connection with a jitter of ${jitter}; waiting ${firstWait}ms`]); try { const controller = new AbortController(); this.initialHeartbeatTimeoutController = controller; await sleep2(firstWait, void 0, { signal: controller.signal }); } catch { this.debug(["Cancelled initial heartbeat due to #destroy being called"]); return; } finally { this.initialHeartbeatTimeoutController = null; } await this.heartbeat(); this.debug([`First heartbeat sent, starting to beat every ${payload.d.heartbeat_interval}ms`]); this.heartbeatInterval = setInterval(() => void this.heartbeat(), payload.d.heartbeat_interval); break; } case GatewayOpcodes2.HeartbeatAck: { this.isAck = true; const ackAt = Date.now(); this.emit("heartbeat" /* HeartbeatComplete */, { ackAt, heartbeatAt: this.lastHeartbeatAt, latency: ackAt - this.lastHeartbeatAt }); break; } } } onError(error) { this.emit("socketError" /* SocketError */, error); this.failedToConnectDueToNetworkError = true; } async onClose(code) { this.emit("closed" /* Closed */, code); switch (code) { case 1e3 /* Normal */: { return this.destroy({ code, reason: "Got disconnected by Discord", recover: 0 /* Reconnect */ }); } case 4200 /* Resuming */: { break; } case GatewayCloseCodes.UnknownError: { this.debug([`An unknown error occurred: ${code}`]); return this.destroy({ code, recover: 1 /* Resume */ }); } case GatewayCloseCodes.UnknownOpcode: { this.debug(["An invalid opcode was sent to Discord."]); return this.destroy({ code, recover: 1 /* Resume */ }); } case GatewayCloseCodes.DecodeError: { this.debug(["An invalid payload was sent to Discord."]); return this.destroy({ code, recover: 1 /* Resume */ }); } case GatewayCloseCodes.NotAuthenticated: { this.debug(["A request was somehow sent before the identify/resume payload."]); return this.destroy({ code, recover: 0 /* Reconnect */ }); } case GatewayCloseCodes.AuthenticationFailed: { this.emit( "error" /* Error */, new Error("Authentication failed") ); return this.destroy({ code }); } case GatewayCloseCodes.AlreadyAuthenticated: { this.debug(["More than one auth payload was sent."]); return this.destroy({ code, recover: 0 /* Reconnect */ }); } case GatewayCloseCodes.InvalidSeq: { this.debug(["An invalid sequence was sent."]); return this.destroy({ code, recover: 0 /* Reconnect */ }); } case GatewayCloseCodes.RateLimited: { this.debug(["The WebSocket rate limit has been hit, this should never happen"]); return this.destroy({ code, recover: 0 /* Reconnect */ }); } case GatewayCloseCodes.SessionTimedOut: { this.debug(["Session timed out."]); return this.destroy({ code, recover: 1 /* Resume */ }); } case GatewayCloseCodes.InvalidShard: { this.emit("error" /* Error */, new Error("Invalid shard")); return this.destroy({ code }); } case GatewayCloseCodes.ShardingRequired: { this.emit( "error" /* Error */, new Error("Sharding is required") ); return this.destroy({ code }); } case GatewayCloseCodes.InvalidAPIVersion: { this.emit( "error" /* Error */, new Error("Used an invalid API version") ); return this.destroy({ code }); } case GatewayCloseCodes.InvalidIntents: { this.emit( "error" /* Error */, new Error("Used invalid intents") ); return this.destroy({ code }); } case GatewayCloseCodes.DisallowedIntents: { this.emit( "error" /* Error */, new Error("Used disallowed intents") ); return this.destroy({ code }); } default: { this.debug([ `The gateway closed with an unexpected code ${code}, attempting to ${this.failedToConnectDueToNetworkError ? "reconnect" : "resume"}.` ]); return this.destroy({ code, recover: this.failedToConnectDueToNetworkError ? 0 /* Reconnect */ : 1 /* Resume */ }); } } } debug(messages) { this.emit("debug" /* Debug */, messages.join("\n ")); } }; // src/utils/WorkerBootstrapper.ts var WorkerBootstrapper = class { static { __name(this, "WorkerBootstrapper"); } /** * The data passed to the worker thread */ data = workerData; /** * The shards that are managed by this worker */ shards = new Collection7(); constructor() { if (isMainThread2) { throw new Error("Expected WorkerBootstrap to not be used within the main thread"); } } /** * Helper method to initiate a shard's connection process */ async connect(shardId) { const shard = this.shards.get(shardId); if (!shard) { throw new RangeError(`Shard ${shardId} does not exist`); } await shard.connect(); } /** * Helper method to destroy a shard */ async destroy(shardId, options) { const shard = this.shards.get(shardId); if (!shard) { throw new RangeError(`Shard ${shardId} does not exist`); } await shard.destroy(options); } /** * Helper method to attach event listeners to the parentPort */ setupThreadEvents() { parentPort2.on("messageerror", (err) => { throw err; }).on("message", async (payload) => { switch (payload.op) { case 0 /* Connect */: { await this.connect(payload.shardId); const response = { op: 0 /* Connected */, shardId: payload.shardId }; parentPort2.postMessage(response); break; } case 1 /* Destroy */: { await this.destroy(payload.shardId, payload.options); const response = { op: 1 /* Destroyed */, shardId: payload.shardId }; parentPort2.postMessage(response); break; } case 2 /* Send */: { const shard = this.shards.get(payload.shardId); if (!shard) { throw new RangeError(`Shard ${payload.shardId} does not exist`); } await shard.send(payload.payload); break; } case 3 /* SessionInfoResponse */: { break; } case 4 /* ShardIdentifyResponse */: { break; } case 5 /* FetchStatus */: { const shard = this.shards.get(payload.shardId); if (!shard) { throw new Error(`Shard ${payload.shardId} does not exist`); } const response = { op: 6 /* FetchStatusResponse */, status: shard.status, nonce: payload.nonce }; parentPort2.postMessage(response); break; } } }); } /** * Bootstraps the worker thread with the provided options */ async bootstrap(options = {}) { for (const shardId of this.data.shardIds) { const shard = new WebSocketShard(new WorkerContextFetchingStrategy(this.data), shardId); for (const event of options.forwardEvents ?? Object.values(WebSocketShardEvents)) { shard.on(event, (...args) => { const payload = { op: 2 /* Event */, event, data: args, shardId }; parentPort2.postMessage(payload); }); } await options.shardCallback?.(shard); this.shards.set(shardId, shard); } this.setupThreadEvents(); const message = { op: 7 /* WorkerReady */ }; parentPort2.postMessage(message); } }; // src/strategies/sharding/defaultWorker.ts var bootstrapper = new WorkerBootstrapper(); void bootstrapper.bootstrap(); //# sourceMappingURL=defaultWorker.mjs.map