aboutsummaryrefslogtreecommitdiff
path: root/node_modules/discord.js/src/sharding
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/discord.js/src/sharding')
-rw-r--r--node_modules/discord.js/src/sharding/Shard.js164
-rw-r--r--node_modules/discord.js/src/sharding/ShardClientUtil.js143
-rw-r--r--node_modules/discord.js/src/sharding/ShardingManager.js193
3 files changed, 500 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/sharding/Shard.js b/node_modules/discord.js/src/sharding/Shard.js
new file mode 100644
index 0000000..ab9b923
--- /dev/null
+++ b/node_modules/discord.js/src/sharding/Shard.js
@@ -0,0 +1,164 @@
+const childProcess = require('child_process');
+const path = require('path');
+const makeError = require('../util/MakeError');
+const makePlainError = require('../util/MakePlainError');
+
+/**
+ * Represents a Shard spawned by the ShardingManager.
+ */
+class Shard {
+ /**
+ * @param {ShardingManager} manager The sharding manager
+ * @param {number} id The ID of this shard
+ * @param {Array} [args=[]] Command line arguments to pass to the script
+ */
+ constructor(manager, id, args = []) {
+ /**
+ * Manager that created the shard
+ * @type {ShardingManager}
+ */
+ this.manager = manager;
+
+ /**
+ * ID of the shard
+ * @type {number}
+ */
+ this.id = id;
+
+ /**
+ * The environment variables for the shard
+ * @type {Object}
+ */
+ this.env = Object.assign({}, process.env, {
+ SHARD_ID: this.id,
+ SHARD_COUNT: this.manager.totalShards,
+ CLIENT_TOKEN: this.manager.token,
+ });
+
+ /**
+ * Process of the shard
+ * @type {ChildProcess}
+ */
+ this.process = childProcess.fork(path.resolve(this.manager.file), args, {
+ env: this.env,
+ });
+ this.process.on('message', this._handleMessage.bind(this));
+ this.process.once('exit', () => {
+ if (this.manager.respawn) this.manager.createShard(this.id);
+ });
+
+ this._evals = new Map();
+ this._fetches = new Map();
+ }
+
+ /**
+ * Sends a message to the shard's process.
+ * @param {*} message Message to send to the shard
+ * @returns {Promise<Shard>}
+ */
+ send(message) {
+ return new Promise((resolve, reject) => {
+ const sent = this.process.send(message, err => {
+ if (err) reject(err); else resolve(this);
+ });
+ if (!sent) throw new Error('Failed to send message to shard\'s process.');
+ });
+ }
+
+ /**
+ * Fetches a Client property value of the shard.
+ * @param {string} prop Name of the Client property to get, using periods for nesting
+ * @returns {Promise<*>}
+ * @example
+ * shard.fetchClientValue('guilds.size').then(count => {
+ * console.log(`${count} guilds in shard ${shard.id}`);
+ * }).catch(console.error);
+ */
+ fetchClientValue(prop) {
+ if (this._fetches.has(prop)) return this._fetches.get(prop);
+
+ const promise = new Promise((resolve, reject) => {
+ const listener = message => {
+ if (!message || message._fetchProp !== prop) return;
+ this.process.removeListener('message', listener);
+ this._fetches.delete(prop);
+ resolve(message._result);
+ };
+ this.process.on('message', listener);
+
+ this.send({ _fetchProp: prop }).catch(err => {
+ this.process.removeListener('message', listener);
+ this._fetches.delete(prop);
+ reject(err);
+ });
+ });
+
+ this._fetches.set(prop, promise);
+ return promise;
+ }
+
+ /**
+ * Evaluates a script on the shard, in the context of the Client.
+ * @param {string} script JavaScript to run on the shard
+ * @returns {Promise<*>} Result of the script execution
+ */
+ eval(script) {
+ if (this._evals.has(script)) return this._evals.get(script);
+
+ const promise = new Promise((resolve, reject) => {
+ const listener = message => {
+ if (!message || message._eval !== script) return;
+ this.process.removeListener('message', listener);
+ this._evals.delete(script);
+ if (!message._error) resolve(message._result); else reject(makeError(message._error));
+ };
+ this.process.on('message', listener);
+
+ this.send({ _eval: script }).catch(err => {
+ this.process.removeListener('message', listener);
+ this._evals.delete(script);
+ reject(err);
+ });
+ });
+
+ this._evals.set(script, promise);
+ return promise;
+ }
+
+ /**
+ * Handles an IPC message
+ * @param {*} message Message received
+ * @private
+ */
+ _handleMessage(message) {
+ if (message) {
+ // 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: 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: makePlainError(err) })
+ );
+ return;
+ }
+ }
+
+ /**
+ * Emitted upon recieving a message from a shard
+ * @event ShardingManager#message
+ * @param {Shard} shard Shard that sent the message
+ * @param {*} message Message that was received
+ */
+ this.manager.emit('message', this, message);
+ }
+}
+
+module.exports = Shard;
diff --git a/node_modules/discord.js/src/sharding/ShardClientUtil.js b/node_modules/discord.js/src/sharding/ShardClientUtil.js
new file mode 100644
index 0000000..6449941
--- /dev/null
+++ b/node_modules/discord.js/src/sharding/ShardClientUtil.js
@@ -0,0 +1,143 @@
+const makeError = require('../util/MakeError');
+const makePlainError = require('../util/MakePlainError');
+
+/**
+ * Helper class for sharded clients spawned as a child process, such as from a ShardingManager
+ */
+class ShardClientUtil {
+ /**
+ * @param {Client} client Client of the current shard
+ */
+ constructor(client) {
+ this.client = client;
+ process.on('message', this._handleMessage.bind(this));
+ }
+
+ /**
+ * ID of this shard
+ * @type {number}
+ * @readonly
+ */
+ get id() {
+ return this.client.options.shardId;
+ }
+
+ /**
+ * Total number of shards
+ * @type {number}
+ * @readonly
+ */
+ get count() {
+ return this.client.options.shardCount;
+ }
+
+ /**
+ * Sends a message to the master process
+ * @param {*} message Message to send
+ * @returns {Promise<void>}
+ */
+ send(message) {
+ return new Promise((resolve, reject) => {
+ const sent = process.send(message, err => {
+ if (err) reject(err); else resolve();
+ });
+ if (!sent) throw new Error('Failed to send message to master process.');
+ });
+ }
+
+ /**
+ * Fetches a Client property value of each shard.
+ * @param {string} prop Name of the Client property to get, using periods for nesting
+ * @returns {Promise<Array>}
+ * @example
+ * client.shard.fetchClientValues('guilds.size').then(results => {
+ * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
+ * }).catch(console.error);
+ */
+ fetchClientValues(prop) {
+ return new Promise((resolve, reject) => {
+ const listener = message => {
+ if (!message || message._sFetchProp !== prop) return;
+ process.removeListener('message', listener);
+ if (!message._error) resolve(message._result); else reject(makeError(message._error));
+ };
+ process.on('message', listener);
+
+ this.send({ _sFetchProp: prop }).catch(err => {
+ process.removeListener('message', listener);
+ reject(err);
+ });
+ });
+ }
+
+ /**
+ * Evaluates a script on all shards, in the context of the Clients.
+ * @param {string} script JavaScript to run on each shard
+ * @returns {Promise<Array>} Results of the script execution
+ */
+ broadcastEval(script) {
+ return new Promise((resolve, reject) => {
+ const listener = message => {
+ if (!message || message._sEval !== script) return;
+ process.removeListener('message', listener);
+ if (!message._error) resolve(message._result); else reject(makeError(message._error));
+ };
+ process.on('message', listener);
+
+ this.send({ _sEval: script }).catch(err => {
+ process.removeListener('message', listener);
+ reject(err);
+ });
+ });
+ }
+
+ /**
+ * Handles an IPC message
+ * @param {*} message Message received
+ * @private
+ */
+ _handleMessage(message) {
+ if (!message) return;
+ if (message._fetchProp) {
+ const props = message._fetchProp.split('.');
+ let value = this.client;
+ for (const prop of props) value = value[prop];
+ this._respond('fetchProp', { _fetchProp: message._fetchProp, _result: value });
+ } else if (message._eval) {
+ try {
+ this._respond('eval', { _eval: message._eval, _result: this.client._eval(message._eval) });
+ } catch (err) {
+ this._respond('eval', { _eval: message._eval, _error: makePlainError(err) });
+ }
+ }
+ }
+
+ /**
+ * Sends a message to the master process, emitting an error from the client upon failure
+ * @param {string} type Type of response to send
+ * @param {*} message Message to send
+ * @private
+ */
+ _respond(type, message) {
+ this.send(message).catch(err => {
+ err.message = `Error when sending ${type} response to master process: ${err.message}`;
+ this.client.emit('error', err);
+ });
+ }
+
+ /**
+ * Creates/gets the singleton of this class
+ * @param {Client} client Client to use
+ * @returns {ShardClientUtil}
+ */
+ static singleton(client) {
+ if (!this._singleton) {
+ this._singleton = new this(client);
+ } else {
+ client.emit('warn', 'Multiple clients created in child process; only the first will handle sharding helpers.');
+ }
+ return this._singleton;
+ }
+}
+
+module.exports = ShardClientUtil;
diff --git a/node_modules/discord.js/src/sharding/ShardingManager.js b/node_modules/discord.js/src/sharding/ShardingManager.js
new file mode 100644
index 0000000..671b5d7
--- /dev/null
+++ b/node_modules/discord.js/src/sharding/ShardingManager.js
@@ -0,0 +1,193 @@
+const path = require('path');
+const fs = require('fs');
+const EventEmitter = require('events').EventEmitter;
+const mergeDefault = require('../util/MergeDefault');
+const Shard = require('./Shard');
+const Collection = require('../util/Collection');
+const fetchRecommendedShards = require('../util/FetchRecommendedShards');
+
+/**
+ * This is a utility class that can be used to help you spawn shards of your Client. Each shard is completely separate
+ * from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
+ * If you do not select an amount of shards, the manager will automatically decide the best amount.
+ * @extends {EventEmitter}
+ */
+class ShardingManager extends EventEmitter {
+ /**
+ * @param {string} file Path to your shard script file
+ * @param {Object} [options] Options for the sharding manager
+ * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto"
+ * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting
+ * @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning
+ * @param {string} [options.token] Token to use for automatic shard count and passing to shards
+ */
+ constructor(file, options = {}) {
+ super();
+ options = mergeDefault({
+ totalShards: 'auto',
+ respawn: true,
+ shardArgs: [],
+ token: null,
+ }, options);
+
+ /**
+ * Path to the shard script file
+ * @type {string}
+ */
+ this.file = file;
+ if (!file) throw new Error('File must be specified.');
+ if (!path.isAbsolute(file)) this.file = path.resolve(process.cwd(), file);
+ const stats = fs.statSync(this.file);
+ if (!stats.isFile()) throw new Error('File path does not point to a file.');
+
+ /**
+ * Amount of shards that this manager is going to spawn
+ * @type {number|string}
+ */
+ this.totalShards = options.totalShards;
+ if (this.totalShards !== 'auto') {
+ if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
+ throw new TypeError('Amount of shards must be a number.');
+ }
+ if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
+ if (this.totalShards !== Math.floor(this.totalShards)) {
+ throw new RangeError('Amount of shards must be an integer.');
+ }
+ }
+
+ /**
+ * Whether shards should automatically respawn upon exiting
+ * @type {boolean}
+ */
+ this.respawn = options.respawn;
+
+ /**
+ * An array of arguments to pass to shards.
+ * @type {string[]}
+ */
+ this.shardArgs = options.shardArgs;
+
+ /**
+ * Token to use for obtaining the automatic shard count, and passing to shards
+ * @type {?string}
+ */
+ this.token = options.token ? options.token.replace(/^Bot\s*/i, '') : null;
+
+ /**
+ * A collection of shards that this manager has spawned
+ * @type {Collection<number, Shard>}
+ */
+ this.shards = new Collection();
+ }
+
+ /**
+ * Spawns a single shard.
+ * @param {number} id The ID of the shard to spawn. **This is usually not necessary.**
+ * @returns {Promise<Shard>}
+ */
+ createShard(id = this.shards.size) {
+ const shard = new Shard(this, id, this.shardArgs);
+ this.shards.set(id, shard);
+ /**
+ * Emitted upon launching a shard
+ * @event ShardingManager#launch
+ * @param {Shard} shard Shard that was launched
+ */
+ this.emit('launch', shard);
+ return Promise.resolve(shard);
+ }
+
+ /**
+ * Spawns multiple shards.
+ * @param {number} [amount=this.totalShards] Number of shards to spawn
+ * @param {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds)
+ * @returns {Promise<Collection<number, Shard>>}
+ */
+ spawn(amount = this.totalShards, delay = 5500) {
+ if (amount === 'auto') {
+ return fetchRecommendedShards(this.token).then(count => {
+ this.totalShards = count;
+ return this._spawn(count, delay);
+ });
+ } else {
+ if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
+ if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
+ if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
+ return this._spawn(amount, delay);
+ }
+ }
+
+ /**
+ * Actually spawns shards, unlike that poser above >:(
+ * @param {number} amount Number of shards to spawn
+ * @param {number} delay How long to wait in between spawning each shard (in milliseconds)
+ * @returns {Promise<Collection<number, Shard>>}
+ * @private
+ */
+ _spawn(amount, delay) {
+ return new Promise(resolve => {
+ if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
+ this.totalShards = amount;
+
+ this.createShard();
+ if (this.shards.size >= this.totalShards) {
+ resolve(this.shards);
+ return;
+ }
+
+ if (delay <= 0) {
+ while (this.shards.size < this.totalShards) this.createShard();
+ resolve(this.shards);
+ } else {
+ const interval = setInterval(() => {
+ this.createShard();
+ if (this.shards.size >= this.totalShards) {
+ clearInterval(interval);
+ resolve(this.shards);
+ }
+ }, delay);
+ }
+ });
+ }
+
+ /**
+ * Send a message to all shards.
+ * @param {*} message Message to be sent to the shards
+ * @returns {Promise<Shard[]>}
+ */
+ broadcast(message) {
+ const promises = [];
+ for (const shard of this.shards.values()) promises.push(shard.send(message));
+ return Promise.all(promises);
+ }
+
+ /**
+ * Evaluates a script on all shards, in the context of the Clients.
+ * @param {string} script JavaScript to run on each shard
+ * @returns {Promise<Array>} 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);
+ }
+
+ /**
+ * Fetches a Client property value of each shard.
+ * @param {string} prop Name of the Client property to get, using periods for nesting
+ * @returns {Promise<Array>}
+ * @example
+ * manager.fetchClientValues('guilds.size').then(results => {
+ * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
+ * }).catch(console.error);
+ */
+ fetchClientValues(prop) {
+ if (this.shards.size === 0) return Promise.reject(new Error('No shards have been spawned.'));
+ if (this.shards.size !== this.totalShards) return Promise.reject(new Error('Still spawning shards.'));
+ const promises = [];
+ for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
+ return Promise.all(promises);
+ }
+}
+
+module.exports = ShardingManager;