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,33 @@
|
|
|
1
|
+
export function getInfo() {
|
|
2
|
+
const shardList: number[] = [];
|
|
3
|
+
const parseShardList = process.env?.SHARD_LIST?.split(',') || [];
|
|
4
|
+
parseShardList.forEach(c => shardList.push(Number(c)));
|
|
5
|
+
|
|
6
|
+
const data: ClusterClientData = {
|
|
7
|
+
SHARD_LIST: shardList,
|
|
8
|
+
TOTAL_SHARDS: Number(process.env.TOTAL_SHARDS),
|
|
9
|
+
CLUSTER_COUNT: Number(process.env.CLUSTER_COUNT),
|
|
10
|
+
CLUSTER: Number(process.env.CLUSTER),
|
|
11
|
+
CLUSTER_MANAGER_MODE: 'process',
|
|
12
|
+
MAINTENANCE: process.env.MAINTENANCE,
|
|
13
|
+
CLUSTER_QUEUE_MODE: process.env.CLUSTER_QUEUE_MODE,
|
|
14
|
+
FIRST_SHARD_ID: shardList[0] as number,
|
|
15
|
+
LAST_SHARD_ID: shardList[shardList.length - 1] as number,
|
|
16
|
+
HEARTBEAT_INTERVAL: Number(process.env.HEARTBEAT_INTERVAL) || 10000,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ClusterClientData {
|
|
23
|
+
SHARD_LIST: number[];
|
|
24
|
+
TOTAL_SHARDS: number;
|
|
25
|
+
LAST_SHARD_ID: number;
|
|
26
|
+
FIRST_SHARD_ID: number;
|
|
27
|
+
CLUSTER_COUNT: number;
|
|
28
|
+
MAINTENANCE?: string;
|
|
29
|
+
CLUSTER_QUEUE_MODE?: 'auto' | string | undefined;
|
|
30
|
+
CLUSTER: number;
|
|
31
|
+
CLUSTER_MANAGER_MODE: 'process';
|
|
32
|
+
HEARTBEAT_INTERVAL: number;
|
|
33
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Cluster } from '../Core/Cluster.js';
|
|
2
|
+
import { ClusterClient } from '../Core/ClusterClient.js';
|
|
3
|
+
import { ClusterManager } from '../Core/ClusterManager.js';
|
|
4
|
+
import { messageType } from '../types/shared.js';
|
|
5
|
+
import { makePlainError } from '../Util/Util.js';
|
|
6
|
+
import { Child, ChildClient } from './Child.js';
|
|
7
|
+
import { RawMessage } from './IPCMessage.js';
|
|
8
|
+
import { ResolveMessage } from './PromiseHandler.js';
|
|
9
|
+
|
|
10
|
+
export class ClusterHandler {
|
|
11
|
+
manager: ClusterManager;
|
|
12
|
+
cluster: Cluster;
|
|
13
|
+
ipc: Child;
|
|
14
|
+
constructor(manager: ClusterManager, cluster: Cluster, ipc: Child) {
|
|
15
|
+
this.manager = manager;
|
|
16
|
+
this.cluster = cluster;
|
|
17
|
+
this.ipc = ipc;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
handleMessage(message: RawMessage) {
|
|
21
|
+
if (message._type === messageType.CLIENT_READY) {
|
|
22
|
+
this.cluster.ready = true;
|
|
23
|
+
this.cluster.emit('ready');
|
|
24
|
+
this.cluster.manager._debug('Ready', this.cluster.id);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (message._type === messageType.CLIENT_BROADCAST_REQUEST) {
|
|
28
|
+
this.cluster.manager
|
|
29
|
+
.broadcastEval(message._eval, message.options)
|
|
30
|
+
?.then(results => {
|
|
31
|
+
return this.ipc.send({
|
|
32
|
+
nonce: message.nonce,
|
|
33
|
+
_type: messageType.CLIENT_BROADCAST_RESPONSE,
|
|
34
|
+
_result: results,
|
|
35
|
+
});
|
|
36
|
+
})
|
|
37
|
+
.catch(err => {
|
|
38
|
+
return this.ipc.send({
|
|
39
|
+
nonce: message.nonce,
|
|
40
|
+
_type: messageType.CLIENT_BROADCAST_RESPONSE,
|
|
41
|
+
_error: makePlainError(err),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (message._type === messageType.CLIENT_MANAGER_EVAL_REQUEST) {
|
|
47
|
+
this.cluster.manager.evalOnManager(message._eval).then(result => {
|
|
48
|
+
if (result._error) {
|
|
49
|
+
return this.ipc.send({
|
|
50
|
+
nonce: message.nonce,
|
|
51
|
+
_type: messageType.CLIENT_MANAGER_EVAL_RESPONSE,
|
|
52
|
+
_error: makePlainError(result._error),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return this.ipc.send({
|
|
56
|
+
nonce: message.nonce,
|
|
57
|
+
_type: messageType.CLIENT_MANAGER_EVAL_RESPONSE,
|
|
58
|
+
_result: result._result,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (message._type === messageType.CLIENT_EVAL_RESPONSE) {
|
|
64
|
+
this.cluster.manager.promise.resolve(message as ResolveMessage);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (message._type === messageType.CLIENT_RESPAWN_ALL) {
|
|
68
|
+
this.cluster.manager.respawnAll(message.options);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (message._type === messageType.CLIENT_RESPAWN) {
|
|
72
|
+
this.cluster.respawn(message.options);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (message._type === messageType.CLIENT_MAINTENANCE) {
|
|
76
|
+
this.cluster.triggerMaintenance(message.maintenance);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (message._type === messageType.CLIENT_MAINTENANCE_ALL) {
|
|
80
|
+
this.cluster.manager.triggerMaintenance(message.maintenance);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (message._type === messageType.CLIENT_SPAWN_NEXT_CLUSTER) {
|
|
84
|
+
this.cluster.manager.queue.next();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (message._type === messageType.HEARTBEAT_ACK) {
|
|
88
|
+
// Heartbeat system now handles acks internally via Redis if needed, or we just ignore here
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (message._type === messageType.CUSTOM_REPLY) {
|
|
92
|
+
this.cluster.manager.promise.resolve(message as ResolveMessage);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class ClusterClientHandler<DiscordClient> {
|
|
100
|
+
client: ClusterClient<DiscordClient>;
|
|
101
|
+
ipc: ChildClient | null;
|
|
102
|
+
constructor(client: ClusterClient<DiscordClient>, ipc: ChildClient | null) {
|
|
103
|
+
this.client = client;
|
|
104
|
+
this.ipc = ipc;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async handleMessage(message: ResolveMessage & { date?: number; maintenance?: string }) {
|
|
108
|
+
if (message._type === messageType.CLIENT_EVAL_REQUEST) {
|
|
109
|
+
try {
|
|
110
|
+
if (!message._eval) throw new Error('Eval Script not provided');
|
|
111
|
+
this.client._respond('eval', {
|
|
112
|
+
_eval: message._eval,
|
|
113
|
+
_result: await this.client._eval(message._eval),
|
|
114
|
+
_type: messageType.CLIENT_EVAL_RESPONSE,
|
|
115
|
+
nonce: message.nonce,
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
this.client._respond('eval', {
|
|
119
|
+
_eval: message._eval,
|
|
120
|
+
_error: makePlainError(err),
|
|
121
|
+
_type: messageType.CLIENT_EVAL_RESPONSE,
|
|
122
|
+
nonce: message.nonce,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (message._type === messageType.CLIENT_MANAGER_EVAL_RESPONSE) {
|
|
128
|
+
this.client.promise.resolve({ _result: message._result, _error: message._error, nonce: message.nonce });
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (message._type === messageType.CLIENT_BROADCAST_RESPONSE) {
|
|
132
|
+
this.client.promise.resolve({ _result: message._result, _error: message._error, nonce: message.nonce });
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (message._type === messageType.HEARTBEAT) {
|
|
136
|
+
this.client.send({ _type: messageType.HEARTBEAT_ACK, date: message.date });
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (message._type === messageType.CLIENT_MAINTENANCE_DISABLE) {
|
|
140
|
+
this.client.maintenance = false;
|
|
141
|
+
this.client.triggerClusterReady();
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (message._type === messageType.CLIENT_MAINTENANCE_ENABLE) {
|
|
145
|
+
this.client.maintenance = message.maintenance || true;
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (message._type === messageType.CUSTOM_REPLY) {
|
|
149
|
+
this.client.promise.resolve(message);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { generateNonce } from '../Util/Util.js';
|
|
2
|
+
import { messageType } from '../types/shared.js';
|
|
3
|
+
import { ClusterClient } from '../Core/ClusterClient.js';
|
|
4
|
+
import { Cluster } from '../Core/Cluster.js';
|
|
5
|
+
|
|
6
|
+
export interface RawMessage {
|
|
7
|
+
nonce?: string;
|
|
8
|
+
_type?: number | messageType;
|
|
9
|
+
[x: string]: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class BaseMessage {
|
|
13
|
+
[x: string]: any;
|
|
14
|
+
nonce: string;
|
|
15
|
+
private readonly _raw: RawMessage;
|
|
16
|
+
constructor(message: RawMessage) {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a Message ID for identifying it for further Usage such as on replies
|
|
19
|
+
*/
|
|
20
|
+
this.nonce = message.nonce || generateNonce();
|
|
21
|
+
message.nonce = this.nonce;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Destructs the Message Object and initializes it on the Constructor
|
|
25
|
+
*/
|
|
26
|
+
this._raw = this.destructMessage(message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Destructs the Message Object and initializes it on the Constructor
|
|
31
|
+
*/
|
|
32
|
+
private destructMessage(message: RawMessage) {
|
|
33
|
+
for (const [key, value] of Object.entries(message)) {
|
|
34
|
+
this[key] = value;
|
|
35
|
+
}
|
|
36
|
+
if (message.nonce) this.nonce = message.nonce;
|
|
37
|
+
this._type = message._type || messageType.CUSTOM_MESSAGE;
|
|
38
|
+
return message;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public toJSON() {
|
|
42
|
+
return this._raw;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class IPCMessage extends BaseMessage {
|
|
47
|
+
raw: RawMessage;
|
|
48
|
+
instance: ClusterClient<any> | Cluster;
|
|
49
|
+
constructor(instance: ClusterClient<any> | Cluster, message: RawMessage) {
|
|
50
|
+
super(message);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Instance, which can be the ParentCluster or the ClusterClient
|
|
54
|
+
*/
|
|
55
|
+
this.instance = instance;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Base Message, which is saved on the raw field.
|
|
59
|
+
*/
|
|
60
|
+
this.raw = new BaseMessage(message).toJSON();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sends a message to the cluster's process/worker or to the ParentCluster.
|
|
65
|
+
*/
|
|
66
|
+
public async send(message: object) {
|
|
67
|
+
if (typeof message !== 'object') throw new TypeError('The Message has to be a object');
|
|
68
|
+
const baseMessage = new BaseMessage({ ...message, _type: messageType.CUSTOM_MESSAGE });
|
|
69
|
+
return this.instance.send(baseMessage.toJSON());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sends a Request to the cluster's process/worker or to the ParentCluster.
|
|
74
|
+
*/
|
|
75
|
+
public async request(message: object) {
|
|
76
|
+
if (typeof message !== 'object') throw new TypeError('The Message has to be a object');
|
|
77
|
+
const baseMessage = new BaseMessage({ ...message, _type: messageType.CUSTOM_REQUEST, nonce: this.nonce });
|
|
78
|
+
return this.instance.request(baseMessage.toJSON());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sends a Reply to Message from the cluster's process/worker or the ParentCluster.
|
|
83
|
+
*/
|
|
84
|
+
public async reply(message: object) {
|
|
85
|
+
if (typeof message !== 'object') throw new TypeError('The Message has to be a object');
|
|
86
|
+
const baseMessage = new BaseMessage({
|
|
87
|
+
...message,
|
|
88
|
+
_type: messageType.CUSTOM_REPLY,
|
|
89
|
+
nonce: this.nonce,
|
|
90
|
+
_result: message,
|
|
91
|
+
});
|
|
92
|
+
return this.instance.send(baseMessage.toJSON());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Gets the type of the Message from the raw properties
|
|
97
|
+
*/
|
|
98
|
+
public getType() {
|
|
99
|
+
return this.raw._type ?? messageType.CUSTOM_MESSAGE
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { generateNonce } from '../Util/Util.js';
|
|
2
|
+
import { RawMessage } from './IPCMessage.js';
|
|
3
|
+
|
|
4
|
+
export interface StoredPromise {
|
|
5
|
+
resolve(value: any): void;
|
|
6
|
+
reject(error: Error): void;
|
|
7
|
+
options: PromiseCreateOptions;
|
|
8
|
+
timeout?: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResolveMessage {
|
|
12
|
+
_error: { message: string; stack: string; name: string };
|
|
13
|
+
_result: any;
|
|
14
|
+
_eval?: string;
|
|
15
|
+
_type?: number;
|
|
16
|
+
nonce: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PromiseCreateOptions {
|
|
20
|
+
timeout?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class PromiseHandler {
|
|
24
|
+
nonce: Map<string, StoredPromise>;
|
|
25
|
+
constructor() {
|
|
26
|
+
this.nonce = new Map();
|
|
27
|
+
}
|
|
28
|
+
public resolve(message: ResolveMessage) {
|
|
29
|
+
const promise = this.nonce.get(message.nonce);
|
|
30
|
+
if (promise) {
|
|
31
|
+
if (promise.timeout) clearTimeout(promise.timeout);
|
|
32
|
+
this.nonce.delete(message.nonce);
|
|
33
|
+
if (message._error) {
|
|
34
|
+
const error = new Error(message._error.message);
|
|
35
|
+
error.stack = message._error.stack;
|
|
36
|
+
error.name = message._error.name;
|
|
37
|
+
promise.reject(error);
|
|
38
|
+
} else {
|
|
39
|
+
promise.resolve(message._result);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async create(
|
|
45
|
+
message: RawMessage & { options?: PromiseCreateOptions; stack?: string },
|
|
46
|
+
options: PromiseCreateOptions,
|
|
47
|
+
) {
|
|
48
|
+
if (!options) options = {};
|
|
49
|
+
if (Object.keys(options).length === 0 && message.options) options = message.options;
|
|
50
|
+
if (!message.nonce) message.nonce = generateNonce();
|
|
51
|
+
return await new Promise((resolve, reject) => {
|
|
52
|
+
if (options.timeout) {
|
|
53
|
+
const timeout = setTimeout(() => {
|
|
54
|
+
this.nonce.delete(message.nonce as string);
|
|
55
|
+
const error = new Error('Promise timed out');
|
|
56
|
+
error.stack = message.stack || error.stack;
|
|
57
|
+
reject(error);
|
|
58
|
+
}, options.timeout);
|
|
59
|
+
this.nonce.set(message.nonce as string, { resolve, reject, options, timeout });
|
|
60
|
+
} else this.nonce.set(message.nonce as string, { resolve, reject, options });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { QueueOptions } from '../types/shared.js';
|
|
2
|
+
import { delayFor } from '../Util/Util.js';
|
|
3
|
+
|
|
4
|
+
export interface QueueItem {
|
|
5
|
+
run(...args: any): Promise<any>;
|
|
6
|
+
args: any[];
|
|
7
|
+
time?: number;
|
|
8
|
+
timeout: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Queue {
|
|
12
|
+
queue: QueueItem[];
|
|
13
|
+
options: Required<QueueOptions>;
|
|
14
|
+
paused: boolean;
|
|
15
|
+
constructor(options: Required<QueueOptions>) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.queue = [];
|
|
18
|
+
this.paused = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Starts the queue and run's the item functions
|
|
23
|
+
*/
|
|
24
|
+
public async start() {
|
|
25
|
+
if (!this.options.auto) {
|
|
26
|
+
return new Promise(resolve => {
|
|
27
|
+
const interval = setInterval(() => {
|
|
28
|
+
if (this.queue.length === 0) {
|
|
29
|
+
clearInterval(interval);
|
|
30
|
+
resolve('Queue finished');
|
|
31
|
+
}
|
|
32
|
+
}, 200);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const length = this.queue.length;
|
|
37
|
+
for (let i = 0; i < length; i++) {
|
|
38
|
+
if (!this.queue[0]) continue;
|
|
39
|
+
const timeout = this.queue[0].timeout;
|
|
40
|
+
await this.next();
|
|
41
|
+
await delayFor(timeout);
|
|
42
|
+
}
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Goes to the next item in the queue
|
|
48
|
+
*/
|
|
49
|
+
public async next() {
|
|
50
|
+
if (this.paused) return;
|
|
51
|
+
const item = this.queue.shift();
|
|
52
|
+
if (!item) return true;
|
|
53
|
+
return item.run(...item.args);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stop's the queue and blocks the next item from running
|
|
58
|
+
*/
|
|
59
|
+
public stop() {
|
|
60
|
+
this.paused = true;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resume's the queue
|
|
66
|
+
*/
|
|
67
|
+
public resume() {
|
|
68
|
+
this.paused = false;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Adds an item to the queue
|
|
74
|
+
*/
|
|
75
|
+
public add(item: QueueItem) {
|
|
76
|
+
this.queue.push({
|
|
77
|
+
run: item.run,
|
|
78
|
+
args: item.args,
|
|
79
|
+
time: Date.now(),
|
|
80
|
+
timeout: item.timeout ?? this.options.timeout,
|
|
81
|
+
});
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createClient, type RedisClientType } from 'redis';
|
|
2
|
+
|
|
3
|
+
export class RedisClient {
|
|
4
|
+
public client: RedisClientType;
|
|
5
|
+
private isConnected = false;
|
|
6
|
+
|
|
7
|
+
constructor(url: string = process.env.REDIS_URL || 'redis://localhost:6379') {
|
|
8
|
+
this.client = createClient({
|
|
9
|
+
url,
|
|
10
|
+
socket: {
|
|
11
|
+
reconnectStrategy: (retries) => {
|
|
12
|
+
return Math.min(retries * 100, 3000);
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
this.setupEvents();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private setupEvents() {
|
|
21
|
+
this.client.on('connect', () => {
|
|
22
|
+
this.isConnected = true;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.client.on('error', (err) => {
|
|
26
|
+
this.isConnected = false;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this.client.on('reconnecting', () => {
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async connect(): Promise<void> {
|
|
34
|
+
if (!this.isConnected) {
|
|
35
|
+
await this.client.connect();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async set(key: string, value: any, ttl?: number): Promise<void> {
|
|
40
|
+
const serialized = JSON.stringify(value);
|
|
41
|
+
|
|
42
|
+
if (ttl) {
|
|
43
|
+
await this.client.setEx(key, ttl, serialized);
|
|
44
|
+
} else {
|
|
45
|
+
await this.client.set(key, serialized);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async get<T>(key: string): Promise<T | null> {
|
|
50
|
+
const data = await this.client.get(key);
|
|
51
|
+
return data ? JSON.parse(data) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async del(key: string): Promise<void> {
|
|
55
|
+
await this.client.del(key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async publish(channel: string, message: any): Promise<void> {
|
|
59
|
+
await this.client.publish(channel, JSON.stringify(message));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async subscribe(channel: string, callback: (message: any) => void): Promise<void> {
|
|
63
|
+
const subscriber = this.client.duplicate();
|
|
64
|
+
await subscriber.connect();
|
|
65
|
+
|
|
66
|
+
await subscriber.subscribe(channel, (message) => {
|
|
67
|
+
callback(JSON.parse(message));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async disconnect(): Promise<void> {
|
|
72
|
+
if (this.isConnected) {
|
|
73
|
+
await this.client.disconnect();
|
|
74
|
+
this.isConnected = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/Util/Util.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DefaultOptions, Endpoints } from "../types/shared.js";
|
|
2
|
+
|
|
3
|
+
export function generateNonce() {
|
|
4
|
+
return Date.now().toString(36) + Math.random().toString(36);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function chunkArray(array: any[], chunkSize: number) {
|
|
8
|
+
const R = [];
|
|
9
|
+
for (let i = 0; i < array.length; i += chunkSize) R.push(array.slice(i, i + chunkSize));
|
|
10
|
+
return R;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function arraysAreTheSame(firstArray: any[], secondArray: any[]) {
|
|
14
|
+
return firstArray.length === secondArray.length && firstArray.every((element, index) => element === secondArray[index]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function delayFor(ms: number) {
|
|
18
|
+
if(ms < 0) return;
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
setTimeout(resolve, ms);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function makePlainError(err: Error) {
|
|
25
|
+
return {
|
|
26
|
+
name: err['name'],
|
|
27
|
+
message: err['message'],
|
|
28
|
+
stack: err['stack'],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shardIdForGuildId(guildId: string, totalShards = 1) {
|
|
33
|
+
const shard = Number(BigInt(guildId) >> BigInt(22)) % totalShards;
|
|
34
|
+
if (shard < 0)
|
|
35
|
+
throw new Error(
|
|
36
|
+
'SHARD_MISCALCULATION_SHARDID_SMALLER_THAN_0 ' +
|
|
37
|
+
`Calculated Shard: ${shard}, guildId: ${guildId}, totalShards: ${totalShards}`,
|
|
38
|
+
);
|
|
39
|
+
return shard;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function fetchRecommendedShards(token: string, guildsPerShard = 1000) {
|
|
43
|
+
if (!token) throw new Error('DISCORD_TOKEN_MISSING');
|
|
44
|
+
|
|
45
|
+
const res = await fetch(
|
|
46
|
+
`${DefaultOptions.http.api}/v${DefaultOptions.http.version}${Endpoints.botGateway}`,
|
|
47
|
+
{
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bot ${token.replace(/^Bot\s*/i, '')}`,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
if (res.status === 401) throw new Error('DISCORD_TOKEN_INVALID');
|
|
57
|
+
throw new Error(`Failed to fetch data. Status code: ${res.status}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const responseData = await res.json() as any;
|
|
61
|
+
return responseData.shards * (1000 / guildsPerShard);
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './Core/Cluster.js';
|
|
2
|
+
export * from './Core/ClusterClient.js';
|
|
3
|
+
export * from './Core/ClusterManager.js';
|
|
4
|
+
export * from './Core/DashboardServer.js';
|
|
5
|
+
export * from './Plugins/HeartbeatSystem.js';
|
|
6
|
+
export * from './Plugins/ReCluster.js';
|
|
7
|
+
export * from './Plugins/AutoResharderSystem.js';
|
|
8
|
+
export * from './Plugins/QueueManager.js';
|
|
9
|
+
export * from './Structures/Child.js';
|
|
10
|
+
export * from './Structures/Data.js';
|
|
11
|
+
export * from './Structures/IPCHandler.js';
|
|
12
|
+
export * from './Structures/IPCMessage.js';
|
|
13
|
+
export * from './Structures/PromiseHandler.js';
|
|
14
|
+
export * from './Structures/Queue.js';
|
|
15
|
+
export * from './types/shared.js';
|
|
16
|
+
export * from './Util/Util.js';
|
|
17
|
+
export * from './Util/RedisClient.js';
|