buncord-hybrid-sharding 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,329 @@
1
+ import EventEmitter from "node:events";
2
+
3
+ import { ChildClient } from "../Structures/Child.js";
4
+ import { getInfo } from "../Structures/Data.js";
5
+ import { ClusterClientHandler } from "../Structures/IPCHandler.js";
6
+ import { BaseMessage, IPCMessage, RawMessage } from "../Structures/IPCMessage.js";
7
+ import { PromiseHandler } from "../Structures/PromiseHandler.js";
8
+ import {
9
+ Awaitable, ClusterClientEvents, DjsDiscordClient, evalOptions, Events, messageType, Serialized
10
+ } from "../types/shared.js";
11
+ import { generateNonce } from "../Util/Util.js";
12
+
13
+ /**
14
+ * communicates between the master and the cluster process
15
+ */
16
+ export class ClusterClient<DiscordClient = DjsDiscordClient> extends EventEmitter {
17
+ client: DiscordClient;
18
+ shardList: number[];
19
+ queue: { mode: 'auto' | string | undefined };
20
+ maintenance: string | undefined | Boolean;
21
+ ready: boolean;
22
+ process: ChildClient | null;
23
+ messageHandler: any;
24
+ promise: PromiseHandler;
25
+
26
+ constructor(client: DiscordClient) {
27
+ super();
28
+ /**
29
+ * Client for the Cluster
30
+ */
31
+ this.client = client;
32
+
33
+ /**
34
+ * Shard list with a number of shard ids
35
+ */
36
+ this.shardList = this.info.SHARD_LIST;
37
+
38
+ /**
39
+ * If the Cluster is spawned automatically or with an own controller
40
+ */
41
+ this.queue = {
42
+ mode: this.info.CLUSTER_QUEUE_MODE,
43
+ };
44
+
45
+ /**
46
+ * If the Cluster is under maintenance
47
+ */
48
+ this.maintenance = this.info.MAINTENANCE;
49
+ if (this.maintenance === 'undefined') this.maintenance = false;
50
+ if (!this.maintenance) {
51
+ // Wait 100ms so listener can be added
52
+ setTimeout(() => this.triggerClusterReady(), 100);
53
+ }
54
+
55
+ this.ready = false;
56
+
57
+ this.process = new ChildClient();
58
+
59
+ this.messageHandler = new ClusterClientHandler<DiscordClient>(this, this.process);
60
+
61
+ this.promise = new PromiseHandler();
62
+
63
+ // Redis Heartbeat
64
+ this._startHeartbeat();
65
+
66
+ // Communication via process message handled globally in Bun/Node child processes
67
+ process.on('message', this._handleMessage.bind(this));
68
+ }
69
+
70
+ private async _startHeartbeat() {
71
+ const redis = new (await import("../Util/RedisClient.js")).RedisClient();
72
+ await redis.connect().catch(() => {});
73
+ const interval = Number(this.info.HEARTBEAT_INTERVAL || 10000);
74
+
75
+ setInterval(async () => {
76
+ if (this.ready) {
77
+ await redis.set(`hb:cluster:${this.id}`, Date.now().toString(), Math.floor(interval * 2.5 / 1000)).catch(() => {});
78
+ }
79
+ }, interval);
80
+ }
81
+ /**
82
+ * cluster's id
83
+ */
84
+ public get id() {
85
+ return this.info.CLUSTER;
86
+ }
87
+
88
+ /**
89
+ * Total number of clusters
90
+ */
91
+ public get count() {
92
+ return this.info.CLUSTER_COUNT;
93
+ }
94
+ /**
95
+ * Gets some Info like Cluster_Count, Number, Total shards...
96
+ */
97
+ public get info() {
98
+ return getInfo();
99
+ }
100
+ /**
101
+ * Sends a message to the master process.
102
+ * @fires Cluster#message
103
+ */
104
+ public send(message: any) {
105
+ if (typeof message === 'object') message = new BaseMessage(message).toJSON();
106
+ return this.process?.send(message);
107
+ }
108
+ /**
109
+ * Fetches a client property value of each cluster, or a given cluster.
110
+ * @example
111
+ * client.cluster.fetchClientValues('guilds.cache.size')
112
+ * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
113
+ * .catch(console.error);
114
+ * @see {@link ClusterManager#fetchClientValues}
115
+ */
116
+ public fetchClientValues(prop: string, cluster?: number) {
117
+ return this.broadcastEval(`this.${prop}`, { cluster });
118
+ }
119
+
120
+ /**
121
+ * Evaluates a script or function on the Cluster Manager
122
+ * @see {@link ClusterManager#evalOnManager}
123
+ */
124
+ public async evalOnManager<T>(script: string | ((manager: any) => T), options?: evalOptions) {
125
+ const evalOptions = options || { _type: undefined };
126
+ evalOptions._type = messageType.CLIENT_MANAGER_EVAL_REQUEST;
127
+
128
+ return await this.broadcastEval(script as string, evalOptions);
129
+ }
130
+
131
+ /**
132
+ * Evaluates a script or function on all clusters, or a given cluster, in the context of the {@link DjsDiscordClient}s.
133
+ * @see {@link ClusterManager#broadcastEval}
134
+ */
135
+ public broadcastEval(script: string): Promise<any[]>;
136
+ public broadcastEval(script: string, options?: evalOptions): Promise<any>;
137
+ public broadcastEval<T>(fn: (client: DiscordClient) => Awaitable<T>): Promise<Serialized<T>[]>;
138
+ public broadcastEval<T>(
139
+ fn: (client: DiscordClient) => Awaitable<T>,
140
+ options?: { cluster?: number; timeout?: number },
141
+ ): Promise<Serialized<T>>;
142
+ public broadcastEval<T, P>(
143
+ fn: (client: DiscordClient, context: Serialized<P>) => Awaitable<T>,
144
+ options?: evalOptions<P>,
145
+ ): Promise<Serialized<T>[]>;
146
+ public broadcastEval<T, P>(
147
+ fn: (client: DiscordClient, context: Serialized<P>) => Awaitable<T>,
148
+ options?: evalOptions<P>,
149
+ ): Promise<Serialized<T>>;
150
+ public async broadcastEval<T, P>(
151
+ script: string | ((client: DiscordClient, context?: Serialized<P>) => Awaitable<T> | Promise<Serialized<T>>),
152
+ options?: evalOptions | evalOptions<P>,
153
+ ) {
154
+ if (!script || (typeof script !== 'string' && typeof script !== 'function'))
155
+ throw new TypeError(
156
+ 'Script for BroadcastEvaling has not been provided or must be a valid String/Function!',
157
+ );
158
+
159
+ const broadcastOptions = options || { context: undefined, _type: undefined, timeout: undefined };
160
+ script =
161
+ typeof script === 'function' ? `(${script})(this, ${JSON.stringify(broadcastOptions.context)})` : script;
162
+ const nonce = generateNonce();
163
+ const message = {
164
+ nonce,
165
+ _eval: script,
166
+ options,
167
+ _type: broadcastOptions._type || messageType.CLIENT_BROADCAST_REQUEST,
168
+ };
169
+ await this.send(message);
170
+
171
+ return await this.promise.create(message, broadcastOptions);
172
+ }
173
+ /**
174
+ * Sends a Request to the Master process and returns the reply
175
+ * @example
176
+ * client.cluster.request({content: 'hello'})
177
+ * .then(result => console.log(result)) //hi
178
+ * .catch(console.error);
179
+ * @see {@link IPCMessage#reply}
180
+ */
181
+ public request(message: RawMessage) {
182
+ const rawMessage = message || { _type: undefined };
183
+ rawMessage._type = messageType.CUSTOM_REQUEST;
184
+ this.send(rawMessage);
185
+ return this.promise.create(rawMessage, {});
186
+ }
187
+
188
+ /**
189
+ * Requests a respawn of all clusters.
190
+ * @see {@link ClusterManager#respawnAll}
191
+ */
192
+ public respawnAll(options: { clusterDelay?: number; respawnDelay?: number; timeout?: number } = {}) {
193
+ return this.send({ _type: messageType.CLIENT_RESPAWN_ALL, options });
194
+ }
195
+
196
+ /**
197
+ * Handles an IPC message.
198
+ * @private
199
+ */
200
+ private async _handleMessage(message: any) {
201
+ if (!message) return;
202
+ const emit = await this.messageHandler.handleMessage(message);
203
+ if (!emit) return;
204
+ let emitMessage;
205
+ if (typeof message === 'object') emitMessage = new IPCMessage(this, message);
206
+ else emitMessage = message;
207
+ /**
208
+ * Emitted upon receiving a message from the master process.
209
+ * @event ClusterClient#message
210
+ * @param {*} message Message that was received
211
+ */
212
+ this.emit('message', emitMessage);
213
+ }
214
+
215
+ public async _eval(script: string) {
216
+ // @ts-ignore
217
+ if (this.client._eval) {
218
+ // @ts-ignore
219
+ return await this.client._eval(script);
220
+ }
221
+ // @ts-ignore
222
+ this.client._eval = function (_: string) {
223
+ return eval(_); // eslint-disable-line no-eval
224
+ }.bind(this.client);
225
+ // @ts-ignore
226
+ return await this.client._eval(script);
227
+ }
228
+
229
+ /**
230
+ * Sends a message to the master process, emitting an error from the client upon failure.
231
+ */
232
+ public _respond(type: string, message: any) {
233
+ this.send(message)?.catch((err: any) => {
234
+ const error = { err, message: '' };
235
+
236
+ error.message = `Error when sending ${type} response to master process: ${err.message}`;
237
+ /**
238
+ * Emitted when the client encounters an error.
239
+ * @event Client#error
240
+ * @param {Error} error The error encountered
241
+ */
242
+ // @ts-ignore
243
+ this.client.emit?.(Events.ERROR, error);
244
+ });
245
+ }
246
+
247
+ // Hooks
248
+ public triggerReady() {
249
+ this.send({ _type: messageType.CLIENT_READY });
250
+ this.ready = true;
251
+ return this.ready;
252
+ }
253
+
254
+ public triggerClusterReady() {
255
+ this.emit('ready', this);
256
+ return true;
257
+ }
258
+
259
+ /**
260
+ *
261
+ * @param maintenance Whether the cluster should opt in maintenance when a reason was provided or opt-out when no reason was provided.
262
+ * @param all Whether to target it on all clusters or just the current one.
263
+ * @returns The maintenance status of the cluster.
264
+ */
265
+ public triggerMaintenance(maintenance: string, all = false) {
266
+ let _type = messageType.CLIENT_MAINTENANCE;
267
+ if (all) _type = messageType.CLIENT_MAINTENANCE_ALL;
268
+ this.send({ _type, maintenance });
269
+ this.maintenance = maintenance;
270
+ return this.maintenance;
271
+ }
272
+
273
+ /**
274
+ * Manually spawn the next cluster, when queue mode is on 'manual'
275
+ */
276
+ public spawnNextCluster() {
277
+ if (this.queue.mode === 'auto')
278
+ throw new Error('Next Cluster can just be spawned when the queue is not on auto mode.');
279
+ return this.send({ _type: messageType.CLIENT_SPAWN_NEXT_CLUSTER });
280
+ }
281
+
282
+ /**
283
+ * gets the total Internal shard count and shard list.
284
+ */
285
+ public static getInfo() {
286
+ return getInfo();
287
+ }
288
+ }
289
+
290
+ export interface ClusterClient<DiscordClient> {
291
+ emit: (<K extends keyof ClusterClientEvents<DiscordClient>>(
292
+ event: K,
293
+ ...args: ClusterClientEvents<DiscordClient>[K]
294
+ ) => boolean) &
295
+ (<S extends string | symbol>(
296
+ event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
297
+ ...args: any[]
298
+ ) => boolean);
299
+
300
+ off: (<K extends keyof ClusterClientEvents<DiscordClient>>(
301
+ event: K,
302
+ listener: (...args: ClusterClientEvents<DiscordClient>[K]) => void,
303
+ ) => this) &
304
+ (<S extends string | symbol>(
305
+ event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
306
+ listener: (...args: any[]) => void,
307
+ ) => this);
308
+
309
+ on: (<K extends keyof ClusterClientEvents<DiscordClient>>(
310
+ event: K,
311
+ listener: (...args: ClusterClientEvents<DiscordClient>[K]) => void,
312
+ ) => this) &
313
+ (<S extends string | symbol>(
314
+ event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
315
+ listener: (...args: any[]) => void,
316
+ ) => this);
317
+
318
+ once: (<K extends keyof ClusterClientEvents<DiscordClient>>(
319
+ event: K,
320
+ listener: (...args: ClusterClientEvents<DiscordClient>[K]) => void,
321
+ ) => this) &
322
+ (<S extends string | symbol>(
323
+ event: Exclude<S, keyof ClusterClientEvents<DiscordClient>>,
324
+ listener: (...args: any[]) => void,
325
+ ) => this);
326
+
327
+ removeAllListeners: (<K extends keyof ClusterClientEvents<DiscordClient>>(event?: K) => this) &
328
+ (<S extends string | symbol>(event?: Exclude<S, keyof ClusterClientEvents<DiscordClient>>) => this);
329
+ }