From 2a6c363a8a317a30cc849bdf6b9a3a0c19ea3adc Mon Sep 17 00:00:00 2001 From: "Matt (IPv4) Cowley" Date: Sun, 22 Nov 2020 12:35:18 +0000 Subject: [PATCH] feat(Shard): shard-specific broadcastEval/fetchClientValues + shard Id util (#4991) --- .eslintrc.json | 4 ++-- src/errors/Messages.js | 3 +++ src/sharding/Shard.js | 14 ++++++------ src/sharding/ShardClientUtil.js | 34 ++++++++++++++++++++--------- src/sharding/ShardingManager.js | 38 ++++++++++++++++++++++++--------- src/util/Snowflake.js | 9 ++++++++ typings/index.d.ts | 9 ++++++++ 7 files changed, 83 insertions(+), 28 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index fc7436fc7..d9f2f558a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,10 +2,10 @@ "extends": ["eslint:recommended", "plugin:prettier/recommended"], "plugins": ["import"], "parserOptions": { - "ecmaVersion": 2019 + "ecmaVersion": 2020 }, "env": { - "es6": true, + "es2020": true, "node": true }, "overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }], diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 9f0f6e44f..a68cd2da2 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -21,6 +21,7 @@ const Messages = { DISALLOWED_INTENTS: 'Privileged intent provided is not enabled or whitelisted.', SHARDING_NO_SHARDS: 'No shards have been spawned.', SHARDING_IN_PROCESS: 'Shards are still being spawned.', + SHARDING_SHARD_NOT_FOUND: id => `Shard ${id} could not be found.`, SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`, SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`, SHARDING_WORKER_EXISTS: id => `Shard ${id} already has an active worker.`, @@ -28,6 +29,8 @@ const Messages = { SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`, SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`, SHARDING_NO_CHILD_EXISTS: id => `Shard ${id} has no active process or worker.`, + SHARDING_SHARD_MISCALCULATION: (shard, guild, count) => + `Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`, COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', COLOR_CONVERT: 'Unable to convert color to a number.', diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index e840707ca..ecd535f67 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -334,18 +334,20 @@ class Shard extends EventEmitter { // Shard is requesting a property fetch if (message._sFetchProp) { - this.manager.fetchClientValues(message._sFetchProp).then( - results => this.send({ _sFetchProp: message._sFetchProp, _result: results }), - err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }), + const resp = { _sFetchProp: message._sFetchProp, _sFetchPropShard: message._sFetchPropShard }; + this.manager.fetchClientValues(message._sFetchProp, message._sFetchPropShard).then( + results => this.send({ ...resp, _result: results }), + err => this.send({ ...resp, _error: Util.makePlainError(err) }), ); return; } // Shard is requesting an eval broadcast if (message._sEval) { - this.manager.broadcastEval(message._sEval).then( - results => this.send({ _sEval: message._sEval, _result: results }), - err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }), + const resp = { _sEval: message._sEval, _sEvalShard: message._sEvalShard }; + this.manager.broadcastEval(message._sEval, message._sEvalShard).then( + results => this.send({ ...resp, _result: results }), + err => this.send({ ...resp, _error: Util.makePlainError(err) }), ); return; } diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 292a033d0..e62304210 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -96,28 +96,29 @@ class ShardClientUtil { } /** - * Fetches a client property value of each shard. + * Fetches a client property value of each shard, or a given shard. * @param {string} prop Name of the client property to get, using periods for nesting - * @returns {Promise>} + * @param {number} [shard] Shard to fetch property from, all if undefined + * @returns {Promise<*>|Promise>} * @example * client.shard.fetchClientValues('guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); * @see {@link ShardingManager#fetchClientValues} */ - fetchClientValues(prop) { + fetchClientValues(prop, shard) { return new Promise((resolve, reject) => { const parent = this.parentPort || process; const listener = message => { - if (!message || message._sFetchProp !== prop) return; + if (!message || message._sFetchProp !== prop || message._sFetchPropShard !== shard) return; parent.removeListener('message', listener); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; parent.on('message', listener); - this.send({ _sFetchProp: prop }).catch(err => { + this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => { parent.removeListener('message', listener); reject(err); }); @@ -125,29 +126,30 @@ class ShardClientUtil { } /** - * Evaluates a script or function on all shards, in the context of the {@link Client}s. + * Evaluates a script or function on all shards, or a given shard, in the context of the {@link Client}s. * @param {string|Function} script JavaScript to run on each shard - * @returns {Promise>} Results of the script execution + * @param {number} [shard] Shard to run script on, all if undefined + * @returns {Promise<*>|Promise>} Results of the script execution * @example * client.shard.broadcastEval('this.guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); * @see {@link ShardingManager#broadcastEval} */ - broadcastEval(script) { + broadcastEval(script, shard) { return new Promise((resolve, reject) => { const parent = this.parentPort || process; script = typeof script === 'function' ? `(${script})(this)` : script; const listener = message => { - if (!message || message._sEval !== script) return; + if (!message || message._sEval !== script || message._sEvalShard !== shard) return; parent.removeListener('message', listener); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; parent.on('message', listener); - this.send({ _sEval: script }).catch(err => { + this.send({ _sEval: script, _sEvalShard: shard }).catch(err => { parent.removeListener('message', listener); reject(err); }); @@ -224,6 +226,18 @@ class ShardClientUtil { } return this._singleton; } + + /** + * Get the shard ID for a given guild ID. + * @param {Snowflake} guildID Snowflake guild ID to get shard ID for + * @param {number} shardCount Number of shards + * @returns {number} + */ + static shardIDForGuildID(guildID, shardCount) { + const shard = Number(BigInt(guildID) >> 22n) % shardCount; + if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildID, shardCount); + return shard; + } } module.exports = ShardClientUtil; diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 05251d69c..9007c5936 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -222,30 +222,48 @@ class ShardingManager extends EventEmitter { } /** - * Evaluates a script on all shards, in the context of the {@link Client}s. + * Evaluates a script on all shards, or a given shard, in the context of the {@link Client}s. * @param {string} script JavaScript to run on each shard - * @returns {Promise>} Results of the script execution + * @param {number} [shard] Shard to run on, all if undefined + * @returns {Promise<*>|Promise>} Results of the script execution */ - broadcastEval(script) { - const promises = []; - for (const shard of this.shards.values()) promises.push(shard.eval(script)); - return Promise.all(promises); + broadcastEval(script, shard) { + return this._performOnShards('eval', [script], shard); } /** - * Fetches a client property value of each shard. + * Fetches a client property value of each shard, or a given shard. * @param {string} prop Name of the client property to get, using periods for nesting - * @returns {Promise>} + * @param {number} [shard] Shard to fetch property from, all if undefined + * @returns {Promise<*>|Promise>} * @example * manager.fetchClientValues('guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); */ - fetchClientValues(prop) { + fetchClientValues(prop, shard) { + return this._performOnShards('fetchClientValue', [prop], shard); + } + + /** + * Runs a method with given arguments on all shards, or a given shard. + * @param {string} method Method name to run on each shard + * @param {Array<*>} args Arguments to pass through to the method call + * @param {number} [shard] Shard to run on, all if undefined + * @returns {Promise<*>|Promise>} Results of the method execution + * @private + */ + _performOnShards(method, args, shard) { if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS')); if (this.shards.size !== this.shardList.length) return Promise.reject(new Error('SHARDING_IN_PROCESS')); + + if (typeof shard === 'number') { + if (this.shards.has(shard)) return this.shards.get(shard)[method](...args); + return Promise.reject(new Error('SHARDING_SHARD_NOT_FOUND', shard)); + } + const promises = []; - for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop)); + for (const sh of this.shards.values()) promises.push(sh[method](...args)); return Promise.all(promises); } diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js index 5d267ae1b..bf1309c7c 100644 --- a/src/util/Snowflake.js +++ b/src/util/Snowflake.js @@ -79,6 +79,15 @@ class SnowflakeUtil { }); return res; } + + /** + * Discord's epoch value (2015-01-01T00:00:00.000Z). + * @type {number} + * @readonly + */ + static get EPOCH() { + return EPOCH; + } } module.exports = SnowflakeUtil; diff --git a/typings/index.d.ts b/typings/index.d.ts index d0b730338..e0be3c612 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1352,12 +1352,16 @@ declare module 'discord.js' { public mode: ShardingManagerMode; public parentPort: any | null; public broadcastEval(script: string): Promise; + public broadcastEval(script: string, shard: number): Promise; public broadcastEval(fn: (client: Client) => T): Promise; + public broadcastEval(fn: (client: Client) => T, shard: number): Promise; public fetchClientValues(prop: string): Promise; + public fetchClientValues(prop: string, shard: number): Promise; public respawnAll(shardDelay?: number, respawnDelay?: number, spawnTimeout?: number): Promise; public send(message: any): Promise; public static singleton(client: Client, mode: ShardingManagerMode): ShardClientUtil; + public static shardIDForGuildID(guildID: Snowflake, shardCount: number): number; } export class ShardingManager extends EventEmitter { @@ -1373,6 +1377,8 @@ declare module 'discord.js' { execArgv?: string[]; }, ); + private _performOnShards(method: string, args: any[]): Promise; + private _performOnShards(method: string, args: any[], shard: number): Promise; public file: string; public respawn: boolean; @@ -1382,8 +1388,10 @@ declare module 'discord.js' { public totalShards: number | 'auto'; public broadcast(message: any): Promise; public broadcastEval(script: string): Promise; + public broadcastEval(script: string, shard: number): Promise; public createShard(id: number): Shard; public fetchClientValues(prop: string): Promise; + public fetchClientValues(prop: string, shard: number): Promise; public respawnAll( shardDelay?: number, respawnDelay?: number, @@ -1399,6 +1407,7 @@ declare module 'discord.js' { export class SnowflakeUtil { public static deconstruct(snowflake: Snowflake): DeconstructedSnowflake; public static generate(timestamp?: number | Date): Snowflake; + public static readonly EPOCH: number; } export class Speaking extends BitField {