Skip to content

Commit 2a6c363

Browse files
authored
feat(Shard): shard-specific broadcastEval/fetchClientValues + shard Id util (#4991)
1 parent 643f96c commit 2a6c363

File tree

7 files changed

+83
-28
lines changed

7 files changed

+83
-28
lines changed

.eslintrc.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
33
"plugins": ["import"],
44
"parserOptions": {
5-
"ecmaVersion": 2019
5+
"ecmaVersion": 2020
66
},
77
"env": {
8-
"es6": true,
8+
"es2020": true,
99
"node": true
1010
},
1111
"overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }],

src/errors/Messages.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ const Messages = {
2121
DISALLOWED_INTENTS: 'Privileged intent provided is not enabled or whitelisted.',
2222
SHARDING_NO_SHARDS: 'No shards have been spawned.',
2323
SHARDING_IN_PROCESS: 'Shards are still being spawned.',
24+
SHARDING_SHARD_NOT_FOUND: id => `Shard ${id} could not be found.`,
2425
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`,
2526
SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`,
2627
SHARDING_WORKER_EXISTS: id => `Shard ${id} already has an active worker.`,
2728
SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`,
2829
SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`,
2930
SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`,
3031
SHARDING_NO_CHILD_EXISTS: id => `Shard ${id} has no active process or worker.`,
32+
SHARDING_SHARD_MISCALCULATION: (shard, guild, count) =>
33+
`Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`,
3134

3235
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
3336
COLOR_CONVERT: 'Unable to convert color to a number.',

src/sharding/Shard.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,18 +334,20 @@ class Shard extends EventEmitter {
334334

335335
// Shard is requesting a property fetch
336336
if (message._sFetchProp) {
337-
this.manager.fetchClientValues(message._sFetchProp).then(
338-
results => this.send({ _sFetchProp: message._sFetchProp, _result: results }),
339-
err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }),
337+
const resp = { _sFetchProp: message._sFetchProp, _sFetchPropShard: message._sFetchPropShard };
338+
this.manager.fetchClientValues(message._sFetchProp, message._sFetchPropShard).then(
339+
results => this.send({ ...resp, _result: results }),
340+
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
340341
);
341342
return;
342343
}
343344

344345
// Shard is requesting an eval broadcast
345346
if (message._sEval) {
346-
this.manager.broadcastEval(message._sEval).then(
347-
results => this.send({ _sEval: message._sEval, _result: results }),
348-
err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }),
347+
const resp = { _sEval: message._sEval, _sEvalShard: message._sEvalShard };
348+
this.manager.broadcastEval(message._sEval, message._sEvalShard).then(
349+
results => this.send({ ...resp, _result: results }),
350+
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
349351
);
350352
return;
351353
}

src/sharding/ShardClientUtil.js

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,58 +96,60 @@ class ShardClientUtil {
9696
}
9797

9898
/**
99-
* Fetches a client property value of each shard.
99+
* Fetches a client property value of each shard, or a given shard.
100100
* @param {string} prop Name of the client property to get, using periods for nesting
101-
* @returns {Promise<Array<*>>}
101+
* @param {number} [shard] Shard to fetch property from, all if undefined
102+
* @returns {Promise<*>|Promise<Array<*>>}
102103
* @example
103104
* client.shard.fetchClientValues('guilds.cache.size')
104105
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
105106
* .catch(console.error);
106107
* @see {@link ShardingManager#fetchClientValues}
107108
*/
108-
fetchClientValues(prop) {
109+
fetchClientValues(prop, shard) {
109110
return new Promise((resolve, reject) => {
110111
const parent = this.parentPort || process;
111112

112113
const listener = message => {
113-
if (!message || message._sFetchProp !== prop) return;
114+
if (!message || message._sFetchProp !== prop || message._sFetchPropShard !== shard) return;
114115
parent.removeListener('message', listener);
115116
if (!message._error) resolve(message._result);
116117
else reject(Util.makeError(message._error));
117118
};
118119
parent.on('message', listener);
119120

120-
this.send({ _sFetchProp: prop }).catch(err => {
121+
this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => {
121122
parent.removeListener('message', listener);
122123
reject(err);
123124
});
124125
});
125126
}
126127

127128
/**
128-
* Evaluates a script or function on all shards, in the context of the {@link Client}s.
129+
* Evaluates a script or function on all shards, or a given shard, in the context of the {@link Client}s.
129130
* @param {string|Function} script JavaScript to run on each shard
130-
* @returns {Promise<Array<*>>} Results of the script execution
131+
* @param {number} [shard] Shard to run script on, all if undefined
132+
* @returns {Promise<*>|Promise<Array<*>>} Results of the script execution
131133
* @example
132134
* client.shard.broadcastEval('this.guilds.cache.size')
133135
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
134136
* .catch(console.error);
135137
* @see {@link ShardingManager#broadcastEval}
136138
*/
137-
broadcastEval(script) {
139+
broadcastEval(script, shard) {
138140
return new Promise((resolve, reject) => {
139141
const parent = this.parentPort || process;
140142
script = typeof script === 'function' ? `(${script})(this)` : script;
141143

142144
const listener = message => {
143-
if (!message || message._sEval !== script) return;
145+
if (!message || message._sEval !== script || message._sEvalShard !== shard) return;
144146
parent.removeListener('message', listener);
145147
if (!message._error) resolve(message._result);
146148
else reject(Util.makeError(message._error));
147149
};
148150
parent.on('message', listener);
149151

150-
this.send({ _sEval: script }).catch(err => {
152+
this.send({ _sEval: script, _sEvalShard: shard }).catch(err => {
151153
parent.removeListener('message', listener);
152154
reject(err);
153155
});
@@ -224,6 +226,18 @@ class ShardClientUtil {
224226
}
225227
return this._singleton;
226228
}
229+
230+
/**
231+
* Get the shard ID for a given guild ID.
232+
* @param {Snowflake} guildID Snowflake guild ID to get shard ID for
233+
* @param {number} shardCount Number of shards
234+
* @returns {number}
235+
*/
236+
static shardIDForGuildID(guildID, shardCount) {
237+
const shard = Number(BigInt(guildID) >> 22n) % shardCount;
238+
if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildID, shardCount);
239+
return shard;
240+
}
227241
}
228242

229243
module.exports = ShardClientUtil;

src/sharding/ShardingManager.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,30 +222,48 @@ class ShardingManager extends EventEmitter {
222222
}
223223

224224
/**
225-
* Evaluates a script on all shards, in the context of the {@link Client}s.
225+
* Evaluates a script on all shards, or a given shard, in the context of the {@link Client}s.
226226
* @param {string} script JavaScript to run on each shard
227-
* @returns {Promise<Array<*>>} Results of the script execution
227+
* @param {number} [shard] Shard to run on, all if undefined
228+
* @returns {Promise<*>|Promise<Array<*>>} Results of the script execution
228229
*/
229-
broadcastEval(script) {
230-
const promises = [];
231-
for (const shard of this.shards.values()) promises.push(shard.eval(script));
232-
return Promise.all(promises);
230+
broadcastEval(script, shard) {
231+
return this._performOnShards('eval', [script], shard);
233232
}
234233

235234
/**
236-
* Fetches a client property value of each shard.
235+
* Fetches a client property value of each shard, or a given shard.
237236
* @param {string} prop Name of the client property to get, using periods for nesting
238-
* @returns {Promise<Array<*>>}
237+
* @param {number} [shard] Shard to fetch property from, all if undefined
238+
* @returns {Promise<*>|Promise<Array<*>>}
239239
* @example
240240
* manager.fetchClientValues('guilds.cache.size')
241241
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
242242
* .catch(console.error);
243243
*/
244-
fetchClientValues(prop) {
244+
fetchClientValues(prop, shard) {
245+
return this._performOnShards('fetchClientValue', [prop], shard);
246+
}
247+
248+
/**
249+
* Runs a method with given arguments on all shards, or a given shard.
250+
* @param {string} method Method name to run on each shard
251+
* @param {Array<*>} args Arguments to pass through to the method call
252+
* @param {number} [shard] Shard to run on, all if undefined
253+
* @returns {Promise<*>|Promise<Array<*>>} Results of the method execution
254+
* @private
255+
*/
256+
_performOnShards(method, args, shard) {
245257
if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS'));
246258
if (this.shards.size !== this.shardList.length) return Promise.reject(new Error('SHARDING_IN_PROCESS'));
259+
260+
if (typeof shard === 'number') {
261+
if (this.shards.has(shard)) return this.shards.get(shard)[method](...args);
262+
return Promise.reject(new Error('SHARDING_SHARD_NOT_FOUND', shard));
263+
}
264+
247265
const promises = [];
248-
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
266+
for (const sh of this.shards.values()) promises.push(sh[method](...args));
249267
return Promise.all(promises);
250268
}
251269

src/util/Snowflake.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ class SnowflakeUtil {
7979
});
8080
return res;
8181
}
82+
83+
/**
84+
* Discord's epoch value (2015-01-01T00:00:00.000Z).
85+
* @type {number}
86+
* @readonly
87+
*/
88+
static get EPOCH() {
89+
return EPOCH;
90+
}
8291
}
8392

8493
module.exports = SnowflakeUtil;

typings/index.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,12 +1352,16 @@ declare module 'discord.js' {
13521352
public mode: ShardingManagerMode;
13531353
public parentPort: any | null;
13541354
public broadcastEval(script: string): Promise<any[]>;
1355+
public broadcastEval(script: string, shard: number): Promise<any>;
13551356
public broadcastEval<T>(fn: (client: Client) => T): Promise<T[]>;
1357+
public broadcastEval<T>(fn: (client: Client) => T, shard: number): Promise<T>;
13561358
public fetchClientValues(prop: string): Promise<any[]>;
1359+
public fetchClientValues(prop: string, shard: number): Promise<any>;
13571360
public respawnAll(shardDelay?: number, respawnDelay?: number, spawnTimeout?: number): Promise<void>;
13581361
public send(message: any): Promise<void>;
13591362

13601363
public static singleton(client: Client, mode: ShardingManagerMode): ShardClientUtil;
1364+
public static shardIDForGuildID(guildID: Snowflake, shardCount: number): number;
13611365
}
13621366

13631367
export class ShardingManager extends EventEmitter {
@@ -1373,6 +1377,8 @@ declare module 'discord.js' {
13731377
execArgv?: string[];
13741378
},
13751379
);
1380+
private _performOnShards(method: string, args: any[]): Promise<any[]>;
1381+
private _performOnShards(method: string, args: any[], shard: number): Promise<any>;
13761382

13771383
public file: string;
13781384
public respawn: boolean;
@@ -1382,8 +1388,10 @@ declare module 'discord.js' {
13821388
public totalShards: number | 'auto';
13831389
public broadcast(message: any): Promise<Shard[]>;
13841390
public broadcastEval(script: string): Promise<any[]>;
1391+
public broadcastEval(script: string, shard: number): Promise<any>;
13851392
public createShard(id: number): Shard;
13861393
public fetchClientValues(prop: string): Promise<any[]>;
1394+
public fetchClientValues(prop: string, shard: number): Promise<any>;
13871395
public respawnAll(
13881396
shardDelay?: number,
13891397
respawnDelay?: number,
@@ -1399,6 +1407,7 @@ declare module 'discord.js' {
13991407
export class SnowflakeUtil {
14001408
public static deconstruct(snowflake: Snowflake): DeconstructedSnowflake;
14011409
public static generate(timestamp?: number | Date): Snowflake;
1410+
public static readonly EPOCH: number;
14021411
}
14031412

14041413
export class Speaking extends BitField<SpeakingString> {

0 commit comments

Comments
 (0)