1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
const path = require('path');
const fs = require('fs');
const EventEmitter = require('events').EventEmitter;
const Shard = require('./Shard');
const Collection = require('../util/Collection');
const Util = require('../util/Util');
/**
* This is a utility class that can be used to help you spawn shards of your client. Each shard is completely separate
* from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
* If you do not select an amount of shards, the manager will automatically decide the best amount.
* @extends {EventEmitter}
*/
class ShardingManager extends EventEmitter {
/**
* @param {string} file Path to your shard script file
* @param {Object} [options] Options for the sharding manager
* @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto"
* @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting
* @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning
* @param {string} [options.token] Token to use for automatic shard count and passing to shards
*/
constructor(file, options = {}) {
super();
options = Util.mergeDefault({
totalShards: 'auto',
respawn: true,
shardArgs: [],
token: null,
}, options);
/**
* Path to the shard script file
* @type {string}
*/
this.file = file;
if (!file) throw new Error('File must be specified.');
if (!path.isAbsolute(file)) this.file = path.resolve(process.cwd(), file);
const stats = fs.statSync(this.file);
if (!stats.isFile()) throw new Error('File path does not point to a file.');
/**
* Amount of shards that this manager is going to spawn
* @type {number|string}
*/
this.totalShards = options.totalShards;
if (this.totalShards !== 'auto') {
if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
throw new TypeError('Amount of shards must be a number.');
}
if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
if (this.totalShards !== Math.floor(this.totalShards)) {
throw new RangeError('Amount of shards must be an integer.');
}
}
/**
* Whether shards should automatically respawn upon exiting
* @type {boolean}
*/
this.respawn = options.respawn;
/**
* An array of arguments to pass to shards
* @type {string[]}
*/
this.shardArgs = options.shardArgs;
/**
* Arguments for the shard's process executable
* @type {?string[]}
*/
this.execArgv = options.execArgv;
/**
* Token to use for obtaining the automatic shard count, and passing to shards
* @type {?string}
*/
this.token = options.token ? options.token.replace(/^Bot\s*/i, '') : null;
/**
* A collection of shards that this manager has spawned
* @type {Collection<number, Shard>}
*/
this.shards = new Collection();
}
/**
* Spawns a single shard.
* @param {number} id The ID of the shard to spawn. **This is usually not necessary**
* @returns {Promise<Shard>}
*/
createShard(id = this.shards.size) {
const shard = new Shard(this, id, this.shardArgs);
this.shards.set(id, shard);
/**
* Emitted upon launching a shard.
* @event ShardingManager#launch
* @param {Shard} shard Shard that was launched
*/
this.emit('launch', shard);
return Promise.resolve(shard);
}
/**
* Spawns multiple shards.
* @param {number} [amount=this.totalShards] Number of shards to spawn
* @param {number} [delay=7500] How long to wait in between spawning each shard (in milliseconds)
* @returns {Promise<Collection<number, Shard>>}
*/
spawn(amount = this.totalShards, delay = 7500) {
if (amount === 'auto') {
return Util.fetchRecommendedShards(this.token).then(count => {
this.totalShards = count;
return this._spawn(count, delay);
});
} else {
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
return this._spawn(amount, delay);
}
}
/**
* Actually spawns shards, unlike that poser above >:(
* @param {number} amount Number of shards to spawn
* @param {number} delay How long to wait in between spawning each shard (in milliseconds)
* @returns {Promise<Collection<number, Shard>>}
* @private
*/
_spawn(amount, delay) {
return new Promise(resolve => {
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
this.totalShards = amount;
this.createShard();
if (this.shards.size >= this.totalShards) {
resolve(this.shards);
return;
}
if (delay <= 0) {
while (this.shards.size < this.totalShards) this.createShard();
resolve(this.shards);
} else {
const interval = setInterval(() => {
this.createShard();
if (this.shards.size >= this.totalShards) {
clearInterval(interval);
resolve(this.shards);
}
}, delay);
}
});
}
/**
* Send a message to all shards.
* @param {*} message Message to be sent to the shards
* @returns {Promise<Shard[]>}
*/
broadcast(message) {
const promises = [];
for (const shard of this.shards.values()) promises.push(shard.send(message));
return Promise.all(promises);
}
/**
* Evaluates a script on all shards, in the context of the Clients.
* @param {string} script JavaScript to run on each shard
* @returns {Promise<Array>} Results of the script execution
*/
broadcastEval(script) {
const promises = [];
for (const shard of this.shards.values()) promises.push(shard.eval(script));
return Promise.all(promises);
}
/**
* Fetches a client property value of each shard.
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<Array>}
* @example
* manager.fetchClientValues('guilds.size')
* .then(results => {
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
* })
* .catch(console.error);
*/
fetchClientValues(prop) {
if (this.shards.size === 0) return Promise.reject(new Error('No shards have been spawned.'));
if (this.shards.size !== this.totalShards) return Promise.reject(new Error('Still spawning shards.'));
const promises = [];
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
return Promise.all(promises);
}
/**
* 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
* @param {number} [currentShardIndex=0] The shard index to start respawning at
* @returns {Promise<Collection<number, Shard>>}
*/
respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true, currentShardIndex = 0) {
let s = 0;
const shard = this.shards.get(currentShardIndex);
const promises = [shard.respawn(respawnDelay, waitForReady)];
if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay));
return Promise.all(promises).then(() => {
if (++currentShardIndex === this.shards.size) return this.shards;
return this.respawnAll(shardDelay, respawnDelay, waitForReady, currentShardIndex);
});
}
}
module.exports = ShardingManager;
|