@@ -31,9 +30,9 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
**Node.js 8.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
-Without voice support: `npm install discord.js --save`
-With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
-With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
+Without voice support: `npm i discord.js`
+With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm i discord.js node-opus`
+With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm i discord.js opusscript`
### Audio engines
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
@@ -41,13 +40,13 @@ Using opusscript is only recommended for development environments where node-opu
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
-- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`)
-- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`)
-- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack --save`)
+- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm i zlib-sync`)
+- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm i discordapp/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium --save`)
- - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`)
-- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`)
+ - [sodium](https://www.npmjs.com/package/sodium) (`npm i sodium`)
+ - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm i libsodium-wrappers`)
+- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm i uws`)
+- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm i bufferutil`)
## Example usage
```js
@@ -70,11 +69,14 @@ client.login('your token');
## Links
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
* [Documentation](https://discord.js.org/#/docs)
-* [Discord.js server](https://discord.gg/bRCvFy9)
-* [Discord API server](https://discord.gg/rV4BwdK)
+* [Discord.js Discord server](https://discord.gg/bRCvFy9)
+* [Discord API Discord server](https://discord.gg/discord-api)
* [GitHub](https://github.com/hydrabolt/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
-* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
+* [Related libraries](https://discordapi.com/unofficial/libs.html)
+
+### Extensions
+* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](https://github.com/devsnek/discord-rpc))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
diff --git a/docs/general/welcome.md b/docs/general/welcome.md
index 11815ee13..84b06ed33 100644
--- a/docs/general/welcome.md
+++ b/docs/general/welcome.md
@@ -1,7 +1,7 @@
-
+
diff --git a/package.json b/package.json
index 363f6b99c..fd2b846c8 100644
--- a/package.json
+++ b/package.json
@@ -34,12 +34,11 @@
"runkitExampleFilename": "./docs/examples/ping.js",
"unpkg": "./webpack/discord.min.js",
"dependencies": {
- "long": "^3.0.0",
"pako": "^1.0.0",
"prism-media": "^0.0.2",
- "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",
@@ -54,11 +53,11 @@
"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",
"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/ClientManager.js b/src/client/ClientManager.js
index 9e0d570d2..ad006b8fe 100644
--- a/src/client/ClientManager.js
+++ b/src/client/ClientManager.js
@@ -22,6 +22,7 @@ class ClientManager {
/**
* The status of the client
+ * @readonly
* @type {number}
*/
get status() {
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/actions/ActionsManager.js b/src/client/actions/ActionsManager.js
index 8341e7453..9708d17b2 100644
--- a/src/client/actions/ActionsManager.js
+++ b/src/client/actions/ActionsManager.js
@@ -14,13 +14,11 @@ class ActionsManager {
this.register(require('./ChannelUpdate'));
this.register(require('./GuildDelete'));
this.register(require('./GuildUpdate'));
- this.register(require('./GuildMemberGet'));
this.register(require('./GuildMemberRemove'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
this.register(require('./GuildRoleUpdate'));
- this.register(require('./UserGet'));
this.register(require('./UserUpdate'));
this.register(require('./UserNoteUpdate'));
this.register(require('./GuildSync'));
diff --git a/src/client/actions/GuildMemberGet.js b/src/client/actions/GuildMemberGet.js
deleted file mode 100644
index 5bf2aafec..000000000
--- a/src/client/actions/GuildMemberGet.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const Action = require('./Action');
-
-class GuildMemberGetAction extends Action {
- handle(guild, data) {
- const member = guild.members.create(data);
- return { member };
- }
-}
-
-module.exports = GuildMemberGetAction;
diff --git a/src/client/actions/UserGet.js b/src/client/actions/UserGet.js
deleted file mode 100644
index 4a135dd58..000000000
--- a/src/client/actions/UserGet.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const Action = require('./Action');
-
-class UserGetAction extends Action {
- handle(data) {
- const client = this.client;
- const user = client.users.create(data);
- return { user };
- }
-}
-
-module.exports = UserGetAction;
diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js
index 678045dca..5a2c54c73 100644
--- a/src/client/voice/VoiceBroadcast.js
+++ b/src/client/voice/VoiceBroadcast.js
@@ -236,7 +236,7 @@ class VoiceBroadcast extends VolumeInterface {
}
/**
- * Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
+ * Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description).
* @param {string} input The arbitrary input
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
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/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js
index a30be35a8..4e3835ef0 100644
--- a/src/client/websocket/WebSocketConnection.js
+++ b/src/client/websocket/WebSocketConnection.js
@@ -272,7 +272,7 @@ class WebSocketConnection extends EventEmitter {
try {
const packet = WebSocket.unpack(this.inflate.result);
this.onPacket(packet);
- if (this.client.listenerCount('raw')) this.client.emit('raw', data);
+ if (this.client.listenerCount('raw')) this.client.emit('raw', packet);
} catch (err) {
this.client.emit('debug', err);
}
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 746aab001..244c6a5ea 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,23 +15,44 @@ module.exports = {
DataResolver: require('./util/DataResolver'),
DataStore: require('./stores/DataStore'),
DiscordAPIError: require('./rest/DiscordAPIError'),
- EvaluatedPermissions: require('./util/Permissions'),
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,
+ // Stores
+ ChannelStore: require('./stores/ChannelStore'),
+ ClientPresenceStore: require('./stores/ClientPresenceStore'),
+ EmojiStore: require('./stores/EmojiStore'),
+ 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
escapeMarkdown: Util.escapeMarkdown,
fetchRecommendedShards: Util.fetchRecommendedShards,
splitMessage: Util.splitMessage,
// Structures
+ Base: require('./structures/Base'),
Activity: require('./structures/Presence').Activity,
+ CategoryChannel: require('./structures/CategoryChannel'),
Channel: require('./structures/Channel'),
- ClientUser: require('./structures/ClientUser'),
+ ClientApplication: require('./structures/ClientApplication'),
+ 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'),
Collector: require('./structures/interfaces/Collector'),
DMChannel: require('./structures/DMChannel'),
@@ -48,15 +69,17 @@ module.exports = {
MessageEmbed: require('./structures/MessageEmbed'),
MessageMentions: require('./structures/MessageMentions'),
MessageReaction: require('./structures/MessageReaction'),
- ClientApplication: require('./structures/ClientApplication'),
PermissionOverwrites: require('./structures/PermissionOverwrites'),
Presence: require('./structures/Presence').Presence,
- ReactionEmoji: require('./structures/ReactionEmoji'),
ReactionCollector: require('./structures/ReactionCollector'),
+ ReactionEmoji: require('./structures/ReactionEmoji'),
+ RichPresenceAssets: require('./structures/Presence').RichPresenceAssets,
Role: require('./structures/Role'),
TextChannel: require('./structures/TextChannel'),
User: require('./structures/User'),
+ UserConnection: require('./structures/UserConnection'),
VoiceChannel: require('./structures/VoiceChannel'),
+ VoiceRegion: require('./structures/VoiceRegion'),
Webhook: require('./structures/Webhook'),
WebSocket: require('./WebSocket'),
diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js
index 48476cca6..d62423979 100644
--- a/src/rest/RESTManager.js
+++ b/src/rest/RESTManager.js
@@ -12,12 +12,22 @@ class RESTManager {
this.globallyRateLimited = false;
this.tokenPrefix = tokenPrefix;
this.versioned = true;
+ this.timeDifferences = [];
}
get api() {
return routeBuilder(this);
}
+ get timeDifference() {
+ return Math.round(this.timeDifferences.reduce((a, b) => a + b, 0) / this.timeDifferences.length);
+ }
+
+ set timeDifference(ms) {
+ this.timeDifferences.unshift(ms);
+ if (this.timeDifferences.length > 5) this.timeDifferences.length = 5;
+ }
+
getAuth() {
const token = this.client.token || this.client.accessToken;
const prefixed = !!this.client.application || (this.client.user && this.client.user.bot);
diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js
index aaeed9d69..c4226a45c 100644
--- a/src/rest/handlers/RequestHandler.js
+++ b/src/rest/handlers/RequestHandler.js
@@ -9,7 +9,6 @@ class RequestHandler {
this.limit = Infinity;
this.resetTime = null;
this.remaining = 1;
- this.timeDifference = 0;
this.queue = [];
}
@@ -32,7 +31,7 @@ class RequestHandler {
const finish = timeout => {
if (timeout || this.limited) {
if (!timeout) {
- timeout = this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset;
+ timeout = this.resetTime - Date.now() + this.manager.timeDifference + this.client.options.restTimeOffset;
}
// eslint-disable-next-line prefer-promise-reject-errors
reject({ timeout });
@@ -40,17 +39,18 @@ 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,
limit: this.limit,
- timeDifference: this.timeDifference,
+ timeDifference: this.manager.timeDifference,
method: item.request.method,
path: item.request.path,
route: item.request.route,
@@ -66,7 +66,7 @@ class RequestHandler {
this.limit = Number(res.headers['x-ratelimit-limit']);
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.remaining = Number(res.headers['x-ratelimit-remaining']);
- this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
+ this.manager.timeDifference = Date.now() - new Date(res.headers.date).getTime();
}
if (err) {
if (err.status === 429) {
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/ChannelStore.js b/src/stores/ChannelStore.js
index 5491b8e59..2ceee8000 100644
--- a/src/stores/ChannelStore.js
+++ b/src/stores/ChannelStore.js
@@ -95,7 +95,7 @@ class ChannelStore extends DataStore {
* @memberof ChannelStore
* @instance
* @param {ChannelResolvable} channel The channel resolvable to resolve
- * @returns {?string}
+ * @returns {?Snowflake}
*/
}
diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js
index 85ce17c90..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);
}
@@ -37,7 +39,7 @@ class DataStore extends Collection {
/**
* Resolves a data entry to a instance ID.
* @param {string|Instance} idOrInstance The id or instance of something in this DataStore
- * @returns {?string}
+ * @returns {?Snowflake}
*/
resolveID(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance.id;
diff --git a/src/stores/EmojiStore.js b/src/stores/EmojiStore.js
index 83b4df812..035cc3d4d 100644
--- a/src/stores/EmojiStore.js
+++ b/src/stores/EmojiStore.js
@@ -38,7 +38,7 @@ class EmojiStore extends DataStore {
/**
* Resolves a EmojiResolvable to a Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
- * @returns {?string}
+ * @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js
index 3e03c8110..0bc3e8c4e 100644
--- a/src/stores/GuildChannelStore.js
+++ b/src/stores/GuildChannelStore.js
@@ -42,7 +42,7 @@ class GuildChannelStore extends DataStore {
* @memberof GuildChannelStore
* @instance
* @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
- * @returns {?string}
+ * @returns {?Snowflake}
*/
}
diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js
index ba8cac903..ad0acbed9 100644
--- a/src/stores/GuildMemberStore.js
+++ b/src/stores/GuildMemberStore.js
@@ -41,7 +41,7 @@ class GuildMemberStore extends DataStore {
/**
* Resolves a GuildMemberResolvable to an member ID string.
* @param {GuildMemberResolvable} member The user that is part of the guild
- * @returns {?string}
+ * @returns {?Snowflake}
*/
resolveID(member) {
const memberResolveable = super.resolveID(member);
diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js
index 5e3e792a4..45e9c9cfc 100644
--- a/src/stores/GuildStore.js
+++ b/src/stores/GuildStore.js
@@ -33,7 +33,7 @@ class GuildStore extends DataStore {
* @memberof GuildStore
* @instance
* @param {GuildResolvable} guild The guild resolvable to identify
- * @returns {?string}
+ * @returns {?Snowflake}
*/
}
diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js
index 75d01620c..cc0ff60b0 100644
--- a/src/stores/MessageStore.js
+++ b/src/stores/MessageStore.js
@@ -112,7 +112,7 @@ class MessageStore extends DataStore {
* @memberof MessageStore
* @instance
* @param {MessageResolvable} message The message resolvable to resolve
- * @returns {?string}
+ * @returns {?Snowflake}
*/
}
diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js
index 1c2649712..8322c9c65 100644
--- a/src/stores/PresenceStore.js
+++ b/src/stores/PresenceStore.js
@@ -39,7 +39,7 @@ class PresenceStore extends DataStore {
/**
* Resolves a PresenceResolvable to a Presence ID string.
* @param {PresenceResolvable} presence The presence resolvable to resolve
- * @returns {?string}
+ * @returns {?Snowflake}
*/
resolveID(presence) {
const presenceResolveable = super.resolveID(presence);
diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js
index bcbca72ac..c11b4e176 100644
--- a/src/stores/ReactionStore.js
+++ b/src/stores/ReactionStore.js
@@ -38,7 +38,7 @@ class ReactionStore extends DataStore {
* @memberof ReactionStore
* @instance
* @param {MessageReactionResolvable} role The role resolvable to resolve
- * @returns {?string}
+ * @returns {?Snowflake}
*/
}
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/stores/RoleStore.js b/src/stores/RoleStore.js
index 7501cb459..bb8cd749d 100644
--- a/src/stores/RoleStore.js
+++ b/src/stores/RoleStore.js
@@ -38,7 +38,7 @@ class RoleStore extends DataStore {
* @memberof RoleStore
* @instance
* @param {RoleResolvable} role The role resolvable to resolve
- * @returns {?string}
+ * @returns {?Snowflake}
*/
}
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 6ee4909a3..432a0768a 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -35,7 +35,7 @@ class UserStore extends DataStore {
/**
* Resolves a UserResolvable to a user ID string.
* @param {UserResolvable} user The UserResolvable to identify
- * @returns {?string}
+ * @returns {?Snowflake}
*/
resolveID(user) {
if (user instanceof GuildMember) return user.user.id;
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 0d2646cee..073fce7ba 100644
--- a/src/structures/ClientApplication.js
+++ b/src/structures/ClientApplication.js
@@ -150,11 +150,12 @@ class ClientApplication extends Base {
* @returns {Promise