kythia-core 0.9.3-beta → 0.9.4-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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kythia-core",
3
- "version": "0.9.3-beta",
3
+ "version": "0.9.4-beta",
4
4
  "description": "Core library for the Kythia main Discord bot: extensible, modular, and scalable foundation for commands, components, and event management.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Kythia.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @file src/Kythia.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * This file contains the main Bot class - acting as an orchestrator (CEO) that
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaModel.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * Caching layer for Sequelize Models, now sharding-aware. When config.db.redis.shard === true,
@@ -16,6 +16,7 @@
16
16
  * - Shard Mode: If using Redis sharding, disables Map fallback for strict consistency.
17
17
  * - Hybrid Fallback: For non-shard setups, automatic fallback is preserved.
18
18
  * - Fast, consistent, safe cache busting.
19
+ * - Multi-Redis Fallback: Support multiple Redis URLs for failover/fallback. Will try connect to next Redis if one fails.
19
20
  */
20
21
 
21
22
  const jsonStringify = require('json-stable-stringify');
@@ -68,7 +69,11 @@ class KythiaModel extends Model {
68
69
 
69
70
  static redisErrorTimestamps = [];
70
71
 
71
- static isShardMode = false; // when true, local fallback is disabled
72
+ static isShardMode = false;
73
+
74
+ static _redisFallbackURLs = [];
75
+ static _redisCurrentIndex = 0;
76
+ static _redisFailedIndexes = new Set();
72
77
 
73
78
  /**
74
79
  * 💉 Injects core dependencies into the KythiaModel class.
@@ -77,7 +82,8 @@ class KythiaModel extends Model {
77
82
  * @param {Object} dependencies.logger - The logger instance
78
83
  * @param {Object} dependencies.config - The application config object
79
84
  * @param {Object} [dependencies.redis] - Optional Redis client instance
80
- * @param {Object} [dependencies.redisOptions] - Redis connection options if not providing a client
85
+ * @param {Object|Array|string} [dependencies.redisOptions] - Redis connection options if not providing a client.
86
+ * Can now be string (URL), object (ioredis options), or array of URLs/options for fallback.
81
87
  */
82
88
  static setDependencies({ logger, config, redis, redisOptions }) {
83
89
  if (!logger || !config) {
@@ -88,18 +94,30 @@ class KythiaModel extends Model {
88
94
  this.config = config;
89
95
  this.CACHE_VERSION = config.db?.redisCacheVersion || '1.0.0';
90
96
 
91
- // Check for sharding
92
97
  this.isShardMode = !!config?.db?.redis?.shard || false;
93
-
94
98
  if (this.isShardMode) {
95
99
  this.logger.info('🟣 [REDIS][SHARD] Detected redis sharding mode (shard: true). Local fallback cache DISABLED!');
96
100
  }
97
101
 
102
+ if (Array.isArray(redisOptions)) {
103
+ this._redisFallbackURLs = redisOptions.slice();
104
+ } else if (typeof redisOptions === 'string') {
105
+ this._redisFallbackURLs = redisOptions.split(',').map((url) => url.trim());
106
+ } else if (redisOptions && typeof redisOptions === 'object' && Array.isArray(redisOptions.urls)) {
107
+ this._redisFallbackURLs = redisOptions.urls.slice();
108
+ } else if (redisOptions) {
109
+ this._redisFallbackURLs = [redisOptions];
110
+ } else {
111
+ this._redisFallbackURLs = [];
112
+ }
113
+
114
+ this._redisCurrentIndex = 0;
115
+
98
116
  if (redis) {
99
117
  this.redis = redis;
100
118
  this.isRedisConnected = redis.status === 'ready';
101
- } else if (redisOptions) {
102
- this.initializeRedis(redisOptions);
119
+ } else if (this._redisFallbackURLs.length > 0) {
120
+ this.initializeRedis();
103
121
  } else {
104
122
  if (this.isShardMode) {
105
123
  this.logger.error('❌ [REDIS][SHARD] No Redis client/options, but shard:true. Application will work WITHOUT caching!');
@@ -114,7 +132,8 @@ class KythiaModel extends Model {
114
132
  /**
115
133
  * Helper: Track redis error timestamp, and check if error count in interval exceeds tolerance.
116
134
  * Jika error yang terjadi >= REDIS_ERROR_TOLERANCE_COUNT dalam REDIS_ERROR_TOLERANCE_INTERVAL_MS,
117
- * barulah fallback ke In-Memory (isRedisConnected = false) -- KECUALI jika shard: true.
135
+ * barulah coba connect ke redis berikutnya (multi redis), jika tidak ada, baru fallback ke In-Memory (isRedisConnected = false)
136
+ * -- KECUALI jika shard: true.
118
137
  */
119
138
  static _trackRedisError(err) {
120
139
  const now = Date.now();
@@ -124,21 +143,22 @@ class KythiaModel extends Model {
124
143
 
125
144
  if (this.redisErrorTimestamps.length >= REDIS_ERROR_TOLERANCE_COUNT) {
126
145
  if (this.isRedisConnected) {
127
- // In shard mode, fallback is not allowed!
128
- if (this.isShardMode) {
146
+ const triedFallback = this._tryRedisFailover();
147
+ if (triedFallback) {
148
+ this.logger.warn(`[REDIS] Error tolerance reached, switching to NEXT Redis failover...`);
149
+ } else if (this.isShardMode) {
129
150
  this.logger.error(
130
151
  `❌ [REDIS][SHARD] ${this.redisErrorTimestamps.length} consecutive errors in ${
131
152
  REDIS_ERROR_TOLERANCE_INTERVAL_MS / 1000
132
153
  }s. SHARD MODE: Disabling cache (NO fallback), all queries go to DB. (Last error: ${err?.message})`
133
154
  );
134
155
  this.isRedisConnected = false;
135
- // Do not schedule reconnect if redis is not supposed to fallback. Reconnect logic is fine.
136
156
  this._scheduleReconnect();
137
157
  } else {
138
158
  this.logger.error(
139
159
  `❌ [REDIS] ${this.redisErrorTimestamps.length} consecutive errors in ${
140
160
  REDIS_ERROR_TOLERANCE_INTERVAL_MS / 1000
141
- }s. Fallback to In-Memory Cache! (Last error: ${err?.message})`
161
+ }s. All Redis exhausted, fallback to In-Memory Cache! (Last error: ${err?.message})`
142
162
  );
143
163
  this.isRedisConnected = false;
144
164
  this._scheduleReconnect();
@@ -154,68 +174,141 @@ class KythiaModel extends Model {
154
174
  }
155
175
 
156
176
  /**
157
- * 🔌 Initializes the Redis connection if not already initialized.
158
- * @param {string|Object} redisOptions - Redis connection string or options object
159
- * @returns {Object} The Redis client instance
177
+ * Coba switch ke redis URL berikutnya jika ada. Return true jika switching, false jika tidak ada lagi.
178
+ * PRIVATE.
160
179
  */
161
- static initializeRedis(redisOptions) {
162
- if (this.redis) return this.redis;
180
+ static _tryRedisFailover() {
181
+ if (!Array.isArray(this._redisFallbackURLs) || this._redisFallbackURLs.length < 2) {
182
+ return false;
183
+ }
184
+ const prevIndex = this._redisCurrentIndex;
185
+ if (this._redisCurrentIndex + 1 < this._redisFallbackURLs.length) {
186
+ this._redisCurrentIndex++;
187
+ this.logger.warn(
188
+ `[REDIS][FAILOVER] Trying to switch Redis connection from url index ${prevIndex} to ${this._redisCurrentIndex}`
189
+ );
190
+ this._closeCurrentRedis();
191
+ this.initializeRedis();
192
+ return true;
193
+ }
194
+ return false;
195
+ }
163
196
 
164
- const Redis = require('ioredis');
165
- this.lastRedisOpts = redisOptions;
197
+ /**
198
+ * Close the current Redis (if exists).
199
+ * PRIVATE.
200
+ */
201
+ static _closeCurrentRedis() {
202
+ if (this.redis && typeof this.redis.quit === 'function') {
203
+ try {
204
+ this.redis.quit();
205
+ } catch (e) {}
206
+ }
207
+ this.redis = undefined;
208
+ this.isRedisConnected = false;
209
+ }
166
210
 
167
- // Check sharding now if not set yet (for runtime .initializeRedis case)
168
- if (redisOptions && typeof redisOptions === 'object' && redisOptions.shard) {
169
- this.isShardMode = true;
211
+ /**
212
+ * 🔌 Initializes the Redis connection if not already initialized.
213
+ * (Versi ini MENGHAPUS lazyConnect dan _attemptConnection untuk fix race condition)
214
+ */
215
+ static initializeRedis(redisOptions) {
216
+ if (redisOptions) {
217
+ // ... (Logic ini biarin aja, udah bener) ...
218
+ if (Array.isArray(redisOptions)) {
219
+ this._redisFallbackURLs = redisOptions.slice();
220
+ this._redisCurrentIndex = 0;
221
+ } else if (redisOptions && typeof redisOptions === 'object' && Array.isArray(redisOptions.urls)) {
222
+ this._redisFallbackURLs = redisOptions.urls.slice();
223
+ this._redisCurrentIndex = 0;
224
+ } else {
225
+ this._redisFallbackURLs = [redisOptions];
226
+ this._redisCurrentIndex = 0;
227
+ }
170
228
  }
171
229
 
172
- if (!redisOptions || (typeof redisOptions === 'string' && redisOptions.trim() === '')) {
230
+ if (!Array.isArray(this._redisFallbackURLs) || this._redisFallbackURLs.length === 0) {
231
+ // ... (Logic error ini biarin aja, udah bener) ...
173
232
  if (this.isShardMode) {
174
233
  this.logger.error('❌ [REDIS][SHARD] No Redis URL/options provided but shard:true. Will run without caching!');
175
234
  this.isRedisConnected = false;
176
235
  } else {
177
- this.logger.warn('🟠 [REDIS] No Redis URL provided. Operating in In-Memory Cache mode only.');
236
+ this.logger.warn('🟠 [REDIS] No Redis client or options provided. Operating in In-Memory Cache mode only.');
178
237
  this.isRedisConnected = false;
179
238
  }
180
239
  return null;
181
240
  }
182
241
 
183
- const retryStrategy = (times) => {
184
- if (times > 5) {
185
- if (this.isShardMode) {
186
- this.logger.error(`❌ [REDIS][SHARD] Could not connect after ${times - 1} retries. Disabling cache (no fallback)!`);
187
- } else {
188
- this.logger.error(`❌ [REDIS] Could not connect after ${times - 1} retries. Falling back to In-Memory Cache.`);
189
- }
190
- return null;
191
- }
192
- const delay = Math.min(times * 500, 2000);
193
- this.logger.warn(`🟠 [REDIS] Connection failed. Retrying in ${delay}ms (Attempt ${times})...`);
194
- return delay;
195
- };
242
+ const Redis = require('ioredis');
243
+ this.lastRedisOpts = Array.isArray(this._redisFallbackURLs) ? this._redisFallbackURLs.slice() : [this._redisFallbackURLs];
196
244
 
197
- const finalOptions =
198
- typeof redisOptions === 'string'
199
- ? { url: redisOptions, retryStrategy, lazyConnect: true }
200
- : { maxRetriesPerRequest: 2, enableReadyCheck: true, retryStrategy, lazyConnect: true, ...redisOptions };
245
+ if (this.redis) return this.redis;
201
246
 
202
- this.redis = new Redis(
203
- typeof redisOptions === 'string' ? redisOptions : finalOptions,
204
- typeof redisOptions === 'string' ? finalOptions : undefined
205
- );
247
+ const opt = this._redisFallbackURLs[this._redisCurrentIndex];
206
248
 
207
- this.redis.connect().catch((err) => {
208
- if (this.isShardMode) {
209
- this.logger.error('❌ [REDIS][SHARD] Initial connection failed: ' + err.message);
210
- } else {
211
- this.logger.error('❌ [REDIS] Initial connection failed:', err.message);
212
- }
213
- });
249
+ if (opt && typeof opt === 'object' && opt.shard) {
250
+ this.isShardMode = true;
251
+ }
252
+
253
+ let redisOpt;
254
+ if (typeof opt === 'string') {
255
+ // --- 👇 PERUBAHAN DI SINI 👇 ---
256
+ // HAPUS lazyConnect: true
257
+ redisOpt = { url: opt, retryStrategy: this._makeRetryStrategy() };
258
+ } else if (opt && typeof opt === 'object') {
259
+ // --- 👇 PERUBAHAN DI SINI 👇 ---
260
+ // HAPUS lazyConnect: true
261
+ redisOpt = {
262
+ maxRetriesPerRequest: 2,
263
+ enableReadyCheck: true,
264
+ retryStrategy: this._makeRetryStrategy(),
265
+ ...opt,
266
+ };
267
+ } else {
268
+ this.logger.error('❌ [REDIS] Invalid redis config detected in list');
269
+ this.isRedisConnected = false;
270
+ return null;
271
+ }
214
272
 
273
+ this.logger.info(
274
+ `[REDIS][INIT] Connecting to Redis fallback #${this._redisCurrentIndex + 1}/${this._redisFallbackURLs.length}: ${
275
+ typeof opt === 'string' ? opt : redisOpt.url || '(object)'
276
+ }`
277
+ );
278
+
279
+ // --- 👇 PERUBAHAN DI SINI 👇 ---
280
+ // Biarin ioredis otomatis konek (nggak pake lazy)
281
+ this.redis = new Redis(redisOpt.url || redisOpt);
282
+
283
+ // Langsung pasang handler
215
284
  this._setupRedisEventHandlers();
285
+
286
+ // HAPUS PANGGILAN ke _attemptConnection()
287
+
216
288
  return this.redis;
217
289
  }
218
290
 
291
+ // HAPUS Fungsi _attemptConnection
292
+ // (Sudah tidak ada atau di bawah ini harus DIHAPUS sepenuhnya)
293
+
294
+ /**
295
+ * Internal: Makes retry strategy function which wraps the fallback failover logic if all failed.
296
+ * Used by initializeRedis.
297
+ */
298
+ static _makeRetryStrategy() {
299
+ return (times) => {
300
+ if (times > 5) {
301
+ this.logger.error(`❌ [REDIS] Could not connect after ${times - 1} retries for Redis #${this._redisCurrentIndex + 1}.`);
302
+ return null;
303
+ }
304
+ const delay = Math.min(times * 500, 2000);
305
+ this.logger.warn(
306
+ `🟠 [REDIS] Connection failed for Redis #${this._redisCurrentIndex + 1}. Retrying in ${delay}ms (Attempt ${times})...`
307
+ );
308
+ return delay;
309
+ };
310
+ }
311
+
219
312
  /**
220
313
  * 🔌 Sets up Redis event handlers
221
314
  * @private
@@ -226,22 +319,19 @@ class KythiaModel extends Model {
226
319
  this.logger.info('✅ [REDIS] Connection established. Switching to Redis Cache mode.');
227
320
  }
228
321
  this.isRedisConnected = true;
229
-
230
322
  this.redisErrorTimestamps = [];
231
-
232
323
  if (this.reconnectTimeout) {
233
324
  clearTimeout(this.reconnectTimeout);
234
325
  this.reconnectTimeout = null;
235
326
  }
327
+ this._redisFailedIndexes.delete(this._redisCurrentIndex); // <-- TAMBAHIN INI
236
328
  });
237
329
 
238
330
  this.redis.on('error', (err) => {
331
+ // (Biarkan handler 'error' ini kosong atau cuma nge-log,
332
+ // karena 'close' yang akan nanganin failover)
239
333
  if (err && (err.code === 'ECONNREFUSED' || err.message)) {
240
- if (this.isShardMode) {
241
- this.logger.warn(`🟠 [REDIS][SHARD] Error: ${err.message}`);
242
- } else {
243
- this.logger.warn(`🟠 [REDIS] Error: ${err.message}`);
244
- }
334
+ this.logger.warn(`🟠 [REDIS] Connection error: ${err.message}`);
245
335
  }
246
336
  });
247
337
 
@@ -250,11 +340,22 @@ class KythiaModel extends Model {
250
340
  if (this.isShardMode) {
251
341
  this.logger.error('❌ [REDIS][SHARD] Connection closed. Cache DISABLED (no fallback).');
252
342
  } else {
253
- this.logger.error('❌ [REDIS] Connection closed. Falling back to In-Memory Cache mode.');
343
+ this.logger.error('❌ [REDIS] Connection closed. Fallback/failover will be attempted.');
254
344
  }
255
345
  }
256
346
  this.isRedisConnected = false;
257
- this._scheduleReconnect();
347
+
348
+ this._redisFailedIndexes.add(this._redisCurrentIndex); // <-- TAMBAHIN INI
349
+
350
+ // --- INI LOGIKA KUNCINYA ---
351
+ this.logger.warn(`[REDIS] Connection #${this._redisCurrentIndex + 1} closed. Attempting immediate failover...`);
352
+ const triedFailover = this._tryRedisFailover();
353
+
354
+ if (!triedFailover) {
355
+ this.logger.warn(`[REDIS] Failover exhausted. Scheduling full reconnect...`);
356
+ this._scheduleReconnect();
357
+ }
358
+ // --- AKHIR LOGIKA KUNCI ---
258
359
  });
259
360
  }
260
361
 
@@ -277,7 +378,11 @@ class KythiaModel extends Model {
277
378
 
278
379
  this.reconnectTimeout = setTimeout(() => {
279
380
  this.reconnectTimeout = null;
280
- this.initializeRedis(this.lastRedisOpts);
381
+
382
+ this._redisCurrentIndex = 0;
383
+ this._redisFailedIndexes.clear(); // <-- TAMBAHIN INI
384
+ this._closeCurrentRedis();
385
+ this.initializeRedis();
281
386
  }, RECONNECT_DELAY_MINUTES * 60 * 1000);
282
387
  }
283
388
 
@@ -329,9 +434,8 @@ class KythiaModel extends Model {
329
434
  if (this.isRedisConnected) {
330
435
  await this._redisSetCacheEntry(cacheKey, data, finalTtl, tags);
331
436
  } else if (!this.isShardMode) {
332
- // NON-shard only
333
437
  this._mapSetCacheEntry(cacheKey, data, finalTtl);
334
- } // else: shard mode, Redis is down, DO NOT cache
438
+ }
335
439
  }
336
440
 
337
441
  /**
@@ -345,10 +449,9 @@ class KythiaModel extends Model {
345
449
  if (this.isRedisConnected) {
346
450
  return this._redisGetCachedEntry(cacheKey, includeOptions);
347
451
  } else if (!this.isShardMode) {
348
- // fallback only if not sharding
349
452
  return this._mapGetCachedEntry(cacheKey, includeOptions);
350
453
  }
351
- // SHARD MODE: no local fallback
454
+
352
455
  return { hit: false, data: undefined };
353
456
  }
354
457
 
@@ -494,7 +597,7 @@ class KythiaModel extends Model {
494
597
  * DISABLED in shard mode.
495
598
  */
496
599
  static _mapGetCachedEntry(cacheKey, includeOptions) {
497
- if (this.isShardMode) return { hit: false, data: undefined }; // DISABLED in shard mode
600
+ if (this.isShardMode) return { hit: false, data: undefined };
498
601
 
499
602
  if (this.localNegativeCache.has(cacheKey)) {
500
603
  this.cacheStats.mapHits++;
@@ -544,7 +647,7 @@ class KythiaModel extends Model {
544
647
  * DISABLED in shard mode.
545
648
  */
546
649
  static _mapClearCache(cacheKey) {
547
- if (this.isShardMode) return; // DISABLED in shard mode
650
+ if (this.isShardMode) return;
548
651
  this.localCache.delete(cacheKey);
549
652
  this.localNegativeCache.delete(cacheKey);
550
653
  this.cacheStats.clears++;
@@ -556,7 +659,7 @@ class KythiaModel extends Model {
556
659
  * DISABLED in shard mode.
557
660
  */
558
661
  static _mapClearAllModelCache() {
559
- if (this.isShardMode) return; // DISABLED in shard mode
662
+ if (this.isShardMode) return;
560
663
  const prefix = `${this.CACHE_VERSION}:${this.name}:`;
561
664
  let cleared = 0;
562
665
 
@@ -639,7 +742,6 @@ class KythiaModel extends Model {
639
742
 
640
743
  const queryPromise = this.findOne(normalizedOptions)
641
744
  .then((record) => {
642
- // Only cache if allowed (no cache in shard/failover unless redis is up)
643
745
  if (this.isRedisConnected || !this.isShardMode) {
644
746
  const tags = [`${this.name}`];
645
747
  if (record) {
@@ -683,7 +785,6 @@ class KythiaModel extends Model {
683
785
 
684
786
  const queryPromise = this.findAll(normalizedOptions)
685
787
  .then((records) => {
686
- // Only cache if allowed
687
788
  if (this.isRedisConnected || !this.isShardMode) {
688
789
  const tags = [`${this.name}`];
689
790
 
@@ -723,7 +824,6 @@ class KythiaModel extends Model {
723
824
  }
724
825
  const findOrCreatePromise = this.findOrCreate(findOrCreateOptions)
725
826
  .then(([instance, created]) => {
726
- // Only cache if allowed
727
827
  if (this.isRedisConnected || !this.isShardMode) {
728
828
  const tags = [`${this.name}`];
729
829
  if (instance) {
@@ -757,7 +857,6 @@ class KythiaModel extends Model {
757
857
  this.cacheStats.misses++;
758
858
  const count = await this.count(countOptions);
759
859
 
760
- // Only cache if allowed
761
860
  if (this.isRedisConnected || !this.isShardMode) {
762
861
  const tags = [`${this.name}`];
763
862
  this.setCacheEntry(cacheKey, count, ttl, tags);
@@ -807,7 +906,6 @@ class KythiaModel extends Model {
807
906
 
808
907
  const result = await this.findAll(queryOptions);
809
908
 
810
- // Only cache if allowed
811
909
  if (this.isRedisConnected || !this.isShardMode) {
812
910
  const tags = [`${this.name}`];
813
911
  if (Array.isArray(cacheTags)) tags.push(...cacheTags);
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaORM.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * A utility for intelligent, hash-based syncing of Sequelize models.
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaSequelize.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * Main Sequelize connection factory for the application
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/AddonManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all addon loading, command registration, and component management.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/EventManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord event listeners except InteractionCreate.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/InteractionManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord interaction events including slash commands, buttons, modals,
@@ -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.3-beta
7
+ * @version 0.9.4-beta
8
8
  *
9
9
  * @description
10
10
  * Handles graceful shutdown procedures including interval tracking,