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.
- package/.gitattributes +2 -0
- package/LICENSE +28 -0
- package/README.md +91 -0
- package/bun.lock +88 -0
- package/package.json +39 -0
- package/src/Core/Cluster.ts +414 -0
- package/src/Core/ClusterClient.ts +329 -0
- package/src/Core/ClusterManager.ts +545 -0
- package/src/Core/DashboardServer.ts +84 -0
- package/src/Plugins/AutoResharderSystem.ts +382 -0
- package/src/Plugins/HeartbeatSystem.ts +56 -0
- package/src/Plugins/QueueManager.ts +49 -0
- package/src/Plugins/ReCluster.ts +101 -0
- package/src/Structures/Child.ts +109 -0
- package/src/Structures/Data.ts +33 -0
- package/src/Structures/IPCHandler.ts +154 -0
- package/src/Structures/IPCMessage.ts +101 -0
- package/src/Structures/ManagerHooks.ts +9 -0
- package/src/Structures/PromiseHandler.ts +63 -0
- package/src/Structures/Queue.ts +84 -0
- package/src/Util/RedisClient.ts +77 -0
- package/src/Util/Util.ts +62 -0
- package/src/index.ts +17 -0
- package/src/types/shared.ts +164 -0
|
@@ -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
|
+
}
|