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,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,9 @@
1
+ import type { Cluster } from '../Core/Cluster';
2
+
3
+ export class ClusterManagerHooks {
4
+ constructor() {}
5
+
6
+ constructClusterArgs(cluster: Cluster, args: string[]) {
7
+ return args;
8
+ }
9
+ }
@@ -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
+ }
@@ -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';