kythia-core 0.9.5-beta → 0.10.1-beta

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.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/ShutdownManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.5-beta
7
+ * @version 0.10.0-beta
8
8
  *
9
9
  * @description
10
10
  * Handles graceful shutdown procedures including interval tracking,
@@ -14,184 +14,205 @@
14
14
  const exitHook = require('async-exit-hook');
15
15
 
16
16
  class ShutdownManager {
17
- /**
18
- * 🏗️ ShutdownManager Constructor
19
- * @param {Object} client - Discord client instance
20
- * @param {Object} container - Dependency container
21
- */
22
- constructor({ client, container }) {
23
- this.client = client;
24
- this.container = container;
25
- this._activeIntervals = new Set();
26
- this._messagesWithActiveCollectors = new Set();
27
- this._collectorPatched = false;
28
- this._cleanupAttached = false;
29
-
30
- this.logger = this.container.logger;
31
- }
32
-
33
- /**
34
- * 🕵️‍♂️ [GLOBAL PATCH] Overrides global interval functions to track all active intervals.
35
- * This allows for a truly generic and scalable graceful shutdown of all timed tasks.
36
- */
37
- initializeGlobalIntervalTracker() {
38
- if (!this._activeIntervals) this._activeIntervals = new Set();
39
-
40
- const botInstance = this;
41
- const originalSetInterval = global.setInterval;
42
- const originalClearInterval = global.clearInterval;
43
-
44
- global.setInterval = function (...args) {
45
- const intervalId = originalSetInterval.apply(this, args);
46
-
47
- botInstance._activeIntervals.add(intervalId);
48
- return intervalId;
49
- };
50
-
51
- global.clearInterval = function (intervalId) {
52
- originalClearInterval.apply(this, [intervalId]);
53
-
54
- botInstance._activeIntervals.delete(intervalId);
55
- };
56
-
57
- this.logger.info('✅ Global setInterval/clearInterval has been patched for tracking.');
58
- }
59
-
60
- /**
61
- * 🛑 [FINAL ARCHITECTURE v5] Manages ALL graceful shutdown procedures.
62
- * This version patches the core message sending/editing methods to automatically
63
- * track ANY message with components, regardless of how its interactions are handled.
64
- */
65
- initializeShutdownCollectors() {
66
- if (!this._messagesWithActiveCollectors) this._messagesWithActiveCollectors = new Set();
67
-
68
- if (!this._collectorPatched) {
69
- const origCreateCollector = require('discord.js').Message.prototype.createMessageComponentCollector;
70
- const botInstance = this;
71
-
72
- require('discord.js').Message.prototype.createMessageComponentCollector = function (...args) {
73
- const collector = origCreateCollector.apply(this, args);
74
- const message = this;
75
-
76
- if (botInstance._messagesWithActiveCollectors) {
77
- botInstance._messagesWithActiveCollectors.add(message);
78
- }
79
-
80
- collector.once('end', () => {
81
- if (botInstance._messagesWithActiveCollectors) {
82
- botInstance._messagesWithActiveCollectors.delete(message);
83
- }
84
- });
85
-
86
- return collector;
87
- };
88
- this._collectorPatched = true;
89
- this.logger.info('✅ Corrected collector-based component tracking has been patched.');
90
- }
91
-
92
- if (!this._cleanupAttached) {
93
- const cleanupAndFlush = async (callback) => {
94
- this.logger.info('🛑 Graceful shutdown initiated...');
95
-
96
- if (this._activeIntervals && this._activeIntervals.size > 0) {
97
- this.logger.info(`🛑 Halting ${this._activeIntervals.size} active global intervals...`);
98
- for (const intervalId of this._activeIntervals) {
99
- clearInterval(intervalId);
100
- }
101
- }
102
-
103
- const messagesToProcess = this._messagesWithActiveCollectors;
104
-
105
- if (messagesToProcess && messagesToProcess.size > 0) {
106
- this.logger.info(`🛑 Disabling components on up to ${messagesToProcess.size} messages.`);
107
- const editPromises = [];
108
-
109
- function disableRecursively(components) {
110
- return components.map((comp) => {
111
- if (comp.components && Array.isArray(comp.components)) {
112
- comp.components = disableRecursively(comp.components);
113
- }
114
-
115
- if (comp.type === 2 || comp.type === 3 || comp.type >= 5) {
116
- return { ...comp, disabled: true };
117
- }
118
- return comp;
119
- });
120
- }
121
-
122
- for (const msg of messagesToProcess) {
123
- if (!msg.editable || !msg.components || msg.components.length === 0) continue;
124
- try {
125
- const rawComponents = msg.components.map((c) => c.toJSON());
126
- const disabledComponents = disableRecursively(rawComponents);
127
- editPromises.push(msg.edit({ components: disabledComponents }).catch(() => {}));
128
- } catch (e) {}
129
- }
130
- await Promise.allSettled(editPromises);
131
- }
132
- this.logger.info('✅ Component cleanup complete.');
133
-
134
- this.logger.info('🚰 Flushing remaining logs...');
135
- this.logger.on('finish', () => {
136
- console.log('⏳ Logger has flushed. Kythia is now safely shutting down.');
137
- if (callback) callback();
138
- });
139
- this.logger.end();
140
- setTimeout(() => {
141
- console.log('⏳ Logger flush timeout. Forcing exit.');
142
- if (callback) callback();
143
- }, 4000);
144
- };
145
-
146
- exitHook(cleanupAndFlush);
147
- process.on('unhandledRejection', (error) => {
148
- this.logger.error('‼️ UNHANDLED PROMISE REJECTION:', error);
149
- });
150
- process.on('uncaughtException', (error) => {
151
- this.logger.error('‼️ UNCAUGHT EXCEPTION! Bot will shutdown.', error);
152
- process.exit(1);
153
- });
154
-
155
- this._cleanupAttached = true;
156
- this.logger.info('🛡️ Graceful shutdown and error handlers are now active.');
157
- }
158
- }
159
-
160
- /**
161
- * Initialize all shutdown procedures
162
- */
163
- initialize() {
164
- this.initializeGlobalIntervalTracker();
165
- this.initializeShutdownCollectors();
166
- }
167
-
168
- /**
169
- * Get active intervals count
170
- * @returns {number} Number of active intervals
171
- */
172
- getActiveIntervalsCount() {
173
- return this._activeIntervals ? this._activeIntervals.size : 0;
174
- }
175
-
176
- /**
177
- * Get messages with active collectors count
178
- * @returns {number} Number of messages with active collectors
179
- */
180
- getActiveCollectorsCount() {
181
- return this._messagesWithActiveCollectors ? this._messagesWithActiveCollectors.size : 0;
182
- }
183
-
184
- /**
185
- * Force cleanup (for testing purposes)
186
- */
187
- forceCleanup() {
188
- if (this._activeIntervals) {
189
- for (const intervalId of this._activeIntervals) {
190
- clearInterval(intervalId);
191
- }
192
- this._activeIntervals.clear();
193
- }
194
- }
17
+ /**
18
+ * 🏗️ ShutdownManager Constructor
19
+ * @param {Object} client - Discord client instance
20
+ * @param {Object} container - Dependency container
21
+ */
22
+ constructor({ client, container }) {
23
+ this.client = client;
24
+ this.container = container;
25
+ this._activeIntervals = new Set();
26
+ this._messagesWithActiveCollectors = new Set();
27
+ this._collectorPatched = false;
28
+ this._cleanupAttached = false;
29
+
30
+ this.logger = this.container.logger;
31
+ }
32
+
33
+ /**
34
+ * 🕵️‍♂️ [GLOBAL PATCH] Overrides global interval functions to track all active intervals.
35
+ * This allows for a truly generic and scalable graceful shutdown of all timed tasks.
36
+ */
37
+ initializeGlobalIntervalTracker() {
38
+ if (!this._activeIntervals) this._activeIntervals = new Set();
39
+
40
+ const botInstance = this;
41
+ const originalSetInterval = global.setInterval;
42
+ const originalClearInterval = global.clearInterval;
43
+
44
+ global.setInterval = function (...args) {
45
+ const intervalId = originalSetInterval.apply(this, args);
46
+
47
+ botInstance._activeIntervals.add(intervalId);
48
+ return intervalId;
49
+ };
50
+
51
+ global.clearInterval = function (intervalId) {
52
+ originalClearInterval.apply(this, [intervalId]);
53
+
54
+ botInstance._activeIntervals.delete(intervalId);
55
+ };
56
+
57
+ this.logger.info(
58
+ '✅ Global setInterval/clearInterval has been patched for tracking.',
59
+ );
60
+ }
61
+
62
+ disableRecursively(components) {
63
+ return components.map((comp) => {
64
+ if (comp.components && Array.isArray(comp.components)) {
65
+ comp.components = disableRecursively(comp.components);
66
+ }
67
+
68
+ if (comp.type === 2 || comp.type === 3 || comp.type >= 5) {
69
+ return { ...comp, disabled: true };
70
+ }
71
+ return comp;
72
+ });
73
+ }
74
+
75
+ /**
76
+ * 🛑 [FINAL ARCHITECTURE v5] Manages ALL graceful shutdown procedures.
77
+ * This version patches the core message sending/editing methods to automatically
78
+ * track ANY message with components, regardless of how its interactions are handled.
79
+ */
80
+ initializeShutdownCollectors() {
81
+ if (!this._messagesWithActiveCollectors)
82
+ this._messagesWithActiveCollectors = new Set();
83
+
84
+ if (!this._collectorPatched) {
85
+ const origCreateCollector =
86
+ require('discord.js').Message.prototype.createMessageComponentCollector;
87
+ const botInstance = this;
88
+
89
+ require('discord.js').Message.prototype.createMessageComponentCollector =
90
+ function (...args) {
91
+ const collector = origCreateCollector.apply(this, args);
92
+
93
+ if (botInstance._messagesWithActiveCollectors) {
94
+ botInstance._messagesWithActiveCollectors.add(this);
95
+ }
96
+
97
+ collector.once('end', () => {
98
+ if (botInstance._messagesWithActiveCollectors) {
99
+ botInstance._messagesWithActiveCollectors.delete(this);
100
+ }
101
+ });
102
+
103
+ return collector;
104
+ };
105
+ this._collectorPatched = true;
106
+ this.logger.info(
107
+ '✅ Corrected collector-based component tracking has been patched.',
108
+ );
109
+ }
110
+
111
+ if (!this._cleanupAttached) {
112
+ const cleanupAndFlush = async (callback) => {
113
+ this.logger.info('🛑 Graceful shutdown initiated...');
114
+
115
+ if (this._activeIntervals && this._activeIntervals.size > 0) {
116
+ this.logger.info(
117
+ `🛑 Halting ${this._activeIntervals.size} active global intervals...`,
118
+ );
119
+ for (const intervalId of this._activeIntervals) {
120
+ clearInterval(intervalId);
121
+ }
122
+ }
123
+
124
+ const messagesToProcess = this._messagesWithActiveCollectors;
125
+
126
+ if (messagesToProcess && messagesToProcess.size > 0) {
127
+ this.logger.info(
128
+ `🛑 Disabling components on up to ${messagesToProcess.size} messages.`,
129
+ );
130
+ const editPromises = [];
131
+
132
+ for (const msg of messagesToProcess) {
133
+ if (!msg.editable || !msg.components || msg.components.length === 0)
134
+ continue;
135
+ try {
136
+ const rawComponents = msg.components.map((c) => c.toJSON());
137
+ const disabledComponents = disableRecursively(rawComponents);
138
+ editPromises.push(
139
+ msg.edit({ components: disabledComponents }).catch(() => {}),
140
+ );
141
+ } catch (error) {
142
+ this.logger.error(error);
143
+ }
144
+ }
145
+ await Promise.allSettled(editPromises);
146
+ }
147
+ this.logger.info(' Component cleanup complete.');
148
+
149
+ this.logger.info('🚰 Flushing remaining logs...');
150
+ this.logger.on('finish', () => {
151
+ console.log(
152
+ '⏳ Logger has flushed. Kythia is now safely shutting down.',
153
+ );
154
+ if (callback) callback();
155
+ });
156
+ this.logger.end();
157
+ setTimeout(() => {
158
+ console.log('⏳ Logger flush timeout. Forcing exit.');
159
+ if (callback) callback();
160
+ }, 4000);
161
+ };
162
+
163
+ exitHook(cleanupAndFlush);
164
+ process.on('unhandledRejection', (error) => {
165
+ this.logger.error('‼️ UNHANDLED PROMISE REJECTION:', error);
166
+ });
167
+ process.on('uncaughtException', (error) => {
168
+ this.logger.error('‼️ UNCAUGHT EXCEPTION! Bot will shutdown.', error);
169
+ process.exit(1);
170
+ });
171
+
172
+ this._cleanupAttached = true;
173
+ this.logger.info(
174
+ '🛡️ Graceful shutdown and error handlers are now active.',
175
+ );
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Initialize all shutdown procedures
181
+ */
182
+ initialize() {
183
+ this.initializeGlobalIntervalTracker();
184
+ this.initializeShutdownCollectors();
185
+ }
186
+
187
+ /**
188
+ * Get active intervals count
189
+ * @returns {number} Number of active intervals
190
+ */
191
+ getActiveIntervalsCount() {
192
+ return this._activeIntervals ? this._activeIntervals.size : 0;
193
+ }
194
+
195
+ /**
196
+ * Get messages with active collectors count
197
+ * @returns {number} Number of messages with active collectors
198
+ */
199
+ getActiveCollectorsCount() {
200
+ return this._messagesWithActiveCollectors
201
+ ? this._messagesWithActiveCollectors.size
202
+ : 0;
203
+ }
204
+
205
+ /**
206
+ * Force cleanup (for testing purposes)
207
+ */
208
+ forceCleanup() {
209
+ if (this._activeIntervals) {
210
+ for (const intervalId of this._activeIntervals) {
211
+ clearInterval(intervalId);
212
+ }
213
+ this._activeIntervals.clear();
214
+ }
215
+ }
195
216
  }
196
217
 
197
218
  module.exports = ShutdownManager;
@@ -4,46 +4,50 @@
4
4
  * @description Base class for all command types (slash commands, context menus, etc.)
5
5
  */
6
6
  class BaseCommand {
7
- /**
8
- * @param {Object} container - Dependency injection container
9
- */
10
- constructor(container) {
11
- if (!container) {
12
- throw new Error('Container is required for BaseCommand');
13
- }
7
+ /**
8
+ * @param {Object} container - Dependency injection container
9
+ */
10
+ constructor(container) {
11
+ if (!container) {
12
+ throw new Error('Container is required for BaseCommand');
13
+ }
14
14
 
15
- this.container = container;
16
- this.client = container.client;
17
- this.logger = container.logger;
18
- this.t = container.t;
19
- this.models = container.models;
20
- this.kythiaConfig = container.kythiaConfig;
21
- this.helpers = container.helpers;
15
+ this.container = container;
16
+ this.client = container.client;
17
+ this.logger = container.logger;
18
+ this.t = container.t;
19
+ this.models = container.models;
20
+ this.kythiaConfig = container.kythiaConfig;
21
+ this.helpers = container.helpers;
22
22
 
23
- this.data = {
24
- name: 'base-command',
25
- description: 'Base command description',
26
- cooldown: 10,
27
- permissions: [],
28
- ownerOnly: false,
29
- teamOnly: false,
30
- guildOnly: false,
31
- };
32
- }
23
+ this.data = {
24
+ name: 'base-command',
25
+ description: 'Base command description',
26
+ cooldown: 10,
27
+ permissions: [],
28
+ ownerOnly: false,
29
+ teamOnly: false,
30
+ guildOnly: false,
31
+ };
32
+ }
33
33
 
34
- /**
35
- * Main command execution method (must be implemented by child classes)
36
- * @param {import('discord.js').CommandInteraction} interaction - The interaction object
37
- * @returns {Promise<void>}
38
- */
39
- async execute(interaction) {
40
- if (interaction.options?.getSubcommand?.(false)) {
41
- this.logger.warn(`Command group ${this.constructor.name} execute called - this should be handled by subcommand`);
42
- return;
43
- }
34
+ /**
35
+ * Main command execution method (must be implemented by child classes)
36
+ * @param {import('discord.js').CommandInteraction} interaction - The interaction object
37
+ * @returns {Promise<void>}
38
+ */
39
+ async execute(interaction) {
40
+ if (interaction.options?.getSubcommand?.(false)) {
41
+ this.logger.warn(
42
+ `Command group ${this.constructor.name} execute called - this should be handled by subcommand`,
43
+ );
44
+ return;
45
+ }
44
46
 
45
- throw new Error(`Execute method not implemented for ${this.constructor.name}`);
46
- }
47
+ throw new Error(
48
+ `Execute method not implemented for ${this.constructor.name}`,
49
+ );
50
+ }
47
51
  }
48
52
 
49
53
  module.exports = BaseCommand;