forked from Galactic/galactic-bot
275 lines
8.7 KiB
TypeScript
275 lines
8.7 KiB
TypeScript
/* Adopted from Discord.js v13 */
|
|
|
|
// const { EventEmitter } = require('events');
|
|
// const { Collection } = require('@discordjs/collection');
|
|
// const { Util } = require('../../utilities');
|
|
// const fs = require('fs');
|
|
// const path = require('path');
|
|
|
|
// const Shard = require('./Shard.js');
|
|
|
|
import { EventEmitter } from 'node:events';
|
|
import { Collection } from '@discordjs/collection';
|
|
import { Util } from '../../utilities/index.js';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import Shard from './Shard.js';
|
|
import { IPCMessage } from '../../@types/Shared.js';
|
|
import { BroadcastEvalOptions, ShardingOptions, ShardMethod } from '../../@types/Shard.js';
|
|
|
|
// NOT USED; SUPERSEDED BY THE CONTROLLER
|
|
class ShardingManager extends EventEmitter
|
|
{
|
|
|
|
#file: string;
|
|
#shardList: 'auto' | number[];
|
|
#totalShards: 'auto' | number;
|
|
#mode: 'worker' | 'process';
|
|
#respawn: boolean;
|
|
#shardArgs: string[];
|
|
#execArgv: string[];
|
|
#token: string | null;
|
|
#shards: Collection<number, Shard>;
|
|
|
|
constructor (file: string, options: ShardingOptions = {})
|
|
{
|
|
super();
|
|
options = Util.mergeDefault(
|
|
{
|
|
totalShards: 'auto',
|
|
mode: 'process',
|
|
respawn: true,
|
|
shardArgs: [],
|
|
execArgv: [],
|
|
token: process.env.DISCORD_TOKEN
|
|
},
|
|
options
|
|
);
|
|
|
|
this.#file = file;
|
|
if (!file)
|
|
throw new Error('[shardmanager] 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('[shardmanager] File path does not point to a valid file.');
|
|
|
|
this.#shardList = options.shardList ?? 'auto';
|
|
if (this.#shardList !== 'auto')
|
|
{
|
|
if (!Array.isArray(this.#shardList))
|
|
{
|
|
throw new TypeError('[shardmanager] ShardList must be an array.');
|
|
}
|
|
this.#shardList = [ ...new Set(this.#shardList) ];
|
|
if (this.#shardList.length < 1)
|
|
throw new RangeError('[shardmanager] ShardList must have one ID.');
|
|
if (this.#shardList.some((shardId) => typeof shardId !== 'number' || isNaN(shardId) || !Number.isInteger(shardId) || shardId < 0))
|
|
{
|
|
throw new TypeError('[shardmanager] ShardList must be an array of positive integers.');
|
|
}
|
|
}
|
|
|
|
this.#totalShards = options.totalShards || 'auto';
|
|
if (this.#totalShards !== 'auto')
|
|
{
|
|
if (typeof this.#totalShards !== 'number' || isNaN(this.#totalShards))
|
|
{
|
|
throw new TypeError('[shardmanager] totalShards must be an integer.');
|
|
}
|
|
if (this.#totalShards < 1)
|
|
throw new RangeError('[shardmanager] totalShards must be at least one.');
|
|
if (!Number.isInteger(this.#totalShards))
|
|
{
|
|
throw new RangeError('[shardmanager] totalShards must be an integer.');
|
|
}
|
|
}
|
|
|
|
this.#mode = options.mode as 'worker' | 'process';
|
|
if (this.#mode !== 'process' && this.#mode !== 'worker')
|
|
{
|
|
throw new RangeError('[shardmanager] Mode must be either \'worker\' or \'process\'.');
|
|
}
|
|
|
|
this.#respawn = options.respawn as boolean;
|
|
this.#shardArgs = options.shardArgs as string[];
|
|
this.#execArgv = options.execArgv as string[];
|
|
|
|
this.#token = options.token?.replace(/^Bot\s*/iu, '') ?? null;
|
|
|
|
this.#shards = new Collection();
|
|
|
|
process.env.SHARDING_MANAGER = true as unknown as string;
|
|
process.env.SHARDING_MANAGER_MODE = this.#mode;
|
|
process.env.DISCORD_TOKEN = this.#token || '';
|
|
}
|
|
|
|
get respawn ()
|
|
{
|
|
return this.#respawn;
|
|
}
|
|
|
|
get shardArgs ()
|
|
{
|
|
return this.#shardArgs;
|
|
}
|
|
|
|
get execArgv ()
|
|
{
|
|
return this.#execArgv;
|
|
}
|
|
|
|
get totalShards ()
|
|
{
|
|
return this.#totalShards as number;
|
|
}
|
|
|
|
get token ()
|
|
{
|
|
return this.#token || '';
|
|
}
|
|
|
|
get mode ()
|
|
{
|
|
return this.#mode;
|
|
}
|
|
|
|
get file ()
|
|
{
|
|
return this.#file;
|
|
}
|
|
|
|
get shards ()
|
|
{
|
|
return this.#shards.clone();
|
|
}
|
|
|
|
createShard (id = this.#shards.size)
|
|
{
|
|
const shard = new Shard(this, id, {
|
|
file: this.file
|
|
});
|
|
this.#shards.set(id, shard);
|
|
this.emit('shardCreate', shard);
|
|
return shard;
|
|
}
|
|
|
|
async spawn ({ amount = this.#totalShards, delay = 5500, timeout = 30000 } = {})
|
|
{
|
|
if (amount === 'auto')
|
|
{
|
|
if (!this.#token)
|
|
throw new Error('[shardmanager] Must supply token for auto shard amount');
|
|
amount = await Util.fetchRecommendedShards(this.#token);
|
|
}
|
|
else
|
|
{
|
|
if (typeof amount !== 'number' || isNaN(amount))
|
|
{
|
|
throw new TypeError('[shardmanager] Amount of shards must be a number.');
|
|
}
|
|
if (amount < 1)
|
|
throw new RangeError('[shardmanager] Amount of shards must be at least one.');
|
|
if (!Number.isInteger(amount))
|
|
{
|
|
throw new TypeError('[shardmanager] Amount of shards must be an integer.');
|
|
}
|
|
}
|
|
|
|
if (this.#shards.size >= amount)
|
|
throw new Error('[shardmanager] Already spawned all necessary shards.');
|
|
if (this.#shardList === 'auto' || this.#totalShards === 'auto' || this.#totalShards !== amount)
|
|
{
|
|
this.#shardList = [ ...Array(amount).keys() ];
|
|
}
|
|
if (this.#totalShards === 'auto' || this.#totalShards !== amount)
|
|
{
|
|
this.#totalShards = amount;
|
|
}
|
|
|
|
if (this.#shardList.some((shardId) => shardId >= (amount as number)))
|
|
{
|
|
throw new RangeError('[shardmanager] Amount of shards cannot be larger than the highest shard ID.');
|
|
}
|
|
|
|
for (const shardId of this.#shardList)
|
|
{
|
|
const shard = this.createShard(shardId);
|
|
await shard.spawn(timeout);
|
|
if (delay > 0 && this.#shards.size !== this.#shardList.length)
|
|
await Util.delayFor(delay);
|
|
}
|
|
|
|
return this.#shards;
|
|
}
|
|
|
|
broadcast (message: IPCMessage)
|
|
{
|
|
const promises = [];
|
|
for (const shard of this.#shards.values())
|
|
promises.push(shard.send(message));
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
broadcastEval (script: string | Function, options: BroadcastEvalOptions = {})
|
|
{
|
|
if (typeof script !== 'function')
|
|
return Promise.reject(new TypeError('[shardmanager] Provided eval must be a function.'));
|
|
return this._performOnShards('eval', [ `(${script})(this, ${JSON.stringify(options.context)})` ], options.shard);
|
|
}
|
|
|
|
fetchClientValues (prop: string, shard: number)
|
|
{
|
|
return this._performOnShards('fetchClientValue', [ prop ], shard);
|
|
}
|
|
|
|
_performOnShards (method: ShardMethod, args: [string, object?], shard?: number): Promise<unknown>
|
|
{
|
|
if (this.#shards.size === 0)
|
|
return Promise.reject(new Error('[shardmanager] No shards available.'));
|
|
|
|
if (typeof shard === 'number')
|
|
{
|
|
if (!this.#shards.has(shard))
|
|
Promise.reject(new Error('[shardmanager] Shard not found.'));
|
|
|
|
const s = this.#shards.get(shard) as Shard;
|
|
if (method === 'eval')
|
|
return s.eval(...args);
|
|
else if (method === 'fetchClientValue')
|
|
return s.eval(args[0]);
|
|
}
|
|
|
|
if (this.#shards.size !== this.#shardList.length)
|
|
return Promise.reject(new Error('[shardmanager] Sharding in progress.'));
|
|
|
|
const promises = [];
|
|
for (const sh of this.#shards.values())
|
|
{
|
|
if (method === 'eval')
|
|
promises.push(sh.eval(...args));
|
|
else if (method === 'fetchClientValue')
|
|
promises.push(sh.eval(args[0]));
|
|
}
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
async respawnAll ({ shardDelay = 5000, respawnDelay = 500, timeout = 30000 } = {})
|
|
{
|
|
let s = 0;
|
|
for (const shard of this.#shards.values())
|
|
{
|
|
const promises: Promise<unknown>[] = [ shard.respawn({ delay: respawnDelay, timeout }) ];
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
export default ShardingManager; |