mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-15 19:13:31 +01:00
fix: Internal Sharding, this time fixed™ (#3140)
* src: WIP Internal Sharding refactor * src: Refactor unavailable guild check Co-Authored-By: kyranet <kyradiscord@gmail.com> * src: More WIP Code F in the chat to the old manager * src: It should work but Discord says no. Seriously why is this not working! * fix: Inflator causing issues * src: Finishing touches and typings * misc: Proper debug message * fix: Making things hidden needs writable: true as well * fix: Sharding allowing multiple of the same shard, negative shards or strings * fix: Again... edge cases I love you guys .w. * misc: Touchups * misc: Better error? * docs: Typo * typings: Requested changes * src: Requested changes * src: Fix issues, validate provided shard options and more * src: Forgot to remove the listener * lint: eslint complaining * fix: Setting shardCount to auto crashing the process * misc: Requested changes * typings: Correct typings for shardCount client option * typings: Add invalidSession event to the shard and correct typings * src: Minor docs adjustements, and code consistency between setHelloTimeout and setHeartbeatTimeout * src: Don't block reconnect while creating shards Might fix silent disconnects *again* * src: Prevent reconnect from running if the Manager isn't READY That way, if a shard dies while we're still spawning, it won't cause issues * fix: Retry to reconnect if there's a network error going on. The manager *should* keep reconnecting unless the token is invalid * src: Enhance onClose handler for shards in the manager - If the close code is between 1000 and 2000 (inclusive), you cannot resume I tested this locally - If there's a session ID still present, immediately try to resume Faster resumes :papaBless: Otherwise, the invalid session event will trigger and it'll handle accordingly I swear if I see a SINGULAR Silent DC I'm yeeting * src: Fix error check * src: Make sure message exists on the error * src: Used the wrong property for the shardQueue * src: Make the hello timeout be made by the client god help * docs: Correct docs for WSEvents * misc: Remove old events from the Events constant * src: Throw the HTTP error if we don't get a 401 * typings: Can't forget about them * src: Implement some more fail safes just in case Seriously, better safe than sorry! Gotta failproof it completely
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^10.12.24",
|
"@types/node": "^10.12.24",
|
||||||
|
"@types/ws": "^6.0.1",
|
||||||
"discord.js-docgen": "discordjs/docgen",
|
"discord.js-docgen": "discordjs/docgen",
|
||||||
"eslint": "^5.13.0",
|
"eslint": "^5.13.0",
|
||||||
"json-filter-loader": "^1.0.0",
|
"json-filter-loader": "^1.0.0",
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ const UserStore = require('../stores/UserStore');
|
|||||||
const ChannelStore = require('../stores/ChannelStore');
|
const ChannelStore = require('../stores/ChannelStore');
|
||||||
const GuildStore = require('../stores/GuildStore');
|
const GuildStore = require('../stores/GuildStore');
|
||||||
const GuildEmojiStore = require('../stores/GuildEmojiStore');
|
const GuildEmojiStore = require('../stores/GuildEmojiStore');
|
||||||
const { Events, WSCodes, browser, DefaultOptions } = require('../util/Constants');
|
const { Events, browser, DefaultOptions } = require('../util/Constants');
|
||||||
const { delayFor } = require('../util/Util');
|
|
||||||
const DataResolver = require('../util/DataResolver');
|
const DataResolver = require('../util/DataResolver');
|
||||||
const Structures = require('../util/Structures');
|
const Structures = require('../util/Structures');
|
||||||
const { Error, TypeError, RangeError } = require('../errors');
|
const { Error, TypeError, RangeError } = require('../errors');
|
||||||
@@ -40,23 +39,33 @@ class Client extends BaseClient {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.shards === DefaultOptions.shards) {
|
if (this.options.shards === DefaultOptions.shards) {
|
||||||
if ('SHARDS' in data) {
|
if ('SHARDS' in data) {
|
||||||
this.options.shards = JSON.parse(data.SHARDS);
|
this.options.shards = JSON.parse(data.SHARDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.totalShardCount === DefaultOptions.totalShardCount) {
|
if (this.options.totalShardCount === DefaultOptions.totalShardCount) {
|
||||||
if ('TOTAL_SHARD_COUNT' in data) {
|
if ('TOTAL_SHARD_COUNT' in data) {
|
||||||
this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT);
|
this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT);
|
||||||
} else if (Array.isArray(this.options.shards)) {
|
} else if (this.options.shards instanceof Array) {
|
||||||
this.options.totalShardCount = this.options.shards.length;
|
this.options.totalShardCount = this.options.shards.length;
|
||||||
} else {
|
} else {
|
||||||
this.options.totalShardCount = this.options.shardCount;
|
this.options.totalShardCount = this.options.shardCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof this.options.shards === 'undefined' && this.options.shardCount) {
|
|
||||||
this.options.shards = [];
|
if (typeof this.options.shards === 'undefined' && typeof this.options.shardCount === 'number') {
|
||||||
for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i);
|
this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.options.shards === 'number') this.options.shards = [this.options.shards];
|
||||||
|
|
||||||
|
if (typeof this.options.shards !== 'undefined') {
|
||||||
|
this.options.shards = [...new Set(
|
||||||
|
this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity)
|
||||||
|
)];
|
||||||
}
|
}
|
||||||
|
|
||||||
this._validateOptions();
|
this._validateOptions();
|
||||||
@@ -199,55 +208,21 @@ class Client extends BaseClient {
|
|||||||
async login(token = this.token) {
|
async login(token = this.token) {
|
||||||
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
|
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
|
||||||
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
|
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
|
||||||
this.emit(Events.DEBUG, `Authenticating using token ${token}`);
|
this.emit(Events.DEBUG, `Provided token: ${token}`);
|
||||||
let endpoint = this.api.gateway;
|
|
||||||
if (this.options.shardCount === 'auto') endpoint = endpoint.bot;
|
|
||||||
const res = await endpoint.get();
|
|
||||||
if (this.options.presence) {
|
if (this.options.presence) {
|
||||||
this.options.ws.presence = await this.presence._parse(this.options.presence);
|
this.options.ws.presence = await this.presence._parse(this.options.presence);
|
||||||
}
|
}
|
||||||
if (res.session_start_limit && res.session_start_limit.remaining === 0) {
|
|
||||||
const { session_start_limit: { reset_after } } = res;
|
this.emit(Events.DEBUG, 'Preparing to connect to the gateway...');
|
||||||
this.emit(Events.DEBUG, `Exceeded identify threshold, setting a timeout for ${reset_after} ms`);
|
|
||||||
await delayFor(reset_after);
|
try {
|
||||||
|
await this.ws.connect();
|
||||||
|
return this.token;
|
||||||
|
} catch (error) {
|
||||||
|
this.destroy();
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
const gateway = `${res.url}/`;
|
|
||||||
if (this.options.shardCount === 'auto') {
|
|
||||||
this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`);
|
|
||||||
this.options.shardCount = res.shards;
|
|
||||||
this.options.totalShardCount = res.shards;
|
|
||||||
if (typeof this.options.shards === 'undefined' || !this.options.shards.length) {
|
|
||||||
this.options.shards = [];
|
|
||||||
for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.emit(Events.DEBUG, `Using gateway ${gateway}`);
|
|
||||||
this.ws.connect(gateway);
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const onready = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
this.removeListener(Events.DISCONNECT, ondisconnect);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
const ondisconnect = event => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
this.removeListener(Events.READY, onready);
|
|
||||||
this.destroy();
|
|
||||||
if (WSCodes[event.code]) {
|
|
||||||
reject(new Error(WSCodes[event.code]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
this.removeListener(Events.READY, onready);
|
|
||||||
this.removeListener(Events.DISCONNECT, ondisconnect);
|
|
||||||
this.destroy();
|
|
||||||
reject(new Error('WS_CONNECTION_TIMEOUT'));
|
|
||||||
}, this.options.shardCount * 25e3);
|
|
||||||
if (timeout.unref !== undefined) timeout.unref();
|
|
||||||
this.once(Events.READY, onready);
|
|
||||||
this.once(Events.DISCONNECT, ondisconnect);
|
|
||||||
});
|
|
||||||
return token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -397,9 +372,10 @@ class Client extends BaseClient {
|
|||||||
if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) {
|
if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) {
|
||||||
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"');
|
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"');
|
||||||
}
|
}
|
||||||
if (options.shards && typeof options.shards !== 'number' && !Array.isArray(options.shards)) {
|
if (options.shards && !(options.shards instanceof Array)) {
|
||||||
throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array');
|
throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array');
|
||||||
}
|
}
|
||||||
|
if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS');
|
||||||
if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1');
|
if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1');
|
||||||
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
|
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
|
||||||
throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number');
|
throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number');
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { Error: DJSError } = require('../../errors');
|
||||||
const Collection = require('../../util/Collection');
|
const Collection = require('../../util/Collection');
|
||||||
const Util = require('../../util/Util');
|
const Util = require('../../util/Util');
|
||||||
const WebSocketShard = require('./WebSocketShard');
|
const WebSocketShard = require('./WebSocketShard');
|
||||||
const { Events, Status, WSEvents } = require('../../util/Constants');
|
const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants');
|
||||||
const PacketHandlers = require('./handlers');
|
const PacketHandlers = require('./handlers');
|
||||||
|
|
||||||
const BeforeReadyWhitelist = [
|
const BeforeReadyWhitelist = [
|
||||||
@@ -16,6 +17,8 @@ const BeforeReadyWhitelist = [
|
|||||||
WSEvents.GUILD_MEMBER_REMOVE,
|
WSEvents.GUILD_MEMBER_REMOVE,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The WebSocket manager for this client.
|
* The WebSocket manager for this client.
|
||||||
*/
|
*/
|
||||||
@@ -25,6 +28,7 @@ class WebSocketManager {
|
|||||||
* The client that instantiated this WebSocketManager
|
* The client that instantiated this WebSocketManager
|
||||||
* @type {Client}
|
* @type {Client}
|
||||||
* @readonly
|
* @readonly
|
||||||
|
* @name WebSocketManager#client
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(this, 'client', { value: client });
|
Object.defineProperty(this, 'client', { value: client });
|
||||||
|
|
||||||
@@ -34,6 +38,13 @@ class WebSocketManager {
|
|||||||
*/
|
*/
|
||||||
this.gateway = undefined;
|
this.gateway = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of shards this manager handles
|
||||||
|
* @private
|
||||||
|
* @type {number|string}
|
||||||
|
*/
|
||||||
|
this.totalShards = this.client.options.shardCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of all shards this manager handles
|
* A collection of all shards this manager handles
|
||||||
* @type {Collection<number, WebSocketShard>}
|
* @type {Collection<number, WebSocketShard>}
|
||||||
@@ -41,18 +52,20 @@ class WebSocketManager {
|
|||||||
this.shards = new Collection();
|
this.shards = new Collection();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of shards to be spawned or reconnected
|
* An array of shards to be connected or that need to reconnect
|
||||||
* @type {Array<number|WebSocketShard>}
|
* @type {Set<WebSocketShard>}
|
||||||
* @private
|
* @private
|
||||||
|
* @name WebSocketManager#shardQueue
|
||||||
*/
|
*/
|
||||||
this.shardQueue = [];
|
Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of queued events before this WebSocketManager became ready
|
* An array of queued events before this WebSocketManager became ready
|
||||||
* @type {object[]}
|
* @type {object[]}
|
||||||
* @private
|
* @private
|
||||||
|
* @name WebSocketManager#packetQueue
|
||||||
*/
|
*/
|
||||||
this.packetQueue = [];
|
Object.defineProperty(this, 'packetQueue', { value: [] });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current status of this WebSocketManager
|
* The current status of this WebSocketManager
|
||||||
@@ -61,28 +74,28 @@ class WebSocketManager {
|
|||||||
this.status = Status.IDLE;
|
this.status = Status.IDLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this manager is expected to close
|
* If this manager was destroyed. It will prevent shards from reconnecting
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.expectingClose = false;
|
this.destroyed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this manager is currently reconnecting one or multiple shards
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.reconnecting = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current session limit of the client
|
* The current session limit of the client
|
||||||
* @type {?Object}
|
|
||||||
* @private
|
* @private
|
||||||
|
* @type {?Object}
|
||||||
* @prop {number} total Total number of identifies available
|
* @prop {number} total Total number of identifies available
|
||||||
* @prop {number} remaining Number of identifies remaining
|
* @prop {number} remaining Number of identifies remaining
|
||||||
* @prop {number} reset_after Number of milliseconds after which the limit resets
|
* @prop {number} reset_after Number of milliseconds after which the limit resets
|
||||||
*/
|
*/
|
||||||
this.sessionStartLimit = null;
|
this.sessionStartLimit = undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* If the manager is currently reconnecting shards
|
|
||||||
* @type {boolean}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this.isReconnectingShards = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,121 +109,198 @@ class WebSocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits a debug event.
|
* Emits a debug message.
|
||||||
* @param {string} message Debug message
|
* @param {string} message The debug message
|
||||||
|
* @param {?WebSocketShard} [shard] The shard that emitted this message, if any
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
debug(message) {
|
debug(message, shard) {
|
||||||
this.client.emit(Events.DEBUG, message);
|
this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a new identify payload can be sent.
|
* Connects this manager to the gateway.
|
||||||
* @private
|
* @private
|
||||||
* @returns {Promise<boolean|number>}
|
|
||||||
*/
|
*/
|
||||||
async _checkSessionLimit() {
|
async connect() {
|
||||||
this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit);
|
const invalidToken = new DJSError(WSCodes[4004]);
|
||||||
const { remaining, reset_after } = this.sessionStartLimit;
|
const {
|
||||||
if (remaining !== 0) return true;
|
url: gatewayURL,
|
||||||
return reset_after;
|
shards: recommendedShards,
|
||||||
}
|
session_start_limit: sessionStartLimit,
|
||||||
|
} = await this.client.api.gateway.bot.get().catch(error => {
|
||||||
|
throw error.httpStatus === 401 ? invalidToken : error;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
this.sessionStartLimit = sessionStartLimit;
|
||||||
* Handles the session identify rate limit for creating a shard.
|
|
||||||
* @private
|
const { total, remaining, reset_after } = sessionStartLimit;
|
||||||
*/
|
|
||||||
async _handleSessionLimit() {
|
this.debug(`Fetched Gateway Information
|
||||||
const canSpawn = await this._checkSessionLimit();
|
URL: ${gatewayURL}
|
||||||
if (typeof canSpawn === 'number') {
|
Recommended Shards: ${recommendedShards}`);
|
||||||
this.debug(`Exceeded identify threshold, setting a timeout for ${canSpawn}ms`);
|
|
||||||
await Util.delayFor(canSpawn);
|
this.debug(`Session Limit Information
|
||||||
|
Total: ${total}
|
||||||
|
Remaining: ${remaining}`);
|
||||||
|
|
||||||
|
this.gateway = `${gatewayURL}/`;
|
||||||
|
|
||||||
|
if (this.totalShards === 'auto') {
|
||||||
|
this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`);
|
||||||
|
this.totalShards = this.client.options.shardCount = this.client.options.totalShardCount = recommendedShards;
|
||||||
|
if (typeof this.client.options.shards === 'undefined' || !this.client.options.shards.length) {
|
||||||
|
this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (this.client.options.shards instanceof Array) {
|
||||||
* Creates a connection to a gateway.
|
const { shards } = this.client.options;
|
||||||
* @param {string} [gateway=this.gateway] The gateway to connect to
|
this.totalShards = shards.length;
|
||||||
* @private
|
this.debug(`Spawning shards: ${shards.join(', ')}`);
|
||||||
*/
|
this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id)));
|
||||||
connect(gateway = this.gateway) {
|
|
||||||
this.gateway = gateway;
|
|
||||||
|
|
||||||
if (typeof this.client.options.shards === 'number') {
|
|
||||||
this.debug(`Spawning shard with ID ${this.client.options.shards}`);
|
|
||||||
this.shardQueue.push(this.client.options.shards);
|
|
||||||
} else if (Array.isArray(this.client.options.shards)) {
|
|
||||||
this.debug(`Spawning ${this.client.options.shards.length} shards`);
|
|
||||||
this.shardQueue.push(...this.client.options.shards);
|
|
||||||
} else {
|
} else {
|
||||||
this.debug(`Spawning ${this.client.options.shardCount} shards`);
|
this.debug(`Spawning ${this.totalShards} shards`);
|
||||||
this.shardQueue.push(...Array.from({ length: this.client.options.shardCount }, (_, index) => index));
|
this.shardQueue = new Set(Array.from({ length: this.totalShards }, (_, id) => new WebSocketShard(this, id)));
|
||||||
}
|
}
|
||||||
this.create();
|
|
||||||
|
await this._handleSessionLimit(remaining, reset_after);
|
||||||
|
|
||||||
|
return this.createShards();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or reconnects a shard.
|
* Handles the creation of a shard.
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
create() {
|
async createShards() {
|
||||||
// Nothing to create
|
// If we don't have any shards to handle, return
|
||||||
if (!this.shardQueue.length) return;
|
if (!this.shardQueue.size) return false;
|
||||||
|
|
||||||
let item = this.shardQueue.shift();
|
const [shard] = this.shardQueue;
|
||||||
if (typeof item === 'string' && !isNaN(item)) item = Number(item);
|
|
||||||
|
|
||||||
if (item instanceof WebSocketShard) {
|
this.shardQueue.delete(shard);
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
this.debug(`[Shard ${item.id}] Failed to connect in 15s... Destroying and trying again`);
|
if (!shard.eventsAttached) {
|
||||||
item.destroy();
|
shard.on(ShardEvents.READY, () => {
|
||||||
if (!this.shardQueue.includes(item)) this.shardQueue.push(item);
|
/**
|
||||||
this.reconnect(true);
|
* Emitted when a shard turns ready.
|
||||||
}, 15000);
|
* @event Client#shardReady
|
||||||
item.once(Events.READY, this._shardReady.bind(this, timeout));
|
* @param {number} id The shard ID that turned ready
|
||||||
item.once(Events.RESUMED, this._shardReady.bind(this, timeout));
|
*/
|
||||||
item.connect();
|
this.client.emit(Events.SHARD_READY, shard.id);
|
||||||
return;
|
|
||||||
|
if (!this.shardQueue.size) this.reconnecting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
shard.on(ShardEvents.RESUMED, () => {
|
||||||
|
/**
|
||||||
|
* Emitted when a shard resumes successfully.
|
||||||
|
* @event Client#shardResumed
|
||||||
|
* @param {number} id The shard ID that resumed
|
||||||
|
*/
|
||||||
|
this.client.emit(Events.SHARD_RESUMED, shard.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
shard.on(ShardEvents.CLOSE, event => {
|
||||||
|
if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) {
|
||||||
|
/**
|
||||||
|
* Emitted when a shard's WebSocket disconnects and will no longer reconnect.
|
||||||
|
* @event Client#shardDisconnected
|
||||||
|
* @param {CloseEvent} event The WebSocket close event
|
||||||
|
* @param {number} id The shard ID that disconnected
|
||||||
|
*/
|
||||||
|
this.client.emit(Events.SHARD_DISCONNECTED, event, shard.id);
|
||||||
|
this.debug(WSCodes[event.code], shard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code >= 1000 && event.code <= 2000) {
|
||||||
|
// Any event code in this range cannot be resumed.
|
||||||
|
shard.sessionID = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when a shard is attempting to reconnect or re-identify.
|
||||||
|
* @event Client#shardReconnecting
|
||||||
|
* @param {number} id The shard ID that is attempting to reconnect
|
||||||
|
*/
|
||||||
|
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||||
|
|
||||||
|
if (shard.sessionID) {
|
||||||
|
this.debug(`Session ID is present, attempting an immediate reconnect...`, shard);
|
||||||
|
shard.connect().catch(() => null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shard.destroy();
|
||||||
|
|
||||||
|
this.shardQueue.add(shard);
|
||||||
|
this.reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
shard.on(ShardEvents.INVALID_SESSION, () => {
|
||||||
|
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||||
|
|
||||||
|
this.shardQueue.add(shard);
|
||||||
|
this.reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
shard.on(ShardEvents.DESTROYED, () => {
|
||||||
|
this.debug('Shard was destroyed but no WebSocket connection existed... Reconnecting...', shard);
|
||||||
|
|
||||||
|
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||||
|
|
||||||
|
this.shardQueue.add(shard);
|
||||||
|
this.reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
shard.eventsAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shard = new WebSocketShard(this, item);
|
this.shards.set(shard.id, shard);
|
||||||
this.shards.set(item, shard);
|
|
||||||
shard.once(Events.READY, this._shardReady.bind(this));
|
try {
|
||||||
|
await shard.connect();
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) {
|
||||||
|
throw new DJSError(WSCodes[error.code]);
|
||||||
|
} else {
|
||||||
|
this.debug('Failed to connect to the gateway, requeueing...', shard);
|
||||||
|
this.shardQueue.add(shard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we have more shards, add a 5s delay
|
||||||
|
if (this.shardQueue.size) {
|
||||||
|
this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`);
|
||||||
|
await Util.delayFor(5000);
|
||||||
|
await this._handleSessionLimit();
|
||||||
|
return this.createShards();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared handler for shards turning ready or resuming.
|
* Handles reconnects for this manager.
|
||||||
* @param {Timeout} [timeout=null] Optional timeout to clear if shard didn't turn ready in time
|
|
||||||
* @private
|
* @private
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
_shardReady(timeout = null) {
|
async reconnect() {
|
||||||
if (timeout) clearTimeout(timeout);
|
if (this.reconnecting || this.status !== Status.READY) return false;
|
||||||
if (this.shardQueue.length) {
|
this.reconnecting = true;
|
||||||
this.client.setTimeout(this._handleSessionLimit.bind(this), 5000);
|
|
||||||
} else {
|
|
||||||
this.isReconnectingShards = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the reconnect of a shard.
|
|
||||||
* @param {WebSocketShard|boolean} shard The shard to reconnect, or a boolean to indicate an immediate reconnect
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
async reconnect(shard) {
|
|
||||||
// If the item is a shard, add it to the queue
|
|
||||||
if (shard instanceof WebSocketShard) this.shardQueue.push(shard);
|
|
||||||
if (typeof shard === 'boolean') {
|
|
||||||
// If a boolean is passed, force a reconnect right now
|
|
||||||
} else if (this.isReconnectingShards) {
|
|
||||||
// If we're already reconnecting shards, and no boolean was provided, return
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isReconnectingShards = true;
|
|
||||||
try {
|
try {
|
||||||
await this._handleSessionLimit();
|
await this._handleSessionLimit();
|
||||||
|
await this.createShards();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
|
||||||
|
if (error.httpStatus !== 401) {
|
||||||
|
this.debug(`Possible network error occured. Retrying in 5s...`);
|
||||||
|
await Util.delayFor(5000);
|
||||||
|
this.reconnecting = false;
|
||||||
|
return this.reconnect();
|
||||||
|
}
|
||||||
// If we get an error at this point, it means we cannot reconnect anymore
|
// If we get an error at this point, it means we cannot reconnect anymore
|
||||||
if (this.client.listenerCount(Events.INVALIDATED)) {
|
if (this.client.listenerCount(Events.INVALIDATED)) {
|
||||||
/**
|
/**
|
||||||
@@ -225,6 +315,52 @@ class WebSocketManager {
|
|||||||
} else {
|
} else {
|
||||||
this.client.destroy();
|
this.client.destroy();
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.reconnecting = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts a packet to every shard this manager handles.
|
||||||
|
* @param {Object} packet The packet to send
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
broadcast(packet) {
|
||||||
|
for (const shard of this.shards.values()) shard.send(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys this manager and all its shards.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this.destroyed) return;
|
||||||
|
this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`);
|
||||||
|
this.destroyed = true;
|
||||||
|
this.shardQueue.clear();
|
||||||
|
for (const shard of this.shards.values()) shard.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the timeout required if we cannot identify anymore.
|
||||||
|
* @param {number} [remaining] The amount of remaining identify sessions that can be done today
|
||||||
|
* @param {number} [resetAfter] The amount of time in which the identify counter resets
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _handleSessionLimit(remaining, resetAfter) {
|
||||||
|
if (typeof remaining === 'undefined' && typeof resetAfter === 'undefined') {
|
||||||
|
const { session_start_limit } = await this.client.api.gateway.bot.get();
|
||||||
|
this.sessionStartLimit = session_start_limit;
|
||||||
|
remaining = session_start_limit.remaining;
|
||||||
|
resetAfter = session_start_limit.reset_after;
|
||||||
|
this.debug(`Session Limit Information
|
||||||
|
Total: ${session_start_limit.total}
|
||||||
|
Remaining: ${remaining}`);
|
||||||
|
}
|
||||||
|
if (!remaining) {
|
||||||
|
this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`);
|
||||||
|
await Util.delayFor(resetAfter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,15 +399,13 @@ class WebSocketManager {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
checkReady() {
|
checkReady() {
|
||||||
if (this.shards.size !== this.client.options.shardCount ||
|
if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) {
|
||||||
this.shards.some(s => s.status !== Status.READY)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let unavailableGuilds = 0;
|
const unavailableGuilds = this.client.guilds.reduce((acc, guild) => guild.available ? acc : acc + 1, 0);
|
||||||
for (const guild of this.client.guilds.values()) {
|
|
||||||
if (!guild.available) unavailableGuilds++;
|
// TODO: Rethink implementation for this
|
||||||
}
|
|
||||||
if (unavailableGuilds === 0) {
|
if (unavailableGuilds === 0) {
|
||||||
this.status = Status.NEARLY;
|
this.status = Status.NEARLY;
|
||||||
if (!this.client.options.fetchAllMembers) return this.triggerReady();
|
if (!this.client.options.fetchAllMembers) return this.triggerReady();
|
||||||
@@ -280,16 +414,18 @@ class WebSocketManager {
|
|||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then(() => this.triggerReady())
|
.then(() => this.triggerReady())
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.debug(`Failed to fetch all members before ready! ${e}`);
|
this.debug(`Failed to fetch all members before ready! ${e}\n${e.stack}`);
|
||||||
this.triggerReady();
|
this.triggerReady();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.debug(`There are ${unavailableGuilds} unavailable guilds. Waiting for their GUILD_CREATE packets`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Causes the client to be marked as ready and emits the ready event.
|
* Causes the client to be marked as ready and emits the ready event.
|
||||||
* @returns {void}
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
triggerReady() {
|
triggerReady() {
|
||||||
@@ -303,31 +439,10 @@ class WebSocketManager {
|
|||||||
* Emitted when the client becomes ready to start working.
|
* Emitted when the client becomes ready to start working.
|
||||||
* @event Client#ready
|
* @event Client#ready
|
||||||
*/
|
*/
|
||||||
this.client.emit(Events.READY);
|
this.client.emit(Events.CLIENT_READY);
|
||||||
|
|
||||||
this.handlePacket();
|
this.handlePacket();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts a message to every shard in this WebSocketManager.
|
|
||||||
* @param {*} packet The packet to send
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
broadcast(packet) {
|
|
||||||
for (const shard of this.shards.values()) shard.send(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroys all shards.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
if (this.expectingClose) return;
|
|
||||||
this.expectingClose = true;
|
|
||||||
this.isReconnectingShards = false;
|
|
||||||
this.shardQueue.length = 0;
|
|
||||||
for (const shard of this.shards.values()) shard.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WebSocketManager;
|
module.exports = WebSocketManager;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const WebSocket = require('../../WebSocket');
|
const WebSocket = require('../../WebSocket');
|
||||||
const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants');
|
const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants');
|
||||||
const Util = require('../../util/Util');
|
|
||||||
|
|
||||||
let zlib;
|
let zlib;
|
||||||
try {
|
try {
|
||||||
@@ -21,13 +20,13 @@ class WebSocketShard extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The WebSocket Manager of this connection
|
* The WebSocketManager of the shard
|
||||||
* @type {WebSocketManager}
|
* @type {WebSocketManager}
|
||||||
*/
|
*/
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the this shard
|
* The ID of the shard
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -91,20 +90,22 @@ class WebSocketShard extends EventEmitter {
|
|||||||
* @type {Object}
|
* @type {Object}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.ratelimit = {
|
Object.defineProperty(this, 'ratelimit', {
|
||||||
queue: [],
|
value: {
|
||||||
total: 120,
|
queue: [],
|
||||||
remaining: 120,
|
total: 120,
|
||||||
time: 60e3,
|
remaining: 120,
|
||||||
timer: null,
|
time: 60e3,
|
||||||
};
|
timer: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The WebSocket connection for the current shard
|
* The WebSocket connection for the current shard
|
||||||
* @type {?WebSocket}
|
* @type {?WebSocket}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.connection = null;
|
Object.defineProperty(this, 'connection', { value: null, writable: true });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @external Inflate
|
* @external Inflate
|
||||||
@@ -116,9 +117,21 @@ class WebSocketShard extends EventEmitter {
|
|||||||
* @type {?Inflate}
|
* @type {?Inflate}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.inflate = null;
|
Object.defineProperty(this, 'inflate', { value: null, writable: true });
|
||||||
|
|
||||||
if (this.manager.gateway) this.connect();
|
/**
|
||||||
|
* The HELLO timeout
|
||||||
|
* @type {?NodeJS.Timer}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the manager attached its event handlers on the shard
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
Object.defineProperty(this, 'eventsAttached', { value: false, writable: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,82 +146,86 @@ class WebSocketShard extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits a debug event.
|
* Emits a debug event.
|
||||||
* @param {string} message Debug message
|
* @param {string} message The debug message
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
debug(message) {
|
debug(message) {
|
||||||
this.manager.debug(`[Shard ${this.id}] ${message}`);
|
this.manager.debug(message, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a heartbeat to the WebSocket.
|
* Connects the shard to the gateway.
|
||||||
* If this shard didn't receive a heartbeat last time, it will destroy it and reconnect
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
sendHeartbeat() {
|
|
||||||
if (!this.lastHeartbeatAcked) {
|
|
||||||
this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting.");
|
|
||||||
this.connection.close(4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.debug('Sending a heartbeat');
|
|
||||||
this.lastHeartbeatAcked = false;
|
|
||||||
this.lastPingTimestamp = Date.now();
|
|
||||||
this.send({ op: OPCodes.HEARTBEAT, d: this.sequence });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the heartbeat timer for this shard.
|
|
||||||
* @param {number} time If -1, clears the interval, any other number sets an interval
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
setHeartbeatTimer(time) {
|
|
||||||
if (time === -1) {
|
|
||||||
if (this.heartbeatInterval) {
|
|
||||||
this.debug('Clearing heartbeat interval');
|
|
||||||
this.manager.client.clearInterval(this.heartbeatInterval);
|
|
||||||
this.heartbeatInterval = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.debug(`Setting a heartbeat interval for ${time}ms`);
|
|
||||||
this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acknowledges a heartbeat.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
ackHeartbeat() {
|
|
||||||
this.lastHeartbeatAcked = true;
|
|
||||||
const latency = Date.now() - this.lastPingTimestamp;
|
|
||||||
this.debug(`Heartbeat acknowledged, latency of ${latency}ms`);
|
|
||||||
this.pings.unshift(latency);
|
|
||||||
if (this.pings.length > 3) this.pings.length = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects this shard to the gateway.
|
|
||||||
* @private
|
* @private
|
||||||
|
* @returns {Promise<void>} A promise that will resolve if the shard turns ready successfully,
|
||||||
|
* or reject if we couldn't connect
|
||||||
*/
|
*/
|
||||||
connect() {
|
connect() {
|
||||||
const { expectingClose, gateway } = this.manager;
|
const { gateway, client } = this.manager;
|
||||||
if (expectingClose) return;
|
|
||||||
this.inflate = new zlib.Inflate({
|
if (this.status === Status.READY && this.connection && this.connection.readyState === WebSocket.OPEN) {
|
||||||
chunkSize: 65535,
|
return Promise.resolve();
|
||||||
flush: zlib.Z_SYNC_FLUSH,
|
}
|
||||||
to: WebSocket.encoding === 'json' ? 'string' : '',
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onReady = () => {
|
||||||
|
this.off(ShardEvents.CLOSE, onClose);
|
||||||
|
this.off(ShardEvents.RESUMED, onResumed);
|
||||||
|
this.off(ShardEvents.INVALID_SESSION, onInvalid);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResumed = () => {
|
||||||
|
this.off(ShardEvents.CLOSE, onClose);
|
||||||
|
this.off(ShardEvents.READY, onReady);
|
||||||
|
this.off(ShardEvents.INVALID_SESSION, onInvalid);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = event => {
|
||||||
|
this.off(ShardEvents.READY, onReady);
|
||||||
|
this.off(ShardEvents.RESUMED, onResumed);
|
||||||
|
this.off(ShardEvents.INVALID_SESSION, onInvalid);
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInvalid = () => {
|
||||||
|
this.off(ShardEvents.READY, onReady);
|
||||||
|
this.off(ShardEvents.RESUMED, onResumed);
|
||||||
|
this.off(ShardEvents.CLOSE, onClose);
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.once(ShardEvents.READY, onReady);
|
||||||
|
this.once(ShardEvents.RESUMED, onResumed);
|
||||||
|
this.once(ShardEvents.CLOSE, onClose);
|
||||||
|
this.once(ShardEvents.INVALID_SESSION, onInvalid);
|
||||||
|
|
||||||
|
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
|
||||||
|
this.identifyNew();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inflate = new zlib.Inflate({
|
||||||
|
chunkSize: 65535,
|
||||||
|
flush: zlib.Z_SYNC_FLUSH,
|
||||||
|
to: WebSocket.encoding === 'json' ? 'string' : '',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.debug(`Trying to connect to ${gateway}, version ${client.options.ws.version}`);
|
||||||
|
|
||||||
|
this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING;
|
||||||
|
this.setHelloTimeout();
|
||||||
|
|
||||||
|
const ws = this.connection = WebSocket.create(gateway, {
|
||||||
|
v: client.options.ws.version,
|
||||||
|
compress: 'zlib-stream',
|
||||||
|
});
|
||||||
|
ws.onopen = this.onOpen.bind(this);
|
||||||
|
ws.onmessage = this.onMessage.bind(this);
|
||||||
|
ws.onerror = this.onError.bind(this);
|
||||||
|
ws.onclose = this.onClose.bind(this);
|
||||||
});
|
});
|
||||||
this.debug(`Connecting to ${gateway}`);
|
|
||||||
const ws = this.connection = WebSocket.create(gateway, {
|
|
||||||
v: this.manager.client.options.ws.version,
|
|
||||||
compress: 'zlib-stream',
|
|
||||||
});
|
|
||||||
ws.onopen = this.onOpen.bind(this);
|
|
||||||
ws.onmessage = this.onMessage.bind(this);
|
|
||||||
ws.onerror = this.onError.bind(this);
|
|
||||||
ws.onclose = this.onClose.bind(this);
|
|
||||||
this.status = Status.CONNECTING;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,7 +233,8 @@ class WebSocketShard extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.debug('Connected to the gateway');
|
this.debug('Opened a connection to the gateway successfully.');
|
||||||
|
this.status = Status.NEARLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,53 +258,106 @@ class WebSocketShard extends EventEmitter {
|
|||||||
packet = WebSocket.unpack(this.inflate.result);
|
packet = WebSocket.unpack(this.inflate.result);
|
||||||
this.manager.client.emit(Events.RAW, packet, this.id);
|
this.manager.client.emit(Events.RAW, packet, this.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.manager.client.emit(Events.ERROR, err);
|
this.manager.client.emit(Events.SHARD_ERROR, err, this.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.onPacket(packet);
|
this.onPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever an error occurs with the WebSocket.
|
||||||
|
* @param {ErrorEvent} error The error that occurred
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
onError({ error }) {
|
||||||
|
if (error && error.message === 'uWs client connection error') {
|
||||||
|
this.debug('Received a uWs error. Closing the connection and reconnecting...');
|
||||||
|
this.connection.close(4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted whenever a shard's WebSocket encounters a connection error.
|
||||||
|
* @event Client#shardError
|
||||||
|
* @param {Error} error The encountered error
|
||||||
|
* @param {number} shardID The shard that encountered this error
|
||||||
|
*/
|
||||||
|
this.manager.client.emit(Events.SHARD_ERROR, error, this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @external CloseEvent
|
||||||
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @external ErrorEvent
|
||||||
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever a connection to the gateway is closed.
|
||||||
|
* @param {CloseEvent} event Close event that was received
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
onClose(event) {
|
||||||
|
this.closeSequence = this.sequence;
|
||||||
|
this.sequence = -1;
|
||||||
|
this.debug(`WebSocket was closed.
|
||||||
|
Event Code: ${event.code}
|
||||||
|
Clean: ${event.wasClean}
|
||||||
|
Reason: ${event.reason || 'No reason received'}`);
|
||||||
|
|
||||||
|
this.status = Status.DISCONNECTED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when a shard's WebSocket closes.
|
||||||
|
* @private
|
||||||
|
* @event WebSocketShard#close
|
||||||
|
* @param {CloseEvent} event The received event
|
||||||
|
*/
|
||||||
|
this.emit(ShardEvents.CLOSE, event);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called whenever a packet is received.
|
* Called whenever a packet is received.
|
||||||
* @param {Object} packet Packet received
|
* @param {Object} packet The received packet
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
onPacket(packet) {
|
onPacket(packet) {
|
||||||
if (!packet) {
|
if (!packet) {
|
||||||
this.debug('Received null or broken packet');
|
this.debug(`Received broken packet: ${packet}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (packet.t) {
|
switch (packet.t) {
|
||||||
case WSEvents.READY:
|
case WSEvents.READY:
|
||||||
/**
|
/**
|
||||||
* Emitted when a shard becomes ready.
|
* Emitted when the shard becomes ready
|
||||||
* @event WebSocketShard#ready
|
* @event WebSocketShard#ready
|
||||||
*/
|
*/
|
||||||
this.emit(Events.READY);
|
this.emit(ShardEvents.READY);
|
||||||
/**
|
|
||||||
* Emitted when a shard becomes ready.
|
|
||||||
* @event Client#shardReady
|
|
||||||
* @param {number} shardID The ID of the shard
|
|
||||||
*/
|
|
||||||
this.manager.client.emit(Events.SHARD_READY, this.id);
|
|
||||||
|
|
||||||
this.sessionID = packet.d.session_id;
|
this.sessionID = packet.d.session_id;
|
||||||
this.trace = packet.d._trace;
|
this.trace = packet.d._trace;
|
||||||
this.status = Status.READY;
|
this.status = Status.READY;
|
||||||
this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}`);
|
this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}.`);
|
||||||
this.lastHeartbeatAcked = true;
|
this.lastHeartbeatAcked = true;
|
||||||
this.sendHeartbeat();
|
this.sendHeartbeat();
|
||||||
break;
|
break;
|
||||||
case WSEvents.RESUMED: {
|
case WSEvents.RESUMED: {
|
||||||
this.emit(Events.RESUMED);
|
/**
|
||||||
|
* Emitted when the shard resumes successfully
|
||||||
|
* @event WebSocketShard#resumed
|
||||||
|
*/
|
||||||
|
this.emit(ShardEvents.RESUMED);
|
||||||
|
|
||||||
this.trace = packet.d._trace;
|
this.trace = packet.d._trace;
|
||||||
this.status = Status.READY;
|
this.status = Status.READY;
|
||||||
const replayed = packet.s - this.closeSequence;
|
const replayed = packet.s - this.closeSequence;
|
||||||
this.debug(`RESUMED ${this.trace.join(' -> ')} | Replayed ${replayed} events.`);
|
this.debug(`RESUMED ${this.trace.join(' -> ')} | Session ${this.sessionID} | Replayed ${replayed} events.`);
|
||||||
this.lastHeartbeatAcked = true;
|
this.lastHeartbeatAcked = true;
|
||||||
this.sendHeartbeat();
|
this.sendHeartbeat();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +365,7 @@ class WebSocketShard extends EventEmitter {
|
|||||||
|
|
||||||
switch (packet.op) {
|
switch (packet.op) {
|
||||||
case OPCodes.HELLO:
|
case OPCodes.HELLO:
|
||||||
|
this.setHelloTimeout(-1);
|
||||||
this.setHeartbeatTimer(packet.d.heartbeat_interval);
|
this.setHeartbeatTimer(packet.d.heartbeat_interval);
|
||||||
this.identify();
|
this.identify();
|
||||||
break;
|
break;
|
||||||
@@ -301,21 +373,20 @@ class WebSocketShard extends EventEmitter {
|
|||||||
this.connection.close(1001);
|
this.connection.close(1001);
|
||||||
break;
|
break;
|
||||||
case OPCodes.INVALID_SESSION:
|
case OPCodes.INVALID_SESSION:
|
||||||
this.debug(`Session was invalidated. Resumable: ${packet.d}.`);
|
this.debug(`Session invalidated. Resumable: ${packet.d}.`);
|
||||||
// If the session isn't resumable
|
// If we can resume the session, do so immediately
|
||||||
if (!packet.d) {
|
if (packet.d) {
|
||||||
// Reset the sequence, since it isn't valid anymore
|
this.identifyResume();
|
||||||
this.sequence = -1;
|
|
||||||
// If we had a session ID before
|
|
||||||
if (this.sessionID) {
|
|
||||||
this.sessionID = null;
|
|
||||||
this.connection.close(1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.connection.close(1000);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.identifyResume();
|
// Reset the sequence
|
||||||
|
this.sequence = -1;
|
||||||
|
// Reset the session ID as it's invalid
|
||||||
|
this.sessionID = null;
|
||||||
|
// Set the status to reconnecting
|
||||||
|
this.status = Status.RECONNECTING;
|
||||||
|
// Finally, emit the INVALID_SESSION event
|
||||||
|
this.emit(ShardEvents.INVALID_SESSION);
|
||||||
break;
|
break;
|
||||||
case OPCodes.HEARTBEAT_ACK:
|
case OPCodes.HEARTBEAT_ACK:
|
||||||
this.ackHeartbeat();
|
this.ackHeartbeat();
|
||||||
@@ -329,10 +400,78 @@ class WebSocketShard extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies the client on a connection.
|
* Sets the HELLO packet timeout.
|
||||||
* @returns {void}
|
* @param {number} [time] If set to -1, it will clear the hello timeout timeout
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
setHelloTimeout(time) {
|
||||||
|
if (time === -1) {
|
||||||
|
if (this.helloTimeout) {
|
||||||
|
this.debug('Clearing the HELLO timeout.');
|
||||||
|
this.manager.client.clearTimeout(this.helloTimeout);
|
||||||
|
this.helloTimeout = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.debug('Setting a HELLO timeout for 20s.');
|
||||||
|
this.helloTimeout = this.manager.client.setTimeout(() => {
|
||||||
|
this.debug('Did not receive HELLO in time. Destroying and connecting again.');
|
||||||
|
this.destroy(4009);
|
||||||
|
}, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the heartbeat timer for this shard.
|
||||||
|
* @param {number} time If -1, clears the interval, any other number sets an interval
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
setHeartbeatTimer(time) {
|
||||||
|
if (time === -1) {
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
this.debug('Clearing the heartbeat interval.');
|
||||||
|
this.manager.client.clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.debug(`Setting a heartbeat interval for ${time}ms.`);
|
||||||
|
this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a heartbeat to the WebSocket.
|
||||||
|
* If this shard didn't receive a heartbeat last time, it will destroy it and reconnect
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
sendHeartbeat() {
|
||||||
|
if (!this.lastHeartbeatAcked) {
|
||||||
|
this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting.");
|
||||||
|
this.destroy(4009);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.debug('Sending a heartbeat.');
|
||||||
|
this.lastHeartbeatAcked = false;
|
||||||
|
this.lastPingTimestamp = Date.now();
|
||||||
|
this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledges a heartbeat.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ackHeartbeat() {
|
||||||
|
this.lastHeartbeatAcked = true;
|
||||||
|
const latency = Date.now() - this.lastPingTimestamp;
|
||||||
|
this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`);
|
||||||
|
this.pings.unshift(latency);
|
||||||
|
if (this.pings.length > 3) this.pings.length = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies the client on the connection.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
identify() {
|
identify() {
|
||||||
return this.sessionID ? this.identifyResume() : this.identifyNew();
|
return this.sessionID ? this.identifyResume() : this.identifyNew();
|
||||||
}
|
}
|
||||||
@@ -342,31 +481,32 @@ class WebSocketShard extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
identifyNew() {
|
identifyNew() {
|
||||||
if (!this.manager.client.token) {
|
const { client } = this.manager;
|
||||||
this.debug('No token available to identify a new session with');
|
if (!client.token) {
|
||||||
|
this.debug('No token available to identify a new session.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Clone the generic payload and assign the token
|
|
||||||
|
// Clone the identify payload and assign the token and shard info
|
||||||
const d = {
|
const d = {
|
||||||
...this.manager.client.options.ws,
|
...client.options.ws,
|
||||||
token: this.manager.client.token,
|
token: client.token,
|
||||||
shard: [this.id, Number(this.manager.client.options.totalShardCount)],
|
shard: [this.id, Number(client.options.totalShardCount)],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the payload
|
this.debug(`Identifying as a new session. Shard ${this.id}/${client.options.totalShardCount}`);
|
||||||
this.debug('Identifying as a new session');
|
this.send({ op: OPCodes.IDENTIFY, d }, true);
|
||||||
this.send({ op: OPCodes.IDENTIFY, d });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resumes a session on the gateway.
|
* Resumes a session on the gateway.
|
||||||
* @returns {void}
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
identifyResume() {
|
identifyResume() {
|
||||||
if (!this.sessionID) {
|
if (!this.sessionID) {
|
||||||
this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead');
|
this.debug('Warning: attempted to resume but no session ID was present; identifying as a new session.');
|
||||||
return this.identifyNew();
|
this.identifyNew();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`);
|
this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`);
|
||||||
@@ -377,85 +517,19 @@ class WebSocketShard extends EventEmitter {
|
|||||||
seq: this.closeSequence,
|
seq: this.closeSequence,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.send({ op: OPCodes.RESUME, d });
|
this.send({ op: OPCodes.RESUME, d }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called whenever an error occurs with the WebSocket.
|
* Adds a packet to the queue to be sent to the gateway.
|
||||||
* @param {Error} error The error that occurred
|
* <warn>If you use this method, make sure you understand that you need to provide
|
||||||
* @private
|
* a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
|
||||||
|
* Do not use this method if you don't know what you're doing.</warn>
|
||||||
|
* @param {Object} data The full packet to send
|
||||||
|
* @param {?boolean} [important=false] If this packet should be added first in queue
|
||||||
*/
|
*/
|
||||||
onError(error) {
|
send(data, important = false) {
|
||||||
if (error && error.message === 'uWs client connection error') {
|
this.ratelimit.queue[important ? 'unshift' : 'push'](data);
|
||||||
this.connection.close(4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted whenever the client's WebSocket encounters a connection error.
|
|
||||||
* @event Client#error
|
|
||||||
* @param {Error} error The encountered error
|
|
||||||
* @param {number} shardID The shard that encountered this error
|
|
||||||
*/
|
|
||||||
this.manager.client.emit(Events.ERROR, error, this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @external CloseEvent
|
|
||||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called whenever a connection to the gateway is closed.
|
|
||||||
* @param {CloseEvent} event Close event that was received
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
onClose(event) {
|
|
||||||
this.closeSequence = this.sequence;
|
|
||||||
this.debug(`WebSocket was closed.
|
|
||||||
Event Code: ${event.code}
|
|
||||||
Reason: ${event.reason}`);
|
|
||||||
|
|
||||||
if (event.code === 1000 ? this.manager.expectingClose : WSCodes[event.code]) {
|
|
||||||
/**
|
|
||||||
* Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect.
|
|
||||||
* @event Client#disconnect
|
|
||||||
* @param {CloseEvent} event The WebSocket close event
|
|
||||||
* @param {number} shardID The shard that disconnected
|
|
||||||
*/
|
|
||||||
this.manager.client.emit(Events.DISCONNECT, event, this.id);
|
|
||||||
this.debug(WSCodes[event.code]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.destroy();
|
|
||||||
|
|
||||||
this.status = Status.RECONNECTING;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted whenever a shard tries to reconnect to the WebSocket.
|
|
||||||
* @event Client#reconnecting
|
|
||||||
* @param {number} shardID The shard ID that is reconnecting
|
|
||||||
*/
|
|
||||||
this.manager.client.emit(Events.RECONNECTING, this.id);
|
|
||||||
|
|
||||||
this.debug(`${this.sessionID ? `Reconnecting in 3500ms` : 'Queueing a reconnect'} to the gateway...`);
|
|
||||||
|
|
||||||
if (this.sessionID) {
|
|
||||||
Util.delayFor(3500).then(() => this.connect());
|
|
||||||
} else {
|
|
||||||
this.manager.reconnect(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds data to the queue to be sent.
|
|
||||||
* @param {Object} data Packet to send
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
send(data) {
|
|
||||||
this.ratelimit.queue.push(data);
|
|
||||||
this.processQueue();
|
this.processQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +546,7 @@ class WebSocketShard extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.connection.send(WebSocket.pack(data), err => {
|
this.connection.send(WebSocket.pack(data), err => {
|
||||||
if (err) this.manager.client.emit(Events.ERROR, err);
|
if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,21 +573,36 @@ class WebSocketShard extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys this shard and closes its connection.
|
* Destroys this shard and closes its WebSocket connection.
|
||||||
|
* @param {?number} [closeCode=1000] The close code to use
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy(closeCode = 1000) {
|
||||||
this.setHeartbeatTimer(-1);
|
this.setHeartbeatTimer(-1);
|
||||||
if (this.connection) this.connection.close(1000);
|
this.setHelloTimeout(-1);
|
||||||
|
// Close the WebSocket connection, if any
|
||||||
|
if (this.connection) {
|
||||||
|
this.connection.close(closeCode);
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Emitted when a shard is destroyed, but no WebSocket connection was present.
|
||||||
|
* @private
|
||||||
|
* @event WebSocketShard#destroyed
|
||||||
|
*/
|
||||||
|
this.emit(ShardEvents.DESTROYED);
|
||||||
|
}
|
||||||
this.connection = null;
|
this.connection = null;
|
||||||
|
// Set the shard status
|
||||||
this.status = Status.DISCONNECTED;
|
this.status = Status.DISCONNECTED;
|
||||||
|
// Reset the sequence
|
||||||
|
this.sequence = -1;
|
||||||
|
// Reset the ratelimit data
|
||||||
this.ratelimit.remaining = this.ratelimit.total;
|
this.ratelimit.remaining = this.ratelimit.total;
|
||||||
this.ratelimit.queue.length = 0;
|
this.ratelimit.queue.length = 0;
|
||||||
if (this.ratelimit.timer) {
|
if (this.ratelimit.timer) {
|
||||||
this.manager.client.clearTimeout(this.ratelimit.timer);
|
this.manager.client.clearTimeout(this.ratelimit.timer);
|
||||||
this.ratelimit.timer = null;
|
this.ratelimit.timer = null;
|
||||||
}
|
}
|
||||||
this.sequence = -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ const { register } = require('./DJSError');
|
|||||||
|
|
||||||
const Messages = {
|
const Messages = {
|
||||||
CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`,
|
CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`,
|
||||||
|
CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.',
|
||||||
|
|
||||||
TOKEN_INVALID: 'An invalid token was provided.',
|
TOKEN_INVALID: 'An invalid token was provided.',
|
||||||
TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.',
|
TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.',
|
||||||
|
|
||||||
WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.',
|
WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.',
|
||||||
WS_CONNECTION_TIMEOUT: 'The connection to the gateway timed out.',
|
|
||||||
WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.',
|
WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.',
|
||||||
WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`,
|
WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`,
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* @extends Error
|
* @extends Error
|
||||||
*/
|
*/
|
||||||
class DiscordAPIError extends Error {
|
class DiscordAPIError extends Error {
|
||||||
constructor(path, error, method) {
|
constructor(path, error, method, status) {
|
||||||
super();
|
super();
|
||||||
const flattened = this.constructor.flattenErrors(error.errors || error).join('\n');
|
const flattened = this.constructor.flattenErrors(error.errors || error).join('\n');
|
||||||
this.name = 'DiscordAPIError';
|
this.name = 'DiscordAPIError';
|
||||||
@@ -28,6 +28,12 @@ class DiscordAPIError extends Error {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.code = error.code;
|
this.code = error.code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP status code
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.httpStatus = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class RequestHandler {
|
|||||||
try {
|
try {
|
||||||
const data = await parseResponse(res);
|
const data = await parseResponse(res);
|
||||||
if (res.status >= 400 && res.status < 500) {
|
if (res.status >= 400 && res.status < 500) {
|
||||||
return reject(new DiscordAPIError(request.path, data, request.method));
|
return reject(new DiscordAPIError(request.path, data, request.method, res.status));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -212,8 +212,7 @@ exports.VoiceOPCodes = {
|
|||||||
|
|
||||||
exports.Events = {
|
exports.Events = {
|
||||||
RATE_LIMIT: 'rateLimit',
|
RATE_LIMIT: 'rateLimit',
|
||||||
READY: 'ready',
|
CLIENT_READY: 'ready',
|
||||||
RESUMED: 'resumed',
|
|
||||||
GUILD_CREATE: 'guildCreate',
|
GUILD_CREATE: 'guildCreate',
|
||||||
GUILD_DELETE: 'guildDelete',
|
GUILD_DELETE: 'guildDelete',
|
||||||
GUILD_UPDATE: 'guildUpdate',
|
GUILD_UPDATE: 'guildUpdate',
|
||||||
@@ -246,8 +245,6 @@ exports.Events = {
|
|||||||
MESSAGE_REACTION_REMOVE: 'messageReactionRemove',
|
MESSAGE_REACTION_REMOVE: 'messageReactionRemove',
|
||||||
MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll',
|
MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll',
|
||||||
USER_UPDATE: 'userUpdate',
|
USER_UPDATE: 'userUpdate',
|
||||||
USER_NOTE_UPDATE: 'userNoteUpdate',
|
|
||||||
USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate',
|
|
||||||
PRESENCE_UPDATE: 'presenceUpdate',
|
PRESENCE_UPDATE: 'presenceUpdate',
|
||||||
VOICE_SERVER_UPDATE: 'voiceServerUpdate',
|
VOICE_SERVER_UPDATE: 'voiceServerUpdate',
|
||||||
VOICE_STATE_UPDATE: 'voiceStateUpdate',
|
VOICE_STATE_UPDATE: 'voiceStateUpdate',
|
||||||
@@ -256,16 +253,26 @@ exports.Events = {
|
|||||||
TYPING_START: 'typingStart',
|
TYPING_START: 'typingStart',
|
||||||
TYPING_STOP: 'typingStop',
|
TYPING_STOP: 'typingStop',
|
||||||
WEBHOOKS_UPDATE: 'webhookUpdate',
|
WEBHOOKS_UPDATE: 'webhookUpdate',
|
||||||
DISCONNECT: 'disconnect',
|
|
||||||
RECONNECTING: 'reconnecting',
|
|
||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
WARN: 'warn',
|
WARN: 'warn',
|
||||||
DEBUG: 'debug',
|
DEBUG: 'debug',
|
||||||
|
SHARD_DISCONNECTED: 'shardDisconnected',
|
||||||
|
SHARD_ERROR: 'shardError',
|
||||||
|
SHARD_RECONNECTING: 'shardReconnecting',
|
||||||
SHARD_READY: 'shardReady',
|
SHARD_READY: 'shardReady',
|
||||||
|
SHARD_RESUMED: 'shardResumed',
|
||||||
INVALIDATED: 'invalidated',
|
INVALIDATED: 'invalidated',
|
||||||
RAW: 'raw',
|
RAW: 'raw',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.ShardEvents = {
|
||||||
|
CLOSE: 'close',
|
||||||
|
DESTROYED: 'destroyed',
|
||||||
|
INVALID_SESSION: 'invalidSession',
|
||||||
|
READY: 'ready',
|
||||||
|
RESUMED: 'resumed',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of Structure allowed to be a partial:
|
* The type of Structure allowed to be a partial:
|
||||||
* * USER
|
* * USER
|
||||||
@@ -312,7 +319,6 @@ exports.PartialTypes = keyMirror([
|
|||||||
* * MESSAGE_REACTION_REMOVE
|
* * MESSAGE_REACTION_REMOVE
|
||||||
* * MESSAGE_REACTION_REMOVE_ALL
|
* * MESSAGE_REACTION_REMOVE_ALL
|
||||||
* * USER_UPDATE
|
* * USER_UPDATE
|
||||||
* * USER_NOTE_UPDATE
|
|
||||||
* * USER_SETTINGS_UPDATE
|
* * USER_SETTINGS_UPDATE
|
||||||
* * PRESENCE_UPDATE
|
* * PRESENCE_UPDATE
|
||||||
* * VOICE_STATE_UPDATE
|
* * VOICE_STATE_UPDATE
|
||||||
|
|||||||
97
typings/index.d.ts
vendored
97
typings/index.d.ts
vendored
@@ -2,6 +2,7 @@ declare module 'discord.js' {
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Stream, Readable, Writable } from 'stream';
|
import { Stream, Readable, Writable } from 'stream';
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess } from 'child_process';
|
||||||
|
import * as WebSocket from 'ws';
|
||||||
|
|
||||||
export const version: string;
|
export const version: string;
|
||||||
|
|
||||||
@@ -181,15 +182,18 @@ declare module 'discord.js' {
|
|||||||
public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this;
|
public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this;
|
||||||
public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this;
|
public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this;
|
||||||
public on(event: 'ready', listener: () => void): this;
|
public on(event: 'ready', listener: () => void): this;
|
||||||
public on(event: 'reconnecting', listener: (shardID: number) => void): this;
|
|
||||||
public on(event: 'resumed', listener: (replayed: number, shardID: number) => void): this;
|
|
||||||
public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this;
|
public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this;
|
||||||
public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this;
|
public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this;
|
||||||
public on(event: 'shardReady', listener: (shardID: number) => void): this;
|
|
||||||
public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this;
|
public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this;
|
||||||
public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this;
|
public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this;
|
||||||
public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this;
|
public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this;
|
||||||
public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this;
|
public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this;
|
||||||
|
public on(event: 'invalidated', listener: () => void): this;
|
||||||
|
public on(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this;
|
||||||
|
public on(event: 'shardError', listener: (error: Error, id: number) => void): this;
|
||||||
|
public on(event: 'shardReconnecting', listener: (id: number) => void): this;
|
||||||
|
public on(event: 'shardReady', listener: (id: number) => void): this;
|
||||||
|
public on(event: 'shardResumed', listener: (id: number) => void): this;
|
||||||
public on(event: string, listener: Function): this;
|
public on(event: string, listener: Function): this;
|
||||||
|
|
||||||
public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this;
|
public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this;
|
||||||
@@ -215,15 +219,18 @@ declare module 'discord.js' {
|
|||||||
public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this;
|
public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this;
|
||||||
public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this;
|
public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this;
|
||||||
public once(event: 'ready', listener: () => void): this;
|
public once(event: 'ready', listener: () => void): this;
|
||||||
public once(event: 'reconnecting', listener: (shardID: number) => void): this;
|
|
||||||
public once(event: 'resumed', listener: (replayed: number, shardID: number) => void): this;
|
|
||||||
public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this;
|
public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this;
|
||||||
public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this;
|
public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this;
|
||||||
public once(event: 'shardReady', listener: (shardID: number) => void): this;
|
|
||||||
public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this;
|
public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this;
|
||||||
public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this;
|
public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this;
|
||||||
public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this;
|
public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this;
|
||||||
public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this;
|
public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this;
|
||||||
|
public once(event: 'invalidated', listener: () => void): this;
|
||||||
|
public once(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this;
|
||||||
|
public once(event: 'shardError', listener: (error: Error, id: number) => void): this;
|
||||||
|
public once(event: 'shardReconnecting', listener: (id: number) => void): this;
|
||||||
|
public once(event: 'shardReady', listener: (id: number) => void): this;
|
||||||
|
public once(event: 'shardResumed', listener: (id: number) => void): this;
|
||||||
public once(event: string, listener: Function): this;
|
public once(event: string, listener: Function): this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,12 +347,13 @@ declare module 'discord.js' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DiscordAPIError extends Error {
|
export class DiscordAPIError extends Error {
|
||||||
constructor(path: string, error: object, method: string);
|
constructor(path: string, error: object, method: string, httpStatus: number);
|
||||||
private static flattenErrors(obj: object, key: string): string[];
|
private static flattenErrors(obj: object, key: string): string[];
|
||||||
|
|
||||||
public code: number;
|
public code: number;
|
||||||
public method: string;
|
public method: string;
|
||||||
public path: string;
|
public path: string;
|
||||||
|
public httpStatus: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DMChannel extends TextBasedChannel(Channel) {
|
export class DMChannel extends TextBasedChannel(Channel) {
|
||||||
@@ -1270,27 +1278,80 @@ declare module 'discord.js' {
|
|||||||
|
|
||||||
export class WebSocketManager {
|
export class WebSocketManager {
|
||||||
constructor(client: Client);
|
constructor(client: Client);
|
||||||
|
private totalShards: number | string;
|
||||||
|
private shardQueue: Set<WebSocketShard>;
|
||||||
|
private packetQueue: object[];
|
||||||
|
private destroyed: boolean;
|
||||||
|
private reconnecting: boolean;
|
||||||
|
private sessionStartLimit?: { total: number; remaining: number; reset_after: number; };
|
||||||
|
|
||||||
public readonly client: Client;
|
public readonly client: Client;
|
||||||
public gateway: string | undefined;
|
public gateway?: string;
|
||||||
public readonly ping: number;
|
|
||||||
public shards: Collection<number, WebSocketShard>;
|
public shards: Collection<number, WebSocketShard>;
|
||||||
public status: Status;
|
public status: Status;
|
||||||
|
public readonly ping: number;
|
||||||
|
|
||||||
public broadcast(packet: object): void;
|
private debug(message: string, shard?: WebSocketShard): void;
|
||||||
|
private connect(): Promise<void>;
|
||||||
|
private createShards(): Promise<void>;
|
||||||
|
private reconnect(): Promise<void>;
|
||||||
|
private broadcast(packet: object): void;
|
||||||
|
private destroy(): void;
|
||||||
|
private _handleSessionLimit(remaining?: number, resetAfter?: number): Promise<void>;
|
||||||
|
private handlePacket(packet?: object, shard?: WebSocketShard): Promise<boolean>;
|
||||||
|
private checkReady(): boolean;
|
||||||
|
private triggerReady(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebSocketShard extends EventEmitter {
|
export class WebSocketShard extends EventEmitter {
|
||||||
constructor(manager: WebSocketManager, id: number);
|
constructor(manager: WebSocketManager, id: number);
|
||||||
public id: number;
|
private sequence: number;
|
||||||
public readonly ping: number;
|
private closeSequence: number;
|
||||||
public pings: number[];
|
private sessionID?: string;
|
||||||
public status: Status;
|
private lastPingTimestamp: number;
|
||||||
|
private lastHeartbeatAcked: boolean;
|
||||||
|
private trace: string[];
|
||||||
|
private ratelimit: { queue: object[]; total: number; remaining: number; time: 60e3; timer: NodeJS.Timeout | null; };
|
||||||
|
private connection: WebSocket | null;
|
||||||
|
private helloTimeout: NodeJS.Timeout | null;
|
||||||
|
private eventsAttached: boolean;
|
||||||
|
|
||||||
public manager: WebSocketManager;
|
public manager: WebSocketManager;
|
||||||
|
public id: number;
|
||||||
|
public status: Status;
|
||||||
|
public pings: [number, number, number];
|
||||||
|
public readonly ping: number;
|
||||||
|
|
||||||
public send(packet: object): void;
|
private debug(message: string): void;
|
||||||
|
private connect(): Promise<void>;
|
||||||
|
private onOpen(): void;
|
||||||
|
private onMessage(event: MessageEvent): void;
|
||||||
|
private onError(error: ErrorEvent): void;
|
||||||
|
private onClose(event: CloseEvent): void;
|
||||||
|
private onPacket(packet: object): void;
|
||||||
|
private setHelloTimeout(time?: number): void;
|
||||||
|
private setHeartbeatTimer(time: number): void;
|
||||||
|
private sendHeartbeat(): void;
|
||||||
|
private ackHeartbeat(): void;
|
||||||
|
private identify(): void;
|
||||||
|
private identifyNew(): void;
|
||||||
|
private identifyResume(): void;
|
||||||
|
private _send(data: object): void;
|
||||||
|
private processQueue(): void;
|
||||||
|
private destroy(closeCode: number): void;
|
||||||
|
|
||||||
|
public send(data: object): void;
|
||||||
public on(event: 'ready', listener: () => void): this;
|
public on(event: 'ready', listener: () => void): this;
|
||||||
|
public on(event: 'resumed', listener: () => void): this;
|
||||||
|
public on(event: 'close', listener: (event: CloseEvent) => void): this;
|
||||||
|
public on(event: 'invalidSession', listener: () => void): this;
|
||||||
|
public on(event: string, listener: Function): this;
|
||||||
|
|
||||||
public once(event: 'ready', listener: () => void): this;
|
public once(event: 'ready', listener: () => void): this;
|
||||||
|
public once(event: 'resumed', listener: () => void): this;
|
||||||
|
public once(event: 'close', listener: (event: CloseEvent) => void): this;
|
||||||
|
public once(event: 'invalidSession', listener: () => void): this;
|
||||||
|
public once(event: string, listener: Function): this;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -1589,7 +1650,7 @@ declare module 'discord.js' {
|
|||||||
|
|
||||||
interface ClientOptions {
|
interface ClientOptions {
|
||||||
shards?: number | number[];
|
shards?: number | number[];
|
||||||
shardCount?: number;
|
shardCount?: number | 'auto';
|
||||||
totalShardCount?: number;
|
totalShardCount?: number;
|
||||||
messageCacheMaxSize?: number;
|
messageCacheMaxSize?: number;
|
||||||
messageCacheLifetime?: number;
|
messageCacheLifetime?: number;
|
||||||
@@ -2149,5 +2210,9 @@ declare module 'discord.js' {
|
|||||||
| 'VOICE_SERVER_UPDATE'
|
| 'VOICE_SERVER_UPDATE'
|
||||||
| 'WEBHOOKS_UPDATE';
|
| 'WEBHOOKS_UPDATE';
|
||||||
|
|
||||||
|
type MessageEvent = { data: WebSocket.Data; type: string; target: WebSocket; };
|
||||||
|
type CloseEvent = { wasClean: boolean; code: number; reason: string; target: WebSocket; };
|
||||||
|
type ErrorEvent = { error: any, message: string, type: string, target: WebSocket; };
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user