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 ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ ------------------------------------------------------------------------------------------
2
+ | This following License accounts to the changes in Changes.md, |
3
+ | the unchanged code is the intellectual property of discord.js and their Contributors. |
4
+ | Check the attached License of Discord.js in the Repo |
5
+ ------------------------------------------------------------------------------------------
6
+
7
+ MIT License
8
+
9
+ Copyright (c) 2026 Luigi Colantuono
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # ⚡ Buncord-Hybrid-Sharding
2
+
3
+ The ultimate **Enterprise Bun-native** sharding manager for Discord bots. Built for performance, reliability, and scale.
4
+
5
+ `buncord-hybrid-sharding` is a ground-up refactor of the hybrid sharding concept, optimized specifically for the Bun runtime. It eliminates all Node.js dependencies, leveraging `Bun.spawn` and native Bun IPC for ultra-fast, low-overhead clustering.
6
+
7
+ ## 🚀 Key Features
8
+
9
+ - **🔋 Bun-Native Core**: Zero Node.js dependencies. Uses `Bun.spawn` and native IPC for maximum performance.
10
+ - **🔄 Zero-Downtime Rolling Restarts**: Built-in `ReClusterManager` for updating your bot with zero service interruption.
11
+ - **💓 Redis-Backed Heartbeats**: Distributed health monitoring using Redis. If a cluster hangs, it's detected and restarted automatically via TTL.
12
+ - **📊 Integrated Dashboard API**: Built-in monitoring server (port 3001) using `Bun.serve` to track cluster health and trigger administrative actions.
13
+ - **🚦 QueueManager Plugin**: Advanced control over cluster spawning to respect Discord's rate limits precisely.
14
+ - **📉 Resource Efficiency**: Hybrid sharding (multiple shards per process) reduces memory overhead by 40-60%.
15
+
16
+ ---
17
+
18
+ ## 📦 Installation
19
+
20
+ ```bash
21
+ bun add buncord-hybrid-sharding
22
+ ```
23
+
24
+ ## 🛠️ Quick Start
25
+
26
+ ### 1. The Manager (`cluster.js`)
27
+
28
+ ```js
29
+ import { ClusterManager, ReClusterManager, HeartbeatManager, DashboardServer } from 'buncord-hybrid-sharding';
30
+
31
+ const manager = new ClusterManager(`./bot.js`, {
32
+ totalShards: 'auto',
33
+ shardsPerClusters: 2,
34
+ mode: 'process', // Native Bun processes
35
+ token: 'YOUR_BOT_TOKEN',
36
+ });
37
+
38
+ // Extend with Enterprise features
39
+ manager.extend(
40
+ new ReClusterManager(),
41
+ new HeartbeatManager({
42
+ redis: { host: 'localhost', port: 6379 },
43
+ interval: 10000,
44
+ }),
45
+ new DashboardServer({ port: 3001 })
46
+ );
47
+
48
+ manager.on('clusterCreate', (cluster) => console.log(`🚀 Launched Cluster ${cluster.id}`));
49
+ manager.spawn();
50
+ ```
51
+
52
+ ### 2. The Client (`bot.js`)
53
+
54
+ ```js
55
+ import { ClusterClient, getInfo } from 'buncord-hybrid-sharding';
56
+ import { Client, GatewayIntentBits } from 'discord.js';
57
+
58
+ const client = new Client({
59
+ shards: getInfo().SHARD_LIST,
60
+ shardCount: getInfo().TOTAL_SHARDS,
61
+ intents: [GatewayIntentBits.Guilds],
62
+ });
63
+
64
+ client.cluster = new ClusterClient(client);
65
+
66
+ client.on('ready', () => {
67
+ client.cluster.triggerReady();
68
+ console.log(`✅ Cluster ${client.cluster.id} is ready!`);
69
+ });
70
+
71
+ client.login('YOUR_BOT_TOKEN');
72
+ ```
73
+
74
+ ---
75
+
76
+ ## 📈 Monitoring API
77
+
78
+ The built-in `DashboardServer` provides a JSON API for monitoring and management:
79
+
80
+ - `GET /stats`: Unified metrics across all clusters.
81
+ - `POST /restart`: Trigger a rolling restart.
82
+ - `POST /maintenance`: Toggle maintenance mode.
83
+
84
+ ---
85
+
86
+ ## 📝 License
87
+
88
+ This project is licensed under the MIT License. See the [LICENSE](file:///LICENSE) file for details.
89
+ Portions of this code are based on `discord.js` and `discord-hybrid-sharding`, copyright of their respective authors.
90
+
91
+ Developed with ❤️ by [Luigi Colantuono](https://github.com/LuigiColantuono).
package/bun.lock ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "buncord-hybrid-sharding",
7
+ "dependencies": {
8
+ "discord.js": "^14.25.1",
9
+ "redis": "^5.1.0",
10
+ },
11
+ "devDependencies": {
12
+ "@types/bun": "latest",
13
+ "typescript": "^5.9.3",
14
+ },
15
+ },
16
+ },
17
+ "packages": {
18
+ "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="],
19
+
20
+ "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
21
+
22
+ "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="],
23
+
24
+ "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="],
25
+
26
+ "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
27
+
28
+ "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
29
+
30
+ "@redis/bloom": ["@redis/bloom@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A=="],
31
+
32
+ "@redis/client": ["@redis/client@5.10.0", "", { "dependencies": { "cluster-key-slot": "1.1.2" } }, "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA=="],
33
+
34
+ "@redis/json": ["@redis/json@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA=="],
35
+
36
+ "@redis/search": ["@redis/search@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg=="],
37
+
38
+ "@redis/time-series": ["@redis/time-series@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg=="],
39
+
40
+ "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
41
+
42
+ "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
43
+
44
+ "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
45
+
46
+ "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
47
+
48
+ "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
49
+
50
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
51
+
52
+ "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
53
+
54
+ "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
55
+
56
+ "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
57
+
58
+ "discord-api-types": ["discord-api-types@0.38.37", "", {}, "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w=="],
59
+
60
+ "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="],
61
+
62
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
63
+
64
+ "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
65
+
66
+ "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
67
+
68
+ "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="],
69
+
70
+ "redis": ["redis@5.10.0", "", { "dependencies": { "@redis/bloom": "5.10.0", "@redis/client": "5.10.0", "@redis/json": "5.10.0", "@redis/search": "5.10.0", "@redis/time-series": "5.10.0" } }, "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw=="],
71
+
72
+ "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
73
+
74
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
75
+
76
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
77
+
78
+ "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
79
+
80
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
81
+
82
+ "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
83
+
84
+ "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
85
+
86
+ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
87
+ }
88
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "buncord-hybrid-sharding",
3
+ "version": "1.0.0",
4
+ "description": "Enterprise Bun-native sharding manager for Discord bots, featuring Redis heartbeats, rolling restarts, and integrated monitoring.",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "test": "bun test",
10
+ "format": "bun format"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/LuigiColantuono/buncord-hybrid-sharding.git"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "discord",
21
+ "bun",
22
+ "sharding",
23
+ "clustering",
24
+ "hybrid-sharding",
25
+ "zero-downtime",
26
+ "redis"
27
+ ],
28
+ "author": "Luigi Colantuono",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "discord.js": "^14.25.1",
32
+ "redis": "^5.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "latest",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }
39
+
@@ -0,0 +1,414 @@
1
+ import EventEmitter from "node:events";
2
+ import path from "node:path";
3
+
4
+ import { Child } from "../Structures/Child.js";
5
+ import { ClusterHandler } from "../Structures/IPCHandler.js";
6
+ import { BaseMessage, IPCMessage, RawMessage } from "../Structures/IPCMessage.js";
7
+ import { ClusterEvents, ClusterKillOptions, DjsDiscordClient, messageType } from "../types/shared.js"; // eslint-disable-line @typescript-eslint/no-unused-vars
8
+ import { delayFor, generateNonce } from "../Util/Util.js";
9
+ import { ClusterManager } from "./ClusterManager.js";
10
+
11
+ /**
12
+ * A self-contained cluster created by the {@link ClusterManager}. Each one has a {@link DjsDiscordClient} that contains
13
+ * an instance of the bot and its {@link DjsDiscordClient}. When its child process exits for any reason, the cluster will
14
+ * spawn a new one to replace it as necessary.
15
+ * @augments EventEmitter
16
+ */
17
+ export class Cluster extends EventEmitter {
18
+ /**
19
+ * Manager that created the cluster
20
+ */
21
+ manager: ClusterManager;
22
+
23
+ /**
24
+ * ID of the cluster in the manager
25
+ */
26
+ id: number;
27
+
28
+ /**
29
+ * Arguments for the shard's process
30
+ */
31
+ args: string[];
32
+
33
+ /**
34
+ * Arguments for the shard's process executable
35
+ */
36
+ execArgv: string[];
37
+
38
+ /**
39
+ * Internal Shards which will get spawned in the cluster
40
+ */
41
+ shardList: number[];
42
+
43
+ /**
44
+ * the amount of real shards
45
+ */
46
+ totalShards: number;
47
+
48
+ /**
49
+ * Environment variables for the cluster's process
50
+ */
51
+ env: Record<string, any> & {
52
+ SHARD_LIST: number[];
53
+ TOTAL_SHARDS: number;
54
+ CLUSTER_MANAGER: boolean;
55
+ CLUSTER: number;
56
+ CLUSTER_COUNT: number;
57
+ DISCORD_TOKEN: string;
58
+ };
59
+
60
+ /**
61
+ * Process of the cluster
62
+ */
63
+ thread: null | Child;
64
+
65
+ restarts: {
66
+ current: number;
67
+ max: number;
68
+ interval: number;
69
+ reset?: any;
70
+ resetRestarts: () => void;
71
+ cleanup: () => void;
72
+ append: () => void;
73
+ };
74
+
75
+ messageHandler: any;
76
+
77
+ /**
78
+ * Whether the cluster's {@link DjsDiscordClient} is ready
79
+ */
80
+ ready: boolean;
81
+
82
+ /**
83
+ * @param manager Manager that is creating this cluster
84
+ * @param id ID of this cluster
85
+ * @param shardList
86
+ * @param totalShards
87
+ */
88
+ constructor(manager: ClusterManager, id: number, shardList: number[], totalShards: number) {
89
+ super();
90
+
91
+ this.manager = manager;
92
+
93
+ this.id = id;
94
+
95
+ this.args = manager.shardArgs || [];
96
+
97
+ this.execArgv = manager.execArgv;
98
+
99
+ this.shardList = shardList;
100
+
101
+ this.totalShards = totalShards;
102
+
103
+ this.env = Object.assign({}, process.env, {
104
+ SHARD_LIST: this.shardList,
105
+ TOTAL_SHARDS: this.totalShards,
106
+ CLUSTER_MANAGER: true,
107
+ CLUSTER: this.id,
108
+ CLUSTER_COUNT: this.manager.totalClusters,
109
+ DISCORD_TOKEN: this.manager.token as string,
110
+ });
111
+
112
+ this.ready = false;
113
+
114
+ this.thread = null;
115
+
116
+ this.restarts = {
117
+ current: this.manager.restarts.current ?? 0,
118
+ max: this.manager.restarts.max,
119
+ interval: this.manager.restarts.interval,
120
+ reset: undefined,
121
+ resetRestarts: () => {
122
+ this.restarts.reset = setInterval(() => {
123
+ this.restarts.current = 0;
124
+ }, this.manager.restarts.interval);
125
+ },
126
+ cleanup: () => {
127
+ if (this.restarts.reset) clearInterval(this.restarts.reset);
128
+ },
129
+ append: () => {
130
+ this.restarts.current++;
131
+ },
132
+ };
133
+ }
134
+ /**
135
+ * Spawns a child process for the cluster.
136
+ * <warn>You should not need to call this manually.</warn>
137
+ * @param spawnTimeout The amount in milliseconds to wait until the {@link DjsDiscordClient} has become ready
138
+ * before resolving. (-1 or Infinity for no wait)
139
+ */
140
+ public async spawn(spawnTimeout = -1) {
141
+ if (this.thread) throw new Error('CLUSTER ALREADY SPAWNED | ClusterId: ' + this.id);
142
+ this.thread = new Child(path.resolve(this.manager.file), {
143
+ ...this.manager.clusterOptions,
144
+ env: this.env as any,
145
+ /** Construct args with hooks, to provide parameters with in the context of a cluster */
146
+ args: this.manager.hooks.constructClusterArgs(this, this.args),
147
+ clusterData: { ...this.env, ...this.manager.clusterData },
148
+ });
149
+ this.messageHandler = new ClusterHandler(this.manager, this, this.thread);
150
+
151
+ this.thread
152
+ .spawn()
153
+ .on('message', this._handleMessage.bind(this))
154
+ .on('exit', this._handleExit.bind(this))
155
+ .on('error', this._handleError.bind(this));
156
+
157
+ /**
158
+ * Emitted upon the creation of the cluster's child process.
159
+ * @event Cluster#spawn
160
+ * @param {Child} process Child process that was created
161
+ */
162
+ this.emit('spawn', this.thread.process);
163
+
164
+ await new Promise((resolve, reject) => {
165
+ let spawnTimeoutTimer: any | undefined = undefined;
166
+
167
+ const cleanup = (death = false) => {
168
+ clearTimeout(spawnTimeoutTimer);
169
+
170
+ // Remove listeners if cluster died to prevent event emitter leaks
171
+ if (death) {
172
+ this.off('ready', onReady);
173
+ this.off('death', onDeath);
174
+ }
175
+ };
176
+
177
+ const onReady = () => {
178
+ this.manager.emit('clusterReady', this);
179
+ this.restarts.cleanup();
180
+ this.restarts.resetRestarts();
181
+ cleanup();
182
+ resolve('Cluster is ready');
183
+ };
184
+
185
+ const onDeath = () => {
186
+ cleanup(true);
187
+ reject(new Error('CLUSTERING_READY_DIED | ClusterId: ' + this.id));
188
+ };
189
+
190
+ const onTimeout = () => {
191
+ cleanup();
192
+ reject(new Error('CLUSTERING_READY_TIMEOUT | ClusterId: ' + this.id));
193
+ };
194
+
195
+ // If there is a spawn timeout wait and error if cluster does not get ready
196
+ if (spawnTimeout !== -1 && spawnTimeout !== Infinity) {
197
+ spawnTimeoutTimer = setTimeout(onTimeout, spawnTimeout);
198
+ } else {
199
+ // No timeout, next cluster will be spawned, without waiting for ready
200
+ resolve('Skipping ready check');
201
+ }
202
+
203
+ this.once('ready', onReady);
204
+ this.once('death', onDeath);
205
+ });
206
+ return this.thread.process;
207
+ }
208
+ /**
209
+ * Immediately kills the clusters process and does not restart it.
210
+ * @param options Some Options for managing the Kill
211
+ * @param options.force Whether the Cluster should be force kill and be ever respawned...
212
+ */
213
+ public kill(options: ClusterKillOptions) {
214
+ this.thread?.kill();
215
+ if (this.thread) {
216
+ this.thread = null;
217
+ }
218
+ // Heartbeat will automatically detect death via Redis TTL Key expiration
219
+ this.restarts.cleanup();
220
+ this.manager._debug('[KILL] Cluster killed with reason: ' + (options?.reason || 'not given'), this.id);
221
+ }
222
+ /**
223
+ * Kills and restarts the cluster's process.
224
+ * @param options Options for respawning the cluster
225
+ */
226
+ public async respawn({ delay = 5500, timeout = -1 } = this.manager.spawnOptions) {
227
+ if (this.thread) this.kill({ force: true });
228
+ if (delay > 0) await delayFor(delay);
229
+ // Heartbeat will automatically detect death via Redis TTL Key expiration
230
+ return this.spawn(timeout);
231
+ }
232
+ /**
233
+ * Sends a message to the cluster's process.
234
+ * @param message Message to send to the cluster
235
+ */
236
+ public send(message: RawMessage) {
237
+ if (typeof message === 'object') this.thread?.send(new BaseMessage(message).toJSON());
238
+ else return this.thread?.send(message);
239
+ }
240
+
241
+ /**
242
+ * Sends a Request to the ClusterClient and returns the reply
243
+ * @param message Message, which should be sent as request
244
+ * @returns Reply of the Message
245
+ * @example
246
+ * client.cluster.request({content: 'hello'})
247
+ * .then(result => console.log(result)) //hi
248
+ * .catch(console.error);
249
+ * @see {@link IPCMessage#reply}
250
+ */
251
+ public request(message: RawMessage) {
252
+ message._type = messageType.CUSTOM_REQUEST;
253
+ this.send(message);
254
+ return this.manager.promise.create(message, message.options);
255
+ }
256
+ /**
257
+ * Evaluates a script or function on the cluster, in the context of the {@link DjsDiscordClient}.
258
+ * @param script JavaScript to run on the cluster
259
+ * @param context
260
+ * @param timeout
261
+ * @returns Result of the script execution
262
+ */
263
+ public async eval(script: string, context: any, timeout: number) {
264
+ // Stringify the script if it's a Function
265
+ const _eval = typeof script === 'function' ? `(${script})(this, ${JSON.stringify(context)})` : script;
266
+
267
+ // cluster is dead (maybe respawning), don't cache anything and error immediately
268
+ if (!this.thread) return Promise.reject(new Error('CLUSTERING_NO_CHILD_EXISTS | ClusterId: ' + this.id));
269
+ const nonce = generateNonce();
270
+ const message = { nonce, _eval, options: { timeout }, _type: messageType.CLIENT_EVAL_REQUEST };
271
+ await this.send(message);
272
+ return await this.manager.promise.create(message, message.options);
273
+ }
274
+
275
+ /**
276
+ * @param reason If maintenance should be enabled with a given reason or disabled when nonce provided
277
+ */
278
+ public triggerMaintenance(reason?: string) {
279
+ const _type = reason ? messageType.CLIENT_MAINTENANCE_ENABLE : messageType.CLIENT_MAINTENANCE_DISABLE;
280
+ return this.send({ _type, maintenance: reason });
281
+ }
282
+
283
+ /**
284
+ * Handles a message received from the child process.
285
+ * @param message Message received
286
+ * @private
287
+ */
288
+ private _handleMessage(message: any) {
289
+ if (!message) return;
290
+ const emit = this.messageHandler.handleMessage(message);
291
+ if (!emit) return;
292
+
293
+ let emitMessage;
294
+ if (typeof message === 'object') {
295
+ emitMessage = new IPCMessage(this, message);
296
+ if (emitMessage._type === messageType.CUSTOM_REQUEST) this.manager.emit('clientRequest', emitMessage);
297
+ } else emitMessage = message;
298
+ /**
299
+ * Emitted upon receiving a message from the child process.
300
+ * @event Shard#message
301
+ * @param {*|IPCMessage} message Message that was received
302
+ */
303
+ this.emit('message', emitMessage);
304
+ }
305
+
306
+ /**
307
+ * Handles the cluster's process exiting.
308
+ * @private
309
+ * @param {Number} exitCode
310
+ */
311
+ private _handleExit(exitCode: number) { // eslint-disable-line @typescript-eslint/no-unused-vars
312
+ /**
313
+ * Emitted upon the cluster's child process exiting.
314
+ * @event Cluster#death
315
+ * @param {Child} process Child process that exited
316
+ */
317
+
318
+ const respawn = this.manager.respawn;
319
+
320
+ // Cleanup functions
321
+ this.manager.heartbeat?.stop(); // This is just a placeholder if needed, but actually the manager-side heartbeat stops automatically or doesn't need per-cluster stop anymore.
322
+ this.restarts.cleanup();
323
+
324
+ this.emit('death', this, this.thread?.process);
325
+
326
+ this.manager._debug(
327
+ '[DEATH] Cluster died, attempting respawn | Restarts Left: ' + (this.restarts.max - this.restarts.current),
328
+ this.id,
329
+ );
330
+
331
+ this.ready = false;
332
+
333
+ this.thread = null;
334
+
335
+ if (!respawn) return;
336
+
337
+ if (this.restarts.current >= this.restarts.max)
338
+ this.manager._debug(
339
+ '[ATTEMPTED_RESPAWN] Attempted Respawn Declined | Max Restarts have been exceeded',
340
+ this.id,
341
+ );
342
+ if (respawn && this.restarts.current < this.restarts.max) this.spawn().catch(err => this.emit('error', err));
343
+
344
+ this.restarts.append();
345
+ }
346
+
347
+ /**
348
+ * Handles the cluster's process error.
349
+ * @param error the error, which occurred on the child process
350
+ * @private
351
+ */
352
+ private _handleError(error: Error) {
353
+ /**
354
+ * Emitted upon the cluster's child process error.
355
+ * @event Cluster#error
356
+ * @param {Child} process Child process, where error occurred
357
+ */
358
+ this.manager.emit('error', error);
359
+ }
360
+ }
361
+
362
+ // Credits for EventEmitter typings: https://github.com/discordjs/discord.js/blob/main/packages/rest/src/lib/RequestManager.ts#L159 | See attached license
363
+ export interface Cluster {
364
+ emit: (<K extends keyof ClusterEvents>(event: K, ...args: ClusterEvents[K]) => boolean) &
365
+ (<S extends string | symbol>(event: Exclude<S, keyof ClusterEvents>, ...args: any[]) => boolean);
366
+
367
+ off: (<K extends keyof ClusterEvents>(event: K, listener: (...args: ClusterEvents[K]) => void) => this) &
368
+ (<S extends string | symbol>(
369
+ event: Exclude<S, keyof ClusterEvents>,
370
+ listener: (...args: any[]) => void,
371
+ ) => this);
372
+
373
+ on: (<K extends keyof ClusterEvents>(event: K, listener: (...args: ClusterEvents[K]) => void) => this) &
374
+ (<S extends string | symbol>(
375
+ event: Exclude<S, keyof ClusterEvents>,
376
+ listener: (...args: any[]) => void,
377
+ ) => this);
378
+
379
+ once: (<K extends keyof ClusterEvents>(event: K, listener: (...args: ClusterEvents[K]) => void) => this) &
380
+ (<S extends string | symbol>(
381
+ event: Exclude<S, keyof ClusterEvents>,
382
+ listener: (...args: any[]) => void,
383
+ ) => this);
384
+
385
+ removeAllListeners: (<K extends keyof ClusterEvents>(event?: K) => this) &
386
+ (<S extends string | symbol>(event?: Exclude<S, keyof ClusterEvents>) => this);
387
+ }
388
+
389
+ // Credits for EventEmitter typings: https://github.com/discordjs/discord.js/blob/main/packages/rest/src/lib/RequestManager.ts#L159 | See attached license
390
+ export interface Cluster {
391
+ emit: (<K extends keyof ClusterEvents>(event: K, ...args: ClusterEvents[K]) => boolean) &
392
+ (<S extends string | symbol>(event: Exclude<S, keyof ClusterEvents>, ...args: any[]) => boolean);
393
+
394
+ off: (<K extends keyof ClusterEvents>(event: K, listener: (...args: ClusterEvents[K]) => void) => this) &
395
+ (<S extends string | symbol>(
396
+ event: Exclude<S, keyof ClusterEvents>,
397
+ listener: (...args: any[]) => void,
398
+ ) => this);
399
+
400
+ on: (<K extends keyof ClusterEvents>(event: K, listener: (...args: ClusterEvents[K]) => void) => this) &
401
+ (<S extends string | symbol>(
402
+ event: Exclude<S, keyof ClusterEvents>,
403
+ listener: (...args: any[]) => void,
404
+ ) => this);
405
+
406
+ once: (<K extends keyof ClusterEvents>(event: K, listener: (...args: ClusterEvents[K]) => void) => this) &
407
+ (<S extends string | symbol>(
408
+ event: Exclude<S, keyof ClusterEvents>,
409
+ listener: (...args: any[]) => void,
410
+ ) => this);
411
+
412
+ removeAllListeners: (<K extends keyof ClusterEvents>(event?: K) => this) &
413
+ (<S extends string | symbol>(event?: Exclude<S, keyof ClusterEvents>) => this);
414
+ }