diff --git a/package.json b/package.json
index 4e2400baa..f99a4ebdd 100644
--- a/package.json
+++ b/package.json
@@ -32,12 +32,11 @@
"homepage": "https://github.com/hydrabolt/discord.js#readme",
"runkitExampleFilename": "./docs/examples/ping.js",
"dependencies": {
- "long": "^3.0.0",
"pako": "^1.0.0",
"prism-media": "github:hydrabolt/prism-media#indev",
- "snekfetch": "^3.0.0",
+ "snekfetch": "^3.5.0",
"tweetnacl": "^1.0.0",
- "ws": "^3.0.0"
+ "ws": "^3.3.1"
},
"peerDependencies": {
"bufferutil": "^3.0.0",
@@ -50,12 +49,12 @@
"devDependencies": {
"@types/node": "^8.0.0",
"discord.js-docgen": "hydrabolt/discord.js-docgen",
- "eslint": "^4.0.0",
+ "eslint": "^4.11.0",
"jsdoc-strip-async-await": "^0.1.0",
"json-filter-loader": "^1.0.0",
- "parallel-webpack": "^2.0.0",
+ "parallel-webpack": "^2.2.0",
"uglifyjs-webpack-plugin": "^1.0.0-beta.2",
- "webpack": "^3.0.0"
+ "webpack": "^3.8.0"
},
"engines": {
"node": ">=8.0.0"
diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js
index 365e9dcb7..f2c91ccdf 100644
--- a/src/client/BaseClient.js
+++ b/src/client/BaseClient.js
@@ -42,6 +42,7 @@ class BaseClient extends EventEmitter {
/**
* API shortcut
* @type {Object}
+ * @readonly
* @private
*/
get api() {
diff --git a/src/client/Client.js b/src/client/Client.js
index b26b73e2e..4b335e88a 100644
--- a/src/client/Client.js
+++ b/src/client/Client.js
@@ -163,6 +163,7 @@ class Client extends BaseClient {
/**
* Timestamp of the latest ping's start time
* @type {number}
+ * @readonly
* @private
*/
get _pingTimestamp() {
diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js
index bc413cef6..c4c297879 100644
--- a/src/client/WebhookClient.js
+++ b/src/client/WebhookClient.js
@@ -3,7 +3,7 @@ const BaseClient = require('./BaseClient');
/**
* The webhook client.
- * @extends {Webhook}
+ * @implements {Webhook}
* @extends {BaseClient}
*/
class WebhookClient extends BaseClient {
diff --git a/src/client/voice/util/Secretbox.js b/src/client/voice/util/Secretbox.js
index 31f5b8d00..b21fb8f9d 100644
--- a/src/client/voice/util/Secretbox.js
+++ b/src/client/voice/util/Secretbox.js
@@ -15,7 +15,7 @@ const libs = {
exports.methods = {};
-(async() => {
+(async () => {
for (const libName of Object.keys(libs)) {
try {
const lib = require(libName);
diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js
index 4fc5363cf..b1a833d5f 100644
--- a/src/client/websocket/packets/handlers/Ready.js
+++ b/src/client/websocket/packets/handlers/Ready.js
@@ -1,6 +1,6 @@
const AbstractHandler = require('./AbstractHandler');
const { Events } = require('../../../../util/Constants');
-const ClientUser = require('../../../../structures/ClientUser');
+let ClientUser;
class ReadyHandler extends AbstractHandler {
handle(packet) {
@@ -12,6 +12,7 @@ class ReadyHandler extends AbstractHandler {
data.user.user_settings = data.user_settings;
data.user.user_guild_settings = data.user_guild_settings;
+ if (!ClientUser) ClientUser = require('../../../../structures/ClientUser');
const clientUser = new ClientUser(client, data.user);
client.user = clientUser;
client.readyAt = new Date();
diff --git a/src/errors/Messages.js b/src/errors/Messages.js
index c3116a203..545452703 100644
--- a/src/errors/Messages.js
+++ b/src/errors/Messages.js
@@ -20,9 +20,13 @@ const Messages = {
SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.',
SHARDING_CHILD_CONNECTION: 'Failed to send message to shard\'s process.',
SHARDING_PARENT_CONNECTION: 'Failed to send message to master process.',
- SHARDING_NO_SHARDS: 'No shards have been spawned',
- SHARDING_IN_PROCESS: 'Shards are still being spawned',
- SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards`,
+ SHARDING_NO_SHARDS: 'No shards have been spawned.',
+ SHARDING_IN_PROCESS: 'Shards are still being spawned.',
+ SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`,
+ SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`,
+ SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`,
+ 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.`,
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
COLOR_CONVERT: 'Unable to convert color to a number.',
diff --git a/src/index.js b/src/index.js
index 7bea36f1b..42de34564 100644
--- a/src/index.js
+++ b/src/index.js
@@ -18,6 +18,7 @@ module.exports = {
Permissions: require('./util/Permissions'),
Snowflake: require('./util/Snowflake'),
SnowflakeUtil: require('./util/Snowflake'),
+ Structures: require('./util/Structures'),
Util: Util,
util: Util,
version: require('../package.json').version,
@@ -29,14 +30,18 @@ module.exports = {
GuildChannelStore: require('./stores/GuildChannelStore'),
GuildMemberStore: require('./stores/GuildMemberStore'),
GuildStore: require('./stores/GuildStore'),
+ ReactionUserStore: require('./stores/ReactionUserStore'),
MessageStore: require('./stores/MessageStore'),
PresenceStore: require('./stores/PresenceStore'),
RoleStore: require('./stores/RoleStore'),
UserStore: require('./stores/UserStore'),
// Shortcuts to Util methods
+ discordSort: Util.discordSort,
escapeMarkdown: Util.escapeMarkdown,
fetchRecommendedShards: Util.fetchRecommendedShards,
+ resolveColor: Util.resolveColor,
+ resolveString: Util.resolveString,
splitMessage: Util.splitMessage,
// Structures
@@ -45,7 +50,10 @@ module.exports = {
CategoryChannel: require('./structures/CategoryChannel'),
Channel: require('./structures/Channel'),
ClientApplication: require('./structures/ClientApplication'),
- ClientUser: require('./structures/ClientUser'),
+ get ClientUser() {
+ // This is a getter so that it properly extends any custom User class
+ return require('./structures/ClientUser');
+ },
ClientUserChannelOverride: require('./structures/ClientUserChannelOverride'),
ClientUserGuildSettings: require('./structures/ClientUserGuildSettings'),
ClientUserSettings: require('./structures/ClientUserSettings'),
diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js
index 64e9ea72b..c4226a45c 100644
--- a/src/rest/handlers/RequestHandler.js
+++ b/src/rest/handlers/RequestHandler.js
@@ -39,12 +39,13 @@ class RequestHandler {
/**
* Emitted when the client hits a rate limit while making a request
* @event Client#rateLimit
- * @prop {number} timeout Timeout in ms
- * @prop {number} limit Number of requests that can be made to this endpoint
- * @prop {number} timeDifference Delta-T in ms between your system and Discord servers
- * @prop {string} method HTTP method used for request that triggered this event
- * @prop {string} path Path used for request that triggered this event
- * @prop {string} route Route used for request that triggered this event
+ * @param {Object} rateLimitInfo Object containing the rate limit info
+ * @param {number} rateLimitInfo.timeout Timeout in ms
+ * @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint
+ * @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers
+ * @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
+ * @param {string} rateLimitInfo.path Path used for request that triggered this event
+ * @param {string} rateLimitInfo.route Route used for request that triggered this event
*/
this.client.emit(RATE_LIMIT, {
timeout,
diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js
index e67bece78..0e44f91a2 100644
--- a/src/sharding/Shard.js
+++ b/src/sharding/Shard.js
@@ -1,18 +1,24 @@
const childProcess = require('child_process');
+const EventEmitter = require('events');
const path = require('path');
const Util = require('../util/Util');
const { Error } = require('../errors');
/**
- * Represents a Shard spawned by the ShardingManager.
+ * A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains
+ * an instance of the bot and its {@link Client}. When its child process exits for any reason, the shard will spawn a
+ * new one to replace it as necessary.
+ * @extends EventEmitter
*/
-class Shard {
+class Shard extends EventEmitter {
/**
- * @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
+ * @param {ShardingManager} manager Manager that is spawning this shard
+ * @param {number} id ID of this shard
+ * @param {string[]} [args=[]] Command line arguments to pass to the script
*/
constructor(manager, id, args = []) {
+ super();
+
/**
* Manager that created the shard
* @type {ShardingManager}
@@ -26,7 +32,13 @@ class Shard {
this.id = id;
/**
- * The environment variables for the shard
+ * Arguments for the shard's process
+ * @type {string[]}
+ */
+ this.args = args;
+
+ /**
+ * Environment variables for the shard's process
* @type {Object}
*/
this.env = Object.assign({}, process.env, {
@@ -36,19 +48,81 @@ class Shard {
});
/**
- * Process of the shard
- * @type {ChildProcess}
+ * Whether the shard's {@link Client} is ready
+ * @type {boolean}
*/
- 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.ready = false;
+ /**
+ * Process of the shard
+ * @type {?ChildProcess}
+ */
+ this.process = null;
+
+ /**
+ * Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with
+ * @type {Map}
+ * @private
+ */
this._evals = new Map();
+
+ /**
+ * Ongoing promises for calls to {@link Shard#fetchClientValue}, mapped by the `prop` they were called with
+ * @type {Map}
+ * @private
+ */
this._fetches = new Map();
+
+ /**
+ * Listener function for the {@link ChildProcess}' `exit` event
+ * @type {Function}
+ * @private
+ */
+ this._exitListener = this._handleExit.bind(this, undefined);
+ }
+
+ /**
+ * Forks a child process for the shard.
+ * You should not need to call this manually.
+ * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving
+ * @returns {Promise}
+ */
+ async spawn(waitForReady = true) {
+ if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id);
+
+ this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { env: this.env })
+ .on('message', this._handleMessage.bind(this))
+ .on('exit', this._exitListener);
+
+ /**
+ * Emitted upon the creation of the shard's child process.
+ * @event Shard#spawn
+ * @param {ChildProcess} process Child process that was created
+ */
+ this.emit('spawn', this.process);
+
+ if (!waitForReady) return this.process;
+ await new Promise((resolve, reject) => {
+ this.once('ready', resolve);
+ this.once('disconnect', () => reject(new Error('SHARDING_READY_DISCONNECTED', this.id)));
+ this.once('death', () => reject(new Error('SHARDING_READY_DIED', this.id)));
+ setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), 30000);
+ });
+ return this.process;
+ }
+
+ /**
+ * Kills and restarts the shard's process.
+ * @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds)
+ * @param {boolean} [waitForReady=true] Whether to wait the {@link Client} has become ready before resolving
+ * @returns {Promise}
+ */
+ async respawn(delay = 500, waitForReady = true) {
+ this.process.removeListener('exit', this._exitListener);
+ this.process.kill();
+ this._handleExit(false);
+ if (delay > 0) await Util.delayFor(delay);
+ return this.spawn(waitForReady);
}
/**
@@ -100,7 +174,7 @@ class Shard {
}
/**
- * Evaluates a script on the shard, in the context of the client.
+ * Evaluates a script on the shard, in the context of the {@link Client}.
* @param {string} script JavaScript to run on the shard
* @returns {Promise<*>} Result of the script execution
*/
@@ -134,6 +208,39 @@ class Shard {
*/
_handleMessage(message) {
if (message) {
+ // Shard is ready
+ if (message._ready) {
+ this.ready = true;
+ /**
+ * Emitted upon the shard's {@link Client#ready} event.
+ * @event Shard#ready
+ */
+ this.emit('ready');
+ return;
+ }
+
+ // Shard has disconnected
+ if (message._disconnect) {
+ this.ready = false;
+ /**
+ * Emitted upon the shard's {@link Client#disconnect} event.
+ * @event Shard#disconnect
+ */
+ this.emit('disconnect');
+ return;
+ }
+
+ // Shard is attempting to reconnect
+ if (message._reconnecting) {
+ this.ready = false;
+ /**
+ * Emitted upon the shard's {@link Client#reconnecting} event.
+ * @event Shard#reconnecting
+ */
+ this.emit('reconnecting');
+ return;
+ }
+
// Shard is requesting a property fetch
if (message._sFetchProp) {
this.manager.fetchClientValues(message._sFetchProp).then(
@@ -151,15 +258,44 @@ class Shard {
);
return;
}
+
+ // Shard is requesting a respawn of all shards
+ if (message._sRespawnAll) {
+ const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll;
+ this.manager.respawnAll(shardDelay, respawnDelay, waitForReady).catch(() => {
+ // Do nothing
+ });
+ return;
+ }
}
/**
- * Emitted upon recieving a message from a shard.
- * @event ShardingManager#message
- * @param {Shard} shard Shard that sent the message
+ * Emitted upon recieving a message from the child process.
+ * @event Shard#message
* @param {*} message Message that was received
*/
- this.manager.emit('message', this, message);
+ this.emit('message', message);
+ }
+
+ /**
+ * Handles the shard's process exiting.
+ * @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again
+ * @private
+ */
+ _handleExit(respawn = this.manager.respawn) {
+ /**
+ * Emitted upon the shard's child process exiting.
+ * @event Shard#death
+ * @param {ChildProcess} process Child process that exited
+ */
+ this.emit('death', this.process);
+
+ this.ready = false;
+ this.process = null;
+ this._evals.clear();
+ this._fetches.clear();
+
+ if (respawn) this.spawn().catch(err => this.emit('error', err));
}
}
diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js
index aceb85f2d..b0e9d57ed 100644
--- a/src/sharding/ShardClientUtil.js
+++ b/src/sharding/ShardClientUtil.js
@@ -3,15 +3,19 @@ const { Events } = require('../util/Constants');
const { Error } = require('../errors');
/**
- * Helper class for sharded clients spawned as a child process, such as from a ShardingManager.
+ * Helper class for sharded clients spawned as a child process, such as from a {@link ShardingManager}.
+ * Utilises IPC to send and receive data to/from the master process and other shards.
*/
class ShardClientUtil {
/**
- * @param {Client} client The client of the current shard
+ * @param {Client} client Client of the current shard
*/
constructor(client) {
this.client = client;
process.on('message', this._handleMessage.bind(this));
+ client.on('ready', () => { process.send({ _ready: true }); });
+ client.on('disconnect', () => { process.send({ _disconnect: true }); });
+ client.on('reconnecting', () => { process.send({ _reconnecting: true }); });
}
/**
@@ -49,13 +53,14 @@ class ShardClientUtil {
/**
* Fetches a client property value of each shard.
* @param {string} prop Name of the client property to get, using periods for nesting
- * @returns {Promise}
+ * @returns {Promise>}
* @example
* client.shard.fetchClientValues('guilds.size')
* .then(results => {
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
* })
* .catch(console.error);
+ * @see {@link ShardingManager#fetchClientValues}
*/
fetchClientValues(prop) {
return new Promise((resolve, reject) => {
@@ -74,9 +79,10 @@ class ShardClientUtil {
}
/**
- * Evaluates a script on all shards, in the context of the Clients.
+ * Evaluates a script on all shards, in the context of the {@link Clients}.
* @param {string} script JavaScript to run on each shard
- * @returns {Promise} Results of the script execution
+ * @returns {Promise>} Results of the script execution
+ * @see {@link ShardingManager#broadcastEval}
*/
broadcastEval(script) {
return new Promise((resolve, reject) => {
@@ -94,6 +100,19 @@ class ShardClientUtil {
});
}
+ /**
+ * Requests a respawn of all shards.
+ * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
+ * @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
+ * (in milliseconds)
+ * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
+ * @returns {Promise} Resolves upon the message being sent
+ * @see {@link ShardingManager#respawnAll}
+ */
+ respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) {
+ return this.send({ _sRespawnAll: { shardDelay, respawnDelay, waitForReady } });
+ }
+
/**
* Handles an IPC message.
* @param {*} message Message received
diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js
index 7a18898be..9596ead2e 100644
--- a/src/sharding/ShardingManager.js
+++ b/src/sharding/ShardingManager.js
@@ -7,9 +7,12 @@ const Util = require('../util/Util');
const { Error, TypeError, RangeError } = require('../errors');
/**
- * 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.
+ * This is a utility class that makes multi-process sharding of a bot an easy and painless experience.
+ * It works by spawning a self-contained {@link ChildProcess} for each individual shard, each containing its own
+ * instance of your bot's {@link Client}. They all have a line of communication with the master process, and there are
+ * several useful methods that utilise it in order to simplify tasks that are normally difficult with sharding. It can
+ * spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a path to your main bot
+ * script to launch for each one.
* @extends {EventEmitter}
*/
class ShardingManager extends EventEmitter {
@@ -82,33 +85,33 @@ class ShardingManager extends EventEmitter {
/**
* Spawns a single shard.
- * @param {number} id The ID of the shard to spawn. **This is usually not necessary**
- * @returns {Promise}
+ * @param {number} [id=this.shards.size] ID of the shard to spawn -
+ * **This is usually not necessary to manually specify.**
+ * @returns {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
+ * Emitted upon creating a shard.
+ * @event ShardingManager#shardCreate
+ * @param {Shard} shard Shard that was created
*/
- this.emit('launch', shard);
- return Promise.resolve(shard);
+ this.emit('shardCreate', shard);
+ return shard;
}
/**
* Spawns multiple shards.
* @param {number} [amount=this.totalShards] Number of shards to spawn
- * @param {number} [delay=7500] How long to wait in between spawning each shard (in milliseconds)
+ * @param {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds)
+ * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
* @returns {Promise>}
*/
- spawn(amount = this.totalShards, delay = 7500) {
+ async spawn(amount = this.totalShards, delay = 5500, waitForReady = true) {
+ // Obtain/verify the number of shards to spawn
if (amount === 'auto') {
- return Util.fetchRecommendedShards(this.token).then(count => {
- this.totalShards = count;
- return this._spawn(count, delay);
- });
+ amount = await Util.fetchRecommendedShards(this.token);
} else {
if (typeof amount !== 'number' || isNaN(amount)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.');
@@ -117,41 +120,22 @@ class ShardingManager extends EventEmitter {
if (amount !== Math.floor(amount)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', '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>}
- * @private
- */
- _spawn(amount, delay) {
- return new Promise(resolve => {
- if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
- this.totalShards = amount;
+ // Make sure this many shards haven't already been spawned
+ if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
+ this.totalShards = amount;
- this.createShard();
- if (this.shards.size >= this.totalShards) {
- resolve(this.shards);
- return;
- }
+ // Spawn the shards
+ for (let s = 1; s <= amount; s++) {
+ const promises = [];
+ const shard = this.createShard();
+ promises.push(shard.spawn(waitForReady));
+ if (delay > 0 && s !== amount) promises.push(Util.delayFor(delay));
+ await Promise.all(promises); // eslint-disable-line no-await-in-loop
+ }
- 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);
- }
- });
+ return this.shards;
}
/**
@@ -166,9 +150,9 @@ class ShardingManager extends EventEmitter {
}
/**
- * Evaluates a script on all shards, in the context of the Clients.
+ * Evaluates a script on all shards, in the context of the {@link Client}s.
* @param {string} script JavaScript to run on each shard
- * @returns {Promise} Results of the script execution
+ * @returns {Promise>} Results of the script execution
*/
broadcastEval(script) {
const promises = [];
@@ -179,7 +163,7 @@ class ShardingManager extends EventEmitter {
/**
* Fetches a client property value of each shard.
* @param {string} prop Name of the client property to get, using periods for nesting
- * @returns {Promise}
+ * @returns {Promise>}
* @example
* manager.fetchClientValues('guilds.size')
* .then(results => {
@@ -194,6 +178,24 @@ class ShardingManager extends EventEmitter {
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
return Promise.all(promises);
}
+
+ /**
+ * Kills all running shards and respawns them.
+ * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
+ * @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
+ * (in milliseconds)
+ * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
+ * @returns {Promise>}
+ */
+ async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) {
+ let s = 0;
+ for (const shard of this.shards) {
+ const promises = [shard.respawn(respawnDelay, waitForReady)];
+ if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay));
+ await Promise.all(promises); // eslint-disable-line no-await-in-loop
+ }
+ return this.shards;
+ }
}
module.exports = ShardingManager;
diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js
index 398910d50..c4256dfe2 100644
--- a/src/stores/DataStore.js
+++ b/src/stores/DataStore.js
@@ -1,4 +1,5 @@
const Collection = require('../util/Collection');
+let Structures;
/**
* Manages the creation, retrieval and deletion of a specific data model.
@@ -7,8 +8,9 @@ const Collection = require('../util/Collection');
class DataStore extends Collection {
constructor(client, iterable, holds) {
super();
+ if (!Structures) Structures = require('../util/Structures');
Object.defineProperty(this, 'client', { value: client });
- Object.defineProperty(this, 'holds', { value: holds });
+ Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) || holds });
if (iterable) for (const item of iterable) this.create(item);
}
diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js
new file mode 100644
index 000000000..b3c3ec012
--- /dev/null
+++ b/src/stores/ReactionUserStore.js
@@ -0,0 +1,33 @@
+const DataStore = require('./DataStore');
+/**
+ * A data store to store User models who reacted to a MessageReaction.
+ * @extends {DataStore}
+ */
+class ReactionUserStore extends DataStore {
+ constructor(client, iterable, reaction) {
+ super(client, iterable, require('../structures/User'));
+ this.reaction = reaction;
+ }
+
+ /**
+ * Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs.
+ * @param {Object} [options] Options for fetching the users
+ * @param {number} [options.limit=100] The maximum amount of users to fetch, defaults to 100
+ * @param {Snowflake} [options.before] Limit fetching users to those with an id lower than the supplied id
+ * @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id
+ * @returns {Promise>}
+ */
+ async fetch({ limit = 100, after, before } = {}) {
+ const message = this.reaction.message;
+ const users = await this.client.api.channels[message.channel.id].messages[message.id]
+ .reactions[this.reaction.emoji.identifier]
+ .get({ query: { limit, before, after } });
+ for (const rawUser of users) {
+ const user = this.client.users.create(rawUser);
+ this.set(user.id, user);
+ }
+ return this;
+ }
+}
+
+module.exports = ReactionUserStore;
diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js
index 2c063f73d..d7121a32b 100644
--- a/src/structures/CategoryChannel.js
+++ b/src/structures/CategoryChannel.js
@@ -6,7 +6,7 @@ const GuildChannel = require('./GuildChannel');
*/
class CategoryChannel extends GuildChannel {
/**
- * The channels that are part of this category
+ * Channels that are part of this category
* @type {?Collection}
* @readonly
*/
diff --git a/src/structures/Channel.js b/src/structures/Channel.js
index 33b851f09..04867b118 100644
--- a/src/structures/Channel.js
+++ b/src/structures/Channel.js
@@ -66,32 +66,37 @@ class Channel extends Base {
}
static create(client, data, guild) {
- const DMChannel = require('./DMChannel');
- const GroupDMChannel = require('./GroupDMChannel');
- const TextChannel = require('./TextChannel');
- const VoiceChannel = require('./VoiceChannel');
- const CategoryChannel = require('./CategoryChannel');
- const GuildChannel = require('./GuildChannel');
+ const Structures = require('../util/Structures');
let channel;
if (data.type === ChannelTypes.DM) {
+ const DMChannel = Structures.get('DMChannel');
channel = new DMChannel(client, data);
} else if (data.type === ChannelTypes.GROUP) {
+ const GroupDMChannel = Structures.get('GroupDMChannel');
channel = new GroupDMChannel(client, data);
} else {
guild = guild || client.guilds.get(data.guild_id);
if (guild) {
switch (data.type) {
- case ChannelTypes.TEXT:
+ case ChannelTypes.TEXT: {
+ const TextChannel = Structures.get('TextChannel');
channel = new TextChannel(guild, data);
break;
- case ChannelTypes.VOICE:
+ }
+ case ChannelTypes.VOICE: {
+ const VoiceChannel = Structures.get('VoiceChannel');
channel = new VoiceChannel(guild, data);
break;
- case ChannelTypes.CATEGORY:
+ }
+ case ChannelTypes.CATEGORY: {
+ const CategoryChannel = Structures.get('CategoryChannel');
channel = new CategoryChannel(guild, data);
break;
- default:
+ }
+ default: {
+ const GuildChannel = Structures.get('GuildChannel');
channel = new GuildChannel(guild, data);
+ }
}
guild.channels.set(channel.id, channel);
}
diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js
index ccca3f7e0..073fce7ba 100644
--- a/src/structures/ClientApplication.js
+++ b/src/structures/ClientApplication.js
@@ -150,11 +150,12 @@ class ClientApplication extends Base {
* @returns {Promise