UNPKG

status-sharding

Version:

Welcome to Status Sharding! This package is designed to provide an efficient and flexible solution for sharding Discord bots, allowing you to scale your bot across multiple processes or workers.

282 lines (281 loc) 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClusterClient = void 0; const types_1 = require("../types"); const message_1 = require("../other/message"); const broker_1 = require("../handlers/broker"); const utils_1 = require("../other/utils"); const message_2 = require("../handlers/message"); const shardingUtils_1 = require("../other/shardingUtils"); const promise_1 = require("../handlers/promise"); const worker_1 = require("../classes/worker"); const child_1 = require("../classes/child"); const events_1 = __importDefault(require("events")); /** Simplified Cluster instance available on the {@link ClusterClient}. */ class ClusterClient extends events_1.default { client; /** Ready state of the cluster. */ ready; /** Handler that resolves sent messages and requests. */ promise; /** Client that manages broker tunnels. */ broker; // IPC Broker for the ClusterManager. /** Client that manages the cluster process. */ process; /** Handler that handles messages from the ClusterManager and the Cluster. */ messageHandler; /** Package type. */ packageType; /** Creates an instance of ClusterClient. */ constructor(client) { super(); this.client = client; this.ready = false; this.packageType = (0, utils_1.detectLibraryFromClient)(client); this.broker = new broker_1.IPCBrokerClient(this); this.process = (this.info.ClusterManagerMode === 'process' ? new child_1.ChildClient() : this.info.ClusterManagerMode === 'worker' ? new worker_1.WorkerClient() : null); this.messageHandler = new message_2.ClusterClientHandler(this); // Handle messages from the ClusterManager. if (!this.process?.ipc) throw new Error('CLUSTERING_NO_PROCESS | No process to handle messages from.'); this.process.ipc.on('message', this._handleMessage.bind(this)); this.promise = new promise_1.PromiseHandler(this); } /** Current cluster id. */ get id() { return this.info.ClusterId; } /** Total number of shards. */ get totalShards() { return this.info.TotalShards; } /** Total number of clusters. */ get totalClusters() { return this.info.ClusterCount; } /** Utility function to get some info about the cluster. */ get info() { return (0, utils_1.getInfo)(); } /** Sends a message to the Cluster as child. (goes to Cluster on _handleMessage). */ send(message) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#1).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#1).')); this.emit('debug', `[IPC] [Child ${this.id}] Sending message to cluster.`); return this.process.send({ data: message, _type: types_1.MessageTypes.CustomMessage, }); } /** Broadcasts a message to all clusters. */ broadcast(message, sendSelf = false) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#2).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#2).')); this.emit('debug', `[IPC] [Child ${this.id}] Sending message to cluster.`); return this.process.send({ data: { message, ignore: sendSelf ? undefined : this.id, }, _type: types_1.MessageTypes.ClientBroadcast, }); } /** Sends a message to the Cluster. */ _sendInstance(message) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#3).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#3).')); else if (!('_type' in message) || !('data' in message)) return Promise.reject(new Error('CLUSTERING_INVALID_MESSAGE | Invalid message object.' + JSON.stringify(message))); this.emit('debug', `[IPC] [Child ${this.id}] Sending message to cluster.`); return this.process.send(message); } /** Evaluates a script on the master process, in the context of the {@link ClusterManager}. */ async evalOnManager(script, options) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#4).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#4).')); else if (typeof script !== 'function') return Promise.reject(new Error('CLUSTERING_INVALID_EVAL_SCRIPT | Eval script is not a function (#1).')); const nonce = shardingUtils_1.ShardingUtils.generateNonce(); this.process.send({ data: { options, script: `(${script})(this,${options?.context ? JSON.stringify(options.context) : undefined})`, }, _nonce: nonce, _type: types_1.MessageTypes.ClientManagerEvalRequest, }); return this.promise.create(nonce, options?.timeout); } /** Evaluates a script on all clusters in parallel. */ async broadcastEval(script, options) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#5).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#5).')); else if (typeof script !== 'string' && typeof script !== 'function') return Promise.reject(new Error('CLUSTERING_INVALID_EVAL_SCRIPT | Eval script is not a function or string.')); const nonce = shardingUtils_1.ShardingUtils.generateNonce(); this.process.send({ data: { options, script: typeof script === 'string' ? script : `(${script})(this,${options?.context ? JSON.stringify(options.context) : undefined})`, }, _nonce: nonce, _type: types_1.MessageTypes.ClientBroadcastRequest, }); return this.promise.create(nonce, options?.timeout); } /** Evaluates a script on specific guild. */ async evalOnGuild(guildId, script, options) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#6).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#6).')); else if (typeof script !== 'function') return Promise.reject(new Error('CLUSTERING_INVALID_EVAL_SCRIPT | Eval script is not a function (#2).')); else if (typeof guildId !== 'string') return Promise.reject(new TypeError('CLUSTERING_GUILD_ID_INVALID | Guild Id must be a string.')); else if (this.packageType !== 'discord.js') return Promise.reject(new Error('CLUSTERING_EVAL_GUILD_UNSUPPORTED | evalOnGuild is only supported in discord.js package type.')); const nonce = shardingUtils_1.ShardingUtils.generateNonce(); this.process.send({ _type: types_1.MessageTypes.ClientBroadcastRequest, _nonce: nonce, data: { script: shardingUtils_1.ShardingUtils.parseInput(script, options?.context, this.packageType, `this?.guilds?.cache?.get('${guildId}')`), options: { ...options, guildId }, }, }); return this.promise.create(nonce, options?.timeout).then((data) => data?.find((v) => v !== undefined)); } /** Evaluates a script on a current client, in the context of the {@link ShardingClient}. */ async evalOnClient(script, options) { switch (this.packageType) { case 'discord.js': { const parsedScript = shardingUtils_1.ShardingUtils.parseInput(script, options?.context, this.packageType); if (!this.client._eval) this.client._eval = function (_) { return (0, eval)(_); }.bind(this.client); return await this.client._eval(parsedScript); } case '@discordjs/core': { if (typeof script === 'function') return await script(this.client, options?.context); const fixedScript = script.replace(/\(this,/, '(client,'); const evalFunction = new Function('client', `return (${fixedScript})`); return await evalFunction(this.client, options?.context); } default: { return Promise.reject(new Error('CLUSTERING_EVAL_CLIENT_UNSUPPORTED | evalOnClient is only supported in discord.js and @discordjs/core package types.')); } } } /** Sends a request to the Cluster (cluster has to respond with a reply (cluster.on('message', (message) => message.reply('reply')))). */ request(message, options = {}) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#7).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#7).')); this.emit('debug', `[IPC] [Child ${this.id}] Sending message to cluster.`); const nonce = shardingUtils_1.ShardingUtils.generateNonce(); this.process.send({ _type: types_1.MessageTypes.CustomRequest, _nonce: nonce, data: message, }); return this.promise.create(nonce, options.timeout); } /** Kills all running clusters and respawns them. */ respawnAll(clusterDelay = 8000, respawnDelay = 5500, timeout = -1, except = []) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#8).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#8).')); this.emit('debug', `[IPC] [Child ${this.id}] Sending message to cluster.`); return this.process.send({ _type: types_1.MessageTypes.ClientRespawnAll, data: { clusterDelay, respawnDelay, timeout, except, }, }); } /** Kills specific clusters and respawns them. */ async respawnClusters(clusters, clusterDelay = 8000, respawnDelay = 5500, timeout = -1) { if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#8).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#8).')); this.emit('debug', `[IPC] [Child ${this.id}] Sending message to cluster.`); return this.process.send({ _type: types_1.MessageTypes.ClientRespawnSpecific, data: { clusterIds: clusters, clusterDelay, respawnDelay, timeout, }, }); } /** Handles a message from the ClusterManager. */ _handleMessage(message) { if (!message || '_data' in message) return this.broker.handleMessage(message); // Debug. this.emit('debug', `[IPC] [Child ${this.id}] Received message from cluster.`); this.messageHandler.handleMessage(message); // Emitted upon receiving a message from the child process/worker. if ([types_1.MessageTypes.CustomMessage, types_1.MessageTypes.CustomRequest].includes(message._type)) { this.emit('message', new message_1.ProcessMessage(this, message)); } } /** Sends a message to the master process. */ _respond(message) { if (!this.process) throw new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#9).'); this.process.send(message); } /** Triggers the ready event, do not use this unless you know what you are doing. */ triggerReady() { if (this.ready) return this.ready; else if (!this.process) throw new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#10).'); this.ready = true; this.process.send({ _type: types_1.MessageTypes.ClientReady, data: { packageType: this.packageType }, }); this.emit('ready', this); return this.ready; } /** Spawns the next cluster, when queue mode is on 'manual'. */ spawnNextCluster() { if (this.info.ClusterQueueMode === 'auto') throw new Error('Next Cluster can just be spawned when the queue is not on auto mode.'); else if (!this.process) return Promise.reject(new Error('CLUSTERING_NO_PROCESS_TO_SEND_TO | No process to send the message to (#12).')); else if (!this.ready) return Promise.reject(new Error('CLUSTERING_NOT_READY | Cluster is not ready yet (#9).')); return this.process.send({ _type: types_1.MessageTypes.ClientSpawnNextCluster, }); } /** Kills the cluster. */ _debug(message) { this.emit('debug', message); } } exports.ClusterClient = ClusterClient;