discord-hybrid-sharding
Version:
The first package which combines sharding manager & internal sharding to save a lot of resources, which allows clustering!
376 lines (333 loc) • 13.2 kB
text/typescript
import { IPCMessage, BaseMessage, RawMessage } from '../Structures/IPCMessage';
import {
Awaitable,
ClusterClientEvents,
DjsDiscordClient,
evalOptions,
Events,
messageType,
Serialized,
} from '../types/shared';
import { ClusterManager as Manager } from '../Core/ClusterManager';
import { getInfo } from '../Structures/Data';
import { WorkerClient } from '../Structures/Worker';
import { ChildClient } from '../Structures/Child';
import { ClusterClientHandler } from '../Structures/IPCHandler';
import { PromiseHandler } from '../Structures/PromiseHandler';
import EventEmitter from 'events';
import { Serializable } from 'child_process';
import { generateNonce } from '../Util/Util';
///communicates between the master workers and the process
export class ClusterClient<DiscordClient = DjsDiscordClient> extends EventEmitter {
client: DiscordClient;
mode: 'process' | 'worker';
shardList: number[];
queue: { mode: 'auto' | string | undefined };
maintenance: string | undefined | Boolean;
ready: boolean;
process: ChildClient | WorkerClient | null;
messageHandler: any;
promise: PromiseHandler;
constructor(client: DiscordClient) {
super();
/**
* Client for the Cluster
*/
this.client = client;
/**
* Mode the Cluster was spawned with
*/
this.mode = this.info.CLUSTER_MANAGER_MODE;
const mode = this.mode;
/**
* Shard list with a number of shard ids
*/
this.shardList = this.info.SHARD_LIST;
/**
* If the Cluster is spawned automatically or with an own controller
*/
this.queue = {
mode: this.info.CLUSTER_QUEUE_MODE,
};
/**
* If the Cluster is under maintenance
*/
this.maintenance = this.info.MAINTENANCE;
if (this.maintenance === 'undefined') this.maintenance = false;
if (!this.maintenance) {
// Wait 100ms so listener can be added
setTimeout(() => this.triggerClusterReady(), 100);
}
this.ready = false;
this.process = null;
if (mode === 'process') this.process = new ChildClient();
else if (mode === 'worker') this.process = new WorkerClient();
this.messageHandler = new ClusterClientHandler<DiscordClient>(this, this.process);
this.promise = new PromiseHandler();
this.process?.ipc?.on('message', this._handleMessage.bind(this));
// @ts-ignore
client.on?.('ready', () => {
this.triggerReady();
});
}
/**
* cluster's id
*/
public get id() {
return this.info.CLUSTER;
}
/**
* Array of shard IDs of this client
* @deprecated use client.cluster.shards for getting a collecton of ws shards or client.cluster.shardList for an array of ids
*/
public get ids() {
const client = this.client as DjsDiscordClient;
// Not djs client
if (!client.ws) return this.info.SHARD_LIST;
return client.ws.shards;
}
/**
*
*/
public get shards() {
const client = this.client as DjsDiscordClient;
if (!client.ws?.shards) {
throw new TypeError('Websocket shards property is missing on your Discord Client.');
}
return client.ws.shards;
}
/**
* Total number of clusters
*/
public get count() {
return this.info.CLUSTER_COUNT;
}
/**
* Gets some Info like Cluster_Count, Number, Total shards...
*/
public get info() {
return getInfo();
}
/**
* Sends a message to the master process.
* @fires Cluster#message
*/
public send(message: Serializable) {
if (typeof message === 'object') message = new BaseMessage(message).toJSON();
return this.process?.send(message);
}
/**
* Fetches a client property value of each cluster, or a given cluster.
* @example
* client.cluster.fetchClientValues('guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
* @see {@link ClusterManager#fetchClientValues}
*/
public fetchClientValues(prop: string, cluster?: number) {
return this.broadcastEval(`this.${prop}`, { cluster });
}
/**
* Evaluates a script or function on the Cluster Manager
* @example
* client.cluster.evalOnManager('process.uptime')
* .then(result => console.log(result))
* .catch(console.error);
* @see {@link ClusterManager#evalOnManager}
*/
public evalOnManager(script: string): Promise<any[]>;
public evalOnManager(script: string, options?: evalOptions): Promise<any>;
public evalOnManager<T>(fn: (manager: Manager) => T, options?: evalOptions): Promise<T>;
public evalOnManager<T>(fn: (manager: Manager) => T, options?: evalOptions): Promise<any[]>;
public async evalOnManager<T>(script: string | ((manager: Manager) => T), options?: evalOptions) {
const evalOptions = options || { _type: undefined };
evalOptions._type = messageType.CLIENT_MANAGER_EVAL_REQUEST;
return await this.broadcastEval(script as string, evalOptions);
}
/**
* Evaluates a script or function on all clusters, or a given cluster, in the context of the {@link Client}s.
* @example
* client.cluster.broadcastEval('this.guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
* @see {@link ClusterManager#broadcastEval}
*/
public broadcastEval(script: string): Promise<any[]>;
public broadcastEval(script: string, options?: evalOptions): Promise<any>;
public broadcastEval<T>(fn: (client: DiscordClient) => Awaitable<T>): Promise<Serialized<T>[]>;
public broadcastEval<T>(
fn: (client: DiscordClient) => Awaitable<T>,
options?: { cluster?: number; timeout?: number },
): Promise<Serialized<T>>;
public broadcastEval<T, P>(
fn: (client: DiscordClient, context: Serialized<P>) => Awaitable<T>,
options?: evalOptions<P>,
): Promise<Serialized<T>[]>;
public broadcastEval<T, P>(
fn: (client: DiscordClient, context: Serialized<P>) => Awaitable<T>,
options?: evalOptions<P>,
): Promise<Serialized<T>>;
public async broadcastEval<T, P>(
script: string | ((client: DiscordClient, context?: Serialized<P>) => Awaitable<T> | Promise<Serialized<T>>),
options?: evalOptions | evalOptions<P>,
) {
if (!script || (typeof script !== 'string' && typeof script !== 'function'))
throw new TypeError(
'Script for BroadcastEvaling has not been provided or must be a valid String/Function!',
);
const broadcastOptions = options || { context: undefined, _type: undefined, timeout: undefined };
script =
typeof script === 'function' ? `(${script})(this, ${JSON.stringify(broadcastOptions.context)})` : script;
const nonce = generateNonce();
const message = {
nonce,
_eval: script,
options,
_type: broadcastOptions._type || messageType.CLIENT_BROADCAST_REQUEST,
};
await this.send(message);
return await this.promise.create(message, broadcastOptions);
}
/**
* Sends a Request to the ParentCluster and returns the reply
* @example
* client.cluster.request({content: 'hello'})
* .then(result => console.log(result)) //hi
* .catch(console.error);
* @see {@link IPCMessage#reply}
*/
public request(message: RawMessage) {
const rawMessage = message || { _type: undefined };
rawMessage._type = messageType.CUSTOM_REQUEST;
this.send(rawMessage);
return this.promise.create(rawMessage, {});
}
/**
* Requests a respawn of all clusters.
* @see {@link ClusterManager#respawnAll}
*/
public respawnAll(options: { clusterDelay?: number; respawnDelay?: number; timeout?: number } = {}) {
return this.send({ _type: messageType.CLIENT_RESPAWN_ALL, options });
}
/**
* Handles an IPC message.
* @private
*/
private async _handleMessage(message: RawMessage) {
if (!message) return;
const emit = await this.messageHandler.handleMessage(message);
if (!emit) return;
let emitMessage;
if (typeof message === 'object') emitMessage = new IPCMessage(this, message);
else emitMessage = message;
/**
* Emitted upon receiving a message from the parent process/worker.
* @event ClusterClient#message
* @param {*} message Message that was received
*/
this.emit('message', emitMessage);
}
public async _eval(script: string) {
// @ts-ignore
if (this.client._eval) {
// @ts-ignore
return await this.client._eval(script);
}
// @ts-ignore
this.client._eval = function (_: string) {
return eval(_);
}.bind(this.client);
// @ts-ignore
return await this.client._eval(script);
}
/**
* Sends a message to the master process, emitting an error from the client upon failure.
*/
public _respond(type: string, message: Serializable) {
this.send(message)?.catch(err => {
const error = { err, message: '' };
error.message = `Error when sending ${type} response to master process: ${err.message}`;
/**
* Emitted when the client encounters an error.
* @event Client#error
* @param {Error} error The error encountered
*/
// @ts-ignore
this.client.emit?.(Events.ERROR, error);
});
}
// Hooks
public triggerReady() {
this.process?.send({ _type: messageType.CLIENT_READY });
this.ready = true;
return this.ready;
}
public triggerClusterReady() {
this.emit('ready', this);
return true;
}
/**
*
* @param maintenance Whether the cluster should opt in maintenance when a reason was provided or opt-out when no reason was provided.
* @param all Whether to target it on all clusters or just the current one.
* @returns The maintenance status of the cluster.
*/
public triggerMaintenance(maintenance: string, all = false) {
let _type = messageType.CLIENT_MAINTENANCE;
if (all) _type = messageType.CLIENT_MAINTENANCE_ALL;
this.process?.send({ _type, maintenance });
this.maintenance = maintenance;
return this.maintenance;
}
/**
* Manually spawn the next cluster, when queue mode is on 'manual'
*/
public spawnNextCluster() {
if (this.queue.mode === 'auto')
throw new Error('Next Cluster can just be spawned when the queue is not on auto mode.');
return this.process?.send({ _type: messageType.CLIENT_SPAWN_NEXT_CLUSTER });
}
/**
* gets the total Internal shard count and shard list.
*/
public static getInfo() {
return getInfo();
}
}
// Credits for EventEmitter typings: https://github.com/discordjs/discord.js/blob/main/packages/rest/src/lib/RequestManager.ts#L159 | See attached license
export interface ClusterClient<DiscordClient> {
emit: (<K extends keyof ClusterClientEvents<DiscordClient>>(
event: K,
...args: ClusterClientEvents<DiscordClient>[K]
) => boolean) &
(<S extends string | symbol>(
event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
...args: any[]
) => boolean);
off: (<K extends keyof ClusterClientEvents<DiscordClient>>(
event: K,
listener: (...args: ClusterClientEvents<DiscordClient>[K]) => void,
) => this) &
(<S extends string | symbol>(
event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
listener: (...args: any[]) => void,
) => this);
on: (<K extends keyof ClusterClientEvents<DiscordClient>>(
event: K,
listener: (...args: ClusterClientEvents<DiscordClient>[K]) => void,
) => this) &
(<S extends string | symbol>(
event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
listener: (...args: any[]) => void,
) => this);
once: (<K extends keyof ClusterClientEvents<DiscordClient>>(
event: K,
listener: (...args: ClusterClientEvents<DiscordClient>[K]) => void,
) => this) &
(<S extends string | symbol>(
event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
listener: (...args: any[]) => void,
) => this);
removeAllListeners: (<K extends keyof ClusterClientEvents<DiscordClient>>(event?: K) => this) &
(<S extends string | symbol>(event?: Exclude<S, keyof ClusterClientEvents<DiscordClient>>) => this);
}