cloudstorm
Advanced tools
Comparing version 0.4.3 to 0.5.0
@@ -58,2 +58,9 @@ /// <reference types="node" /> | ||
/** | ||
* Method to grab initial connection info from Discord. | ||
* Should only be called automatically by the lib unless you are a large bot with a max_concurrency not equal to 1. | ||
* If you are a large bot, you should call this method at a rate of your own discretion to update your max_concurrency cached value to have up to date bucket info. | ||
* @returns The amount of shards the bot should spawn if set to auto. | ||
*/ | ||
fetchConnectInfo(): Promise<number>; | ||
/** | ||
* Get the gateway endpoint to connect to. | ||
@@ -64,3 +71,3 @@ * @returns String url with the Gateway Endpoint to connect to. | ||
/** | ||
* Get the GatewayData including recommended amount of shards. | ||
* Get the GatewayData including recommended amount of shards and other helpful info. | ||
* @returns Object with url and shards to use to connect to discord. | ||
@@ -67,0 +74,0 @@ */ |
@@ -10,2 +10,3 @@ "use strict"; | ||
const ShardManager_1 = __importDefault(require("./ShardManager")); | ||
const RatelimitBucket_1 = __importDefault(require("./structures/RatelimitBucket")); | ||
/** | ||
@@ -25,5 +26,3 @@ * Main class used for receiving events and interacting with the Discord gateway. | ||
largeGuildThreshold: 250, | ||
firstShardId: 0, | ||
lastShardId: 0, | ||
shardAmount: 1, | ||
shards: "auto", | ||
reconnect: true, | ||
@@ -50,7 +49,37 @@ intents: 0, | ||
async connect() { | ||
const gateway = await this.getGateway(); | ||
this._updateEndpoint(gateway); | ||
const initial = await this.fetchConnectInfo(); | ||
if (this.options.shards === "auto") | ||
this.options.totalShards = initial; | ||
this.shardManager.spawn(); | ||
} | ||
/** | ||
* Method to grab initial connection info from Discord. | ||
* Should only be called automatically by the lib unless you are a large bot with a max_concurrency not equal to 1. | ||
* If you are a large bot, you should call this method at a rate of your own discretion to update your max_concurrency cached value to have up to date bucket info. | ||
* @returns The amount of shards the bot should spawn if set to auto. | ||
*/ | ||
async fetchConnectInfo() { | ||
const gateway = await this.getGatewayBot(); | ||
this._updateEndpoint(gateway.url); | ||
const oldQueueConcurrency = []; | ||
const oldQueueIdentify = []; | ||
if (this.shardManager.concurrencyBucket && this.shardManager.concurrencyBucket.fnQueue.length) { | ||
oldQueueConcurrency.push(...this.shardManager.concurrencyBucket.fnQueue.map(i => [i.fn, i.callback])); | ||
this.shardManager.concurrencyBucket.dropQueue(); | ||
} | ||
if (this.shardManager.identifyBucket.fnQueue.length) | ||
oldQueueIdentify.push(...this.shardManager.identifyBucket.fnQueue.map(i => [i.fn, i.callback])); | ||
this.shardManager.identifyBucket.dropQueue(); | ||
this.shardManager.concurrencyBucket = new RatelimitBucket_1.default(gateway.session_start_limit.max_concurrency, 5000); | ||
this.shardManager.identifyBucket.remaining = gateway.session_start_limit.remaining; | ||
this.shardManager.identifyBucket.limitReset = gateway.session_start_limit.reset_after; | ||
for (const [fn, callback] of oldQueueConcurrency) { | ||
this.shardManager.concurrencyBucket.queue(fn).then(callback); | ||
} | ||
for (const [fn, callback] of oldQueueIdentify) { | ||
this.shardManager.identifyBucket.queue(fn).then(callback); | ||
} | ||
return gateway.shards; | ||
} | ||
/** | ||
* Get the gateway endpoint to connect to. | ||
@@ -64,3 +93,3 @@ * @returns String url with the Gateway Endpoint to connect to. | ||
/** | ||
* Get the GatewayData including recommended amount of shards. | ||
* Get the GatewayData including recommended amount of shards and other helpful info. | ||
* @returns Object with url and shards to use to connect to discord. | ||
@@ -67,0 +96,0 @@ */ |
@@ -100,2 +100,3 @@ "use strict"; | ||
case Constants_1.GATEWAY_OP_CODES.HELLO: | ||
this.client.emit("debug", `Shard ${this.id} received HELLO`); | ||
this.heartbeat(); | ||
@@ -115,4 +116,3 @@ this.heartbeatInterval = message.d.heartbeat_interval; | ||
this._trace = message.d._trace; | ||
this.identify(); | ||
this.client.emit("debug", `Shard ${this.id} received HELLO`); | ||
this.emit("queueIdentify", this.id); | ||
break; | ||
@@ -171,2 +171,3 @@ case Constants_1.GATEWAY_OP_CODES.HEARTBEAT_ACK: | ||
return this.resume(); | ||
this.client.emit("debug", `Shard ${this.id} is identifying`); | ||
this.status = "identifying"; | ||
@@ -184,3 +185,3 @@ this.emit("stateChange", "identifying"); | ||
large_threshold: this.options.largeGuildThreshold, | ||
shard: [this.id, this.options.shardAmount], | ||
shard: [this.id, this.options.totalShards || 1], | ||
intents: this.options.intents ? Intents_1.default.resolve(this.options.intents) : 0 | ||
@@ -199,2 +200,3 @@ } | ||
void this.client.emit("debug", "Client was attempting to resume when the ws was not open"); | ||
this.client.emit("debug", `Shard ${this.id} is resuming`); | ||
this.status = "resuming"; | ||
@@ -201,0 +203,0 @@ this.emit("stateChange", "resuming"); |
@@ -1,3 +0,3 @@ | ||
/// <reference types="node" /> | ||
import Shard from "./Shard"; | ||
import RatelimitBucket from "./structures/RateLimitBucket"; | ||
/** | ||
@@ -14,8 +14,4 @@ * Class used for managing shards for the user. | ||
}; | ||
connectQueue: Array<{ | ||
action: string; | ||
shard: Shard; | ||
}>; | ||
lastConnectionAttempt: number | null; | ||
connectQueueInterval: NodeJS.Timeout; | ||
identifyBucket: RatelimitBucket; | ||
concurrencyBucket: RatelimitBucket | null; | ||
static readonly default: typeof ShardManager; | ||
@@ -35,11 +31,2 @@ /** | ||
/** | ||
* Actually connect/re-identify a single shard spawned by this manager by calling it's connect() or identify() method and reset the connection timer. | ||
* @param data Object with a shard and action key. | ||
*/ | ||
private _connectShard; | ||
/** | ||
* Check if there are shards that have been spawned by this manager that are not connected yet and connect them if over 6 seconds have passed since the last attempt. | ||
*/ | ||
private _checkQueue; | ||
/** | ||
* Add event listeners to a shard to that the manager can act on received events. | ||
@@ -46,0 +33,0 @@ * @param shard Shard to add the event listeners to. |
@@ -6,2 +6,3 @@ "use strict"; | ||
const Shard_1 = __importDefault(require("./Shard")); | ||
const RateLimitBucket_1 = __importDefault(require("./structures/RateLimitBucket")); | ||
/** | ||
@@ -17,12 +18,7 @@ * Class used for managing shards for the user. | ||
constructor(client) { | ||
this.concurrencyBucket = null; | ||
this.client = client; | ||
this.options = client.options; | ||
if (!this.options.connectQueueInterval) | ||
this.options.connectQueueInterval = 1000 * 5; | ||
this.shards = {}; | ||
this.connectQueue = []; | ||
this.lastConnectionAttempt = null; | ||
this.connectQueueInterval = setInterval(() => { | ||
this._checkQueue(); | ||
}, this.options.connectQueueInterval); | ||
this.identifyBucket = new RateLimitBucket_1.default(1000, 1000 * 60 * 60 * 24, 1000 * 60 * 60 * 24); | ||
} | ||
@@ -33,9 +29,9 @@ /** | ||
spawn() { | ||
const firstShardID = this.options.firstShardId ? this.options.firstShardId : 0; | ||
const lastShardId = this.options.lastShardId ? this.options.lastShardId : 0; | ||
for (let i = firstShardID; i < lastShardId + 1; i++) { | ||
this.client.emit("debug", `Spawned shard ${i}`); | ||
this.shards[i] = new Shard_1.default(i, this.client); | ||
this.connectQueue.push({ action: "connect", shard: this.shards[i] }); | ||
this._addListener(this.shards[i]); | ||
if (!this.concurrencyBucket) | ||
throw new Error("Trying to spawn shards without calling Client.connect()"); | ||
for (const id of (this.options.shards === "auto" ? Array(this.options.totalShards).fill(0).map((_, index) => index) : this.options.shards || [0])) { | ||
this.client.emit("debug", `Spawned shard ${id}`); | ||
this.shards[id] = new Shard_1.default(id, this.client); | ||
this._addListener(this.shards[id]); | ||
this.shards[id].connector.connect(); | ||
} | ||
@@ -48,41 +44,6 @@ } | ||
for (const shardKey in this.shards) { | ||
if (this.shards[shardKey]) { | ||
const shard = this.shards[shardKey]; | ||
shard.disconnect(); | ||
} | ||
this.shards[shardKey].disconnect(); | ||
} | ||
} | ||
/** | ||
* Actually connect/re-identify a single shard spawned by this manager by calling it's connect() or identify() method and reset the connection timer. | ||
* @param data Object with a shard and action key. | ||
*/ | ||
_connectShard(data) { | ||
const { action, shard } = data; | ||
this.client.emit("debug", `${action === "connect" ? "Connecting" : "Identifying"} Shard ${shard.id} Status: ${shard.connector.status} Ready: ${shard.ready}`); | ||
if ((this.lastConnectionAttempt || 0) <= Date.now() - 6000) { | ||
if (action === "identify") { | ||
this.lastConnectionAttempt = Date.now(); | ||
shard.connector.identify(true); | ||
} | ||
else { | ||
if (shard.connector.status !== "connecting" && !shard.ready) { | ||
this.lastConnectionAttempt = Date.now(); | ||
shard.connect(); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Check if there are shards that have been spawned by this manager that are not connected yet and connect them if over 6 seconds have passed since the last attempt. | ||
*/ | ||
_checkQueue() { | ||
// this.client.emit("debug", `Checking queue Length: ${this.connectQueue.length} LastAttempt: ${this.lastConnectionAttempt} Current Time: ${Date.now()}`); | ||
if (this.connectQueue.length > 0 && ((this.lastConnectionAttempt || 0) <= Date.now() - 6000)) { | ||
const toConnect = this.connectQueue.splice(0, 1); | ||
for (const shard of toConnect) { | ||
this._connectShard(shard); | ||
} | ||
} | ||
} | ||
/** | ||
* Add event listeners to a shard to that the manager can act on received events. | ||
@@ -93,3 +54,3 @@ * @param shard Shard to add the event listeners to. | ||
shard.on("ready", (resume) => { | ||
this.shards[shard.id].ready = true; | ||
shard.ready = true; | ||
this.client.emit("debug", `Shard ${shard.id} ${resume ? "has resumed" : "is ready"}`); | ||
@@ -99,17 +60,17 @@ this.client.emit("shardReady", { id: shard.id, ready: !resume }); | ||
}); | ||
shard.on("queueIdentify", (shardId) => { | ||
var _a; | ||
if (!this.shards[shardId]) | ||
return this.client.emit("debug", `Received a queueIdentify event for shard ${shardId} but it does not exist. Was it removed?`); | ||
this.client.emit("debug", `Shard ${shardId} is ready to identify`); | ||
(_a = this.concurrencyBucket) === null || _a === void 0 ? void 0 : _a.queue(() => { | ||
this.identifyBucket.queue(() => this.shards[shardId].connector.identify()); | ||
}); | ||
}); | ||
shard.on("disconnect", (code, reason, gracefulClose) => { | ||
this.client.emit("debug", `Websocket of shard ${shard.id} closed with code ${code} and reason: ${reason ? reason : "None"}`); | ||
if (code === 1000 && gracefulClose) { | ||
this._checkDisconnect(); | ||
return; | ||
} | ||
this.connectQueue.push({ action: "connect", shard }); | ||
if (code === 1000 && gracefulClose) | ||
return this._checkDisconnect(); | ||
shard.connector.connect(); | ||
}); | ||
shard.on("queueIdentify", (shardId) => { | ||
if (!this.shards[shardId]) { | ||
this.client.emit("debug", `Received a queueIdentify event for not existing shard ${shardId}`); | ||
return; | ||
} | ||
this.connectQueue.unshift({ action: "identify", shard: this.shards[shardId] }); | ||
}); | ||
} | ||
@@ -116,0 +77,0 @@ /** |
@@ -40,3 +40,3 @@ /// <reference types="node" /> | ||
constructor(address: string, options: import("../Types").IClientWSOptions); | ||
get status(): 2 | 3 | 4 | 1; | ||
get status(): 1 | 2 | 3 | 4; | ||
connect(): Promise<void>; | ||
@@ -43,0 +43,0 @@ close(code: number, reason?: string): Promise<void>; |
@@ -7,4 +7,4 @@ /// <reference types="node" /> | ||
fnQueue: Array<{ | ||
fn: (...args: Array<any>) => any; | ||
callback: () => any; | ||
fn: () => unknown; | ||
callback: () => unknown; | ||
error: Error; | ||
@@ -15,2 +15,3 @@ }>; | ||
limitReset: number; | ||
defaultReset: number | undefined; | ||
resetTimeout: NodeJS.Timeout | null; | ||
@@ -22,4 +23,6 @@ static readonly default: typeof RatelimitBucket; | ||
* @param limitReset Timeframe in milliseconds until the ratelimit resets. | ||
* @param defaultLimit If the bucket info does not provide default values, but provides remaining, this is the limit to use after the initial reset. | ||
* @param defaultReset If the bucket info does not provide default values, but provides remaining, this is the reset to use after the initial reset. | ||
*/ | ||
constructor(limit?: number, limitReset?: number); | ||
constructor(limit?: number, limitReset?: number, defaultReset?: number); | ||
/** | ||
@@ -30,3 +33,3 @@ * Queue a function to be executed. | ||
*/ | ||
queue(fn: (...args: Array<any>) => any): Promise<any>; | ||
queue<T>(fn: () => T): Promise<T>; | ||
/** | ||
@@ -33,0 +36,0 @@ * Check if there are any functions in the queue that haven't been executed yet. |
@@ -10,4 +10,6 @@ "use strict"; | ||
* @param limitReset Timeframe in milliseconds until the ratelimit resets. | ||
* @param defaultLimit If the bucket info does not provide default values, but provides remaining, this is the limit to use after the initial reset. | ||
* @param defaultReset If the bucket info does not provide default values, but provides remaining, this is the reset to use after the initial reset. | ||
*/ | ||
constructor(limit = 5, limitReset = 5000) { | ||
constructor(limit = 5, limitReset = 5000, defaultReset) { | ||
this.fnQueue = []; | ||
@@ -18,2 +20,3 @@ this.limit = limit; | ||
this.resetTimeout = null; | ||
this.defaultReset = defaultReset; | ||
} | ||
@@ -87,2 +90,4 @@ /** | ||
this.remaining = this.limit; | ||
if (this.defaultReset) | ||
this.limitReset = this.defaultReset; | ||
if (this.resetTimeout) { | ||
@@ -89,0 +94,0 @@ clearTimeout(this.resetTimeout); |
@@ -30,9 +30,16 @@ export interface IntentFlags { | ||
largeGuildThreshold?: number; | ||
firstShardId?: number; | ||
lastShardId?: number; | ||
shardAmount?: number; | ||
/** | ||
* A note on "auto" sharding: | ||
* "auto" will always start at 0 as there is no way to know the next available shard id. | ||
* If you have more than one "cluster", you must specify an Array of shard ids. along with totalShards | ||
*/ | ||
shards?: "auto" | Array<number>; | ||
/** | ||
* Ignored and overwrote if using "auto" sharding. | ||
* The total number of shards expected across all clusters. | ||
*/ | ||
totalShards?: number; | ||
reconnect?: boolean; | ||
initialPresence?: import("discord-typings").GatewayPresenceUpdate; | ||
intents?: IntentResolvable; | ||
connectQueueInterval?: number; | ||
snowtransferInstance?: import("snowtransfer").SnowTransfer; | ||
@@ -39,0 +46,0 @@ ws?: IClientWSOptions; |
{ | ||
"name": "cloudstorm", | ||
"version": "0.4.3", | ||
"version": "0.5.0", | ||
"description": "Minimalistic Discord Gateway library", | ||
@@ -17,3 +17,3 @@ "main": "./dist/index.js", | ||
"dependencies": { | ||
"snowtransfer": "^0.4.x" | ||
"snowtransfer": "^0.4.3" | ||
}, | ||
@@ -23,9 +23,9 @@ "devDependencies": { | ||
"@types/node": "17.0.23", | ||
"@typescript-eslint/eslint-plugin": "^5.16.0", | ||
"@typescript-eslint/parser": "^5.16.0", | ||
"eslint": "^8.11.0", | ||
"@typescript-eslint/eslint-plugin": "^5.17.0", | ||
"@typescript-eslint/parser": "^5.17.0", | ||
"eslint": "^8.12.0", | ||
"typedoc": "^0.22.13", | ||
"typedoc-plugin-mdn-links": "^1.0.5", | ||
"typedoc-plugin-missing-exports": "^0.22.6", | ||
"typescript": "^4.6.2" | ||
"typescript": "^4.6.3" | ||
}, | ||
@@ -32,0 +32,0 @@ "files": [ |
@@ -50,2 +50,8 @@ # A minimal discord gateway library | ||
``` | ||
## Sharding for VERY large bots | ||
CloudStorm supports max_concurrency, but does not automatically attempt to fetch new info related to max_concurrency. You are expected to re-fetch this data at your own discretion as Discord does not recommend caching the data for extended periods as it can change as your client leaves and joins guilds and possibly cause rate limit errors. | ||
You should start your clusters 1 by 1 as rate limit info is only fetched on Client.connect or when you manually call Client.fetchConnectInfo when /gateway/bot is fetched | ||
### Microservice Bots: | ||
@@ -52,0 +58,0 @@ I've written a general whitepaper on the idea of microservice bots, which you can find on gist: [Microservice Bot Whitepaper](https://gist.github.com/DasWolke/c9d7dfe6a78445011162a12abd32091d) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
119430
2269
66
Updatedsnowtransfer@^0.4.3