Caching improvements

This commit is contained in:
yeongaori
2025-08-21 15:27:26 +09:00
parent 5c468d5d5c
commit 045b6c88f0
5 changed files with 170 additions and 98 deletions

View File

@@ -1,10 +1,11 @@
require('dotenv').config(); require('dotenv').config();
const { Client, GatewayIntentBits, Partials, Collection } = require('discord.js'); const { Client, GatewayIntentBits, Partials } = require('discord.js');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const logger = require('./modules/colorfulLogger'); const logger = require('./modules/colorfulLogger');
const { loadData } = require('./src/data/dataManager'); const { loadData } = require('./src/data/dataManager');
const { setupProcessHandlers } = require('./src/handlers/processHandlers'); const { setupProcessHandlers } = require('./src/handlers/processHandlers');
const { loadCommands } = require('./src/handlers/commandHandler');
const client = new Client({ const client = new Client({
intents: [ intents: [
@@ -16,39 +17,13 @@ const client = new Client({
partials: [Partials.Channel], partials: [Partials.Channel],
}); });
// Load data and setup process handlers // Load data and commands
loadData(); loadData();
loadCommands();
// Setup process handlers
setupProcessHandlers(client); setupProcessHandlers(client);
// Load commands
client.commands = new Collection();
client.legacyCommands = new Collection();
const commandFolders = fs.readdirSync(path.join(__dirname, 'src', 'commands'));
for (const folder of commandFolders) {
const commandFiles = fs.readdirSync(path.join(__dirname, 'src', 'commands', folder)).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(__dirname, 'src', 'commands', folder, file);
const command = require(filePath);
// Load slash command part
if (command.data && command.execute) {
client.commands.set(command.data.name, command);
}
// Load legacy command part
if (command.legacy) {
client.legacyCommands.set(command.legacy.name, command.legacy);
if (command.legacy.aliases) {
command.legacy.aliases.forEach(alias => {
client.legacyCommands.set(alias, command.legacy);
});
}
}
}
}
// Load event handlers // Load event handlers
const eventsPath = path.join(__dirname, 'src', 'events'); const eventsPath = path.join(__dirname, 'src', 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js')); const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
@@ -57,9 +32,9 @@ for (const file of eventFiles) {
const filePath = path.join(eventsPath, file); const filePath = path.join(eventsPath, file);
const event = require(filePath); const event = require(filePath);
if (event.once) { if (event.once) {
client.once(event.name, (...args) => event.execute(...args)); client.once(event.name, (...args) => event.execute(...args, client));
} else { } else {
client.on(event.name, (...args) => event.execute(...args)); client.on(event.name, (...args) => event.execute(...args, client));
} }
} }

View File

@@ -7,10 +7,9 @@ const {
notificationHistoryPath, notificationHistoryPath,
dataDir dataDir
} = require('../../config/constants'); } = require('../../config/constants');
const cacheManager = require('../utils/cacheManager');
let ipdaeDatas = []; const CACHE_TTL = 300000; // 5 minutes
let celebrationConfig = {};
let notificationHistory = {};
async function loadData() { async function loadData() {
try { try {
@@ -19,36 +18,47 @@ async function loadData() {
logger.error(`Failed to create data directory ${dataDir}: ${e.message}`); logger.error(`Failed to create data directory ${dataDir}: ${e.message}`);
} }
await loadIpdaeData();
await loadNotificationHistory();
await loadCelebrationConfig();
await migrateData();
}
async function loadIpdaeData() {
try { try {
const data = await fs.readFile(ipdaeDataDirectory, 'utf8'); const data = await fs.readFile(ipdaeDataDirectory, 'utf8');
ipdaeDatas = JSON.parse(data); const ipdaeDatas = JSON.parse(data);
ipdaeDatas.sort((a, b) => a.id.localeCompare(b.id)); ipdaeDatas.sort((a, b) => a.id.localeCompare(b.id));
logger.info('ipdaeData.json loaded and parsed.'); cacheManager.set('ipdaeData', ipdaeDatas, CACHE_TTL);
logger.info('ipdaeData.json loaded and cached.');
} catch (e) { } catch (e) {
logger.warn(`Failed to load ipdaeData.json: ${e.message}. Initializing as empty array.`); logger.warn(`Failed to load ipdaeData.json: ${e.message}. Initializing as empty array.`);
ipdaeDatas = []; cacheManager.set('ipdaeData', [], CACHE_TTL);
if (e.code === 'ENOENT') { if (e.code === 'ENOENT') {
await saveData('ipdae'); await saveData('ipdae');
} }
} }
}
async function loadNotificationHistory() {
try { try {
const historyData = await fs.readFile(notificationHistoryPath, 'utf8'); const historyData = await fs.readFile(notificationHistoryPath, 'utf8');
notificationHistory = JSON.parse(historyData); const notificationHistory = JSON.parse(historyData);
logger.info('notificationHistory.json loaded and parsed.'); cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL);
logger.info('notificationHistory.json loaded and cached.');
} catch (e) { } catch (e) {
logger.warn(`notificationHistory.json not found or invalid: ${e.message}. Initializing as empty object.`); logger.warn(`notificationHistory.json not found or invalid: ${e.message}. Initializing as empty object.`);
notificationHistory = {}; cacheManager.set('notificationHistory', {}, CACHE_TTL);
if (e.code === 'ENOENT') { if (e.code === 'ENOENT') {
await saveData('history'); await saveData('history');
} }
} }
}
await migrateData(); async function loadCelebrationConfig() {
try { try {
const configData = await fs.readFile(celebrationConfigPath, 'utf8'); const configData = await fs.readFile(celebrationConfigPath, 'utf8');
celebrationConfig = JSON.parse(configData); const celebrationConfig = JSON.parse(configData);
if (celebrationConfig.milestones && Array.isArray(celebrationConfig.milestones)) { if (celebrationConfig.milestones && Array.isArray(celebrationConfig.milestones)) {
celebrationConfig.milestones.forEach(m => { celebrationConfig.milestones.forEach(m => {
m.mentionUser = m.mentionUser ?? true; m.mentionUser = m.mentionUser ?? true;
@@ -57,10 +67,11 @@ async function loadData() {
} else { } else {
throw new Error("Milestones data is missing or invalid in celebrationConfig.json"); throw new Error("Milestones data is missing or invalid in celebrationConfig.json");
} }
logger.info('celebrationConfig.json loaded and parsed.'); cacheManager.set('celebrationConfig', celebrationConfig, CACHE_TTL);
logger.info('celebrationConfig.json loaded and cached.');
} catch (e) { } catch (e) {
logger.warn(`celebrationConfig.json error: ${e.message}. Creating/Using default config.`); logger.warn(`celebrationConfig.json error: ${e.message}. Creating/Using default config.`);
celebrationConfig = { const defaultConfig = {
channelTopicKeyword: "전역축하알림", channelTopicKeyword: "전역축하알림",
milestones: [ milestones: [
{ days: 100, message: "🎉 {userName}님, 전역까지 D-100일! 영광의 그날까지 파이팅입니다! 🎉", mentionUser: true, mentionEveryone: false }, { days: 100, message: "🎉 {userName}님, 전역까지 D-100일! 영광의 그날까지 파이팅입니다! 🎉", mentionUser: true, mentionEveryone: false },
@@ -71,16 +82,19 @@ async function loadData() {
{ days: 0, message: "🫡 {userName}님, 🎉축 전역🎉 대한민국의 자랑스러운 아들! 그동안 정말 고 생 많으셨습니다! 🫡", mentionUser: true, mentionEveryone: true } { days: 0, message: "🫡 {userName}님, 🎉축 전역🎉 대한민국의 자랑스러운 아들! 그동안 정말 고 생 많으셨습니다! 🫡", mentionUser: true, mentionEveryone: true }
] ]
}; };
cacheManager.set('celebrationConfig', defaultConfig, CACHE_TTL);
await saveData('config'); await saveData('config');
} }
} }
async function migrateData() { async function migrateData() {
let ipdaeData = getIpdaeData();
let notificationHistory = getNotificationHistory();
let ipdaeDataNeedsSave = false; let ipdaeDataNeedsSave = false;
let notificationHistoryNeedsSave = false; let notificationHistoryNeedsSave = false;
for (let i = 0; i < ipdaeDatas.length; i++) { for (let i = 0; i < ipdaeData.length; i++) {
const user = ipdaeDatas[i]; const user = ipdaeData[i];
if (user.hasOwnProperty('notifiedMilestones')) { if (user.hasOwnProperty('notifiedMilestones')) {
if (Array.isArray(user.notifiedMilestones) && user.notifiedMilestones.length > 0) { if (Array.isArray(user.notifiedMilestones) && user.notifiedMilestones.length > 0) {
if (!notificationHistory[user.id] || Object.keys(notificationHistory[user.id]).length === 0) { if (!notificationHistory[user.id] || Object.keys(notificationHistory[user.id]).length === 0) {
@@ -97,9 +111,11 @@ async function migrateData() {
} }
if (ipdaeDataNeedsSave) { if (ipdaeDataNeedsSave) {
cacheManager.set('ipdaeData', ipdaeData, CACHE_TTL);
await saveData('ipdae'); await saveData('ipdae');
} }
if (notificationHistoryNeedsSave) { if (notificationHistoryNeedsSave) {
cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL);
await saveData('history'); await saveData('history');
} }
} }
@@ -108,16 +124,22 @@ async function saveData(type) {
try { try {
switch (type) { switch (type) {
case 'ipdae': case 'ipdae':
await fs.writeFile(ipdaeDataDirectory, JSON.stringify(ipdaeDatas, null, 4), 'utf8'); const ipdaeData = getIpdaeData();
logger.info('ipdaeData.json saved.'); await fs.writeFile(ipdaeDataDirectory, JSON.stringify(ipdaeData, null, 4), 'utf8');
cacheManager.set('ipdaeData', ipdaeData, CACHE_TTL);
logger.info('ipdaeData.json saved and cache updated.');
break; break;
case 'config': case 'config':
const celebrationConfig = getCelebrationConfig();
await fs.writeFile(celebrationConfigPath, JSON.stringify(celebrationConfig, null, 4), 'utf8'); await fs.writeFile(celebrationConfigPath, JSON.stringify(celebrationConfig, null, 4), 'utf8');
logger.info('celebrationConfig.json saved.'); cacheManager.set('celebrationConfig', celebrationConfig, CACHE_TTL);
logger.info('celebrationConfig.json saved and cache updated.');
break; break;
case 'history': case 'history':
const notificationHistory = getNotificationHistory();
await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8'); await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8');
logger.info('notificationHistory.json saved.'); cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL);
logger.info('notificationHistory.json saved and cache updated.');
break; break;
default: default:
logger.warn(`Unknown data type for saving: ${type}`); logger.warn(`Unknown data type for saving: ${type}`);
@@ -128,18 +150,38 @@ async function saveData(type) {
} }
function getIpdaeData() { function getIpdaeData() {
return ipdaeDatas; let data = cacheManager.get('ipdaeData');
if (!data) {
logger.info("ipdaeData not in cache or expired, reloading from file.");
loadIpdaeData();
data = cacheManager.get('ipdaeData');
}
return data || [];
} }
function getCelebrationConfig() { function getCelebrationConfig() {
return celebrationConfig; let data = cacheManager.get('celebrationConfig');
if (!data) {
logger.info("celebrationConfig not in cache or expired, reloading from file.");
loadCelebrationConfig();
data = cacheManager.get('celebrationConfig');
}
return data || {};
} }
function getNotificationHistory() { function getNotificationHistory() {
return notificationHistory; let data = cacheManager.get('notificationHistory');
if (!data) {
logger.info("notificationHistory not in cache or expired, reloading from file.");
loadNotificationHistory();
data = cacheManager.get('notificationHistory');
}
return data || {};
} }
async function updateEnlistmentData(userId, date, type) { async function updateEnlistmentData(userId, date, type) {
let ipdaeDatas = getIpdaeData();
let notificationHistory = getNotificationHistory();
let notificationHistoryModified = false; let notificationHistoryModified = false;
const index = binarySearch(ipdaeDatas, userId); const index = binarySearch(ipdaeDatas, userId);
@@ -161,8 +203,10 @@ async function updateEnlistmentData(userId, date, type) {
notificationHistoryModified = true; notificationHistoryModified = true;
} }
cacheManager.set('ipdaeData', ipdaeDatas, CACHE_TTL);
await saveData('ipdae'); await saveData('ipdae');
if (notificationHistoryModified) { if (notificationHistoryModified) {
cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL);
await saveData('history'); await saveData('history');
} }
@@ -176,7 +220,4 @@ module.exports = {
getCelebrationConfig, getCelebrationConfig,
getNotificationHistory, getNotificationHistory,
updateEnlistmentData, updateEnlistmentData,
ipdaeDatas,
celebrationConfig,
notificationHistory
}; };

View File

@@ -4,29 +4,43 @@ const path = require('path');
const logger = require('../../modules/colorfulLogger'); const logger = require('../../modules/colorfulLogger');
const { commandPrefix } = require('../../config/constants'); const { commandPrefix } = require('../../config/constants');
const { getNameById } = require('../utils/discordUtils'); const { getNameById } = require('../utils/discordUtils');
const cacheManager = require('../utils/cacheManager');
function loadLegacyCommands(client) { function loadCommands() {
client.legacyCommands = new Collection(); const slashCommands = new Collection();
const legacyCommands = new Collection();
const commandFolders = fs.readdirSync(path.join(__dirname, '..', 'commands')); const commandFolders = fs.readdirSync(path.join(__dirname, '..', 'commands'));
for (const folder of commandFolders) { for (const folder of commandFolders) {
const commandFiles = fs.readdirSync(path.join(__dirname, '..', 'commands', folder)).filter(file => file.endsWith('.js')); const commandFiles = fs.readdirSync(path.join(__dirname, '..', 'commands', folder)).filter(file => file.endsWith('.js'));
for (const file of commandFiles) { for (const file of commandFiles) {
try { try {
const command = require(path.join(__dirname, '..', 'commands', folder, file)); const filePath = path.join(__dirname, '..', 'commands', folder, file);
const command = require(filePath);
// Load slash command part
if (command.data && command.execute) {
slashCommands.set(command.data.name, command);
}
// Load legacy command part
if (command.legacy) { if (command.legacy) {
const legacyCommand = command.legacy; legacyCommands.set(command.legacy.name, command.legacy);
client.legacyCommands.set(legacyCommand.name, legacyCommand); if (command.legacy.aliases) {
if (legacyCommand.aliases && legacyCommand.aliases.length > 0) { command.legacy.aliases.forEach(alias => {
legacyCommand.aliases.forEach(alias => client.legacyCommands.set(alias, legacyCommand)); legacyCommands.set(alias, command.legacy);
});
} }
} }
} catch (error) { } catch (error) {
logger.error(`Error loading legacy command from ${file}:`, error); logger.error(`Error loading command from ${file}:`, error);
} }
} }
} }
cacheManager.set('slashCommands', slashCommands);
cacheManager.set('legacyCommands', legacyCommands);
logger.info('All commands loaded and cached.');
} }
async function handleCommand(message) { async function handleCommand(message) {
@@ -35,7 +49,13 @@ async function handleCommand(message) {
const args = message.content.slice(commandPrefix.length).trim().split(/\s+/); const args = message.content.slice(commandPrefix.length).trim().split(/\s+/);
const commandName = args.shift().toLowerCase(); const commandName = args.shift().toLowerCase();
const command = message.client.legacyCommands.get(commandName); const legacyCommands = cacheManager.get('legacyCommands');
if (!legacyCommands) {
logger.error('Legacy commands not found in cache.');
return;
}
const command = legacyCommands.get(commandName);
if (!command) return; if (!command) return;
@@ -52,4 +72,4 @@ async function handleCommand(message) {
} }
} }
module.exports = { loadLegacyCommands, handleCommand }; module.exports = { loadCommands, handleCommand };

View File

@@ -1,33 +1,15 @@
const { Collection } = require('discord.js');
const fs = require('fs');
const path = require('path');
const logger = require('../../modules/colorfulLogger');
const { handleCommandError } = require('../../utils/errorHandler'); const { handleCommandError } = require('../../utils/errorHandler');
const cacheManager = require('../utils/cacheManager');
function loadCommands() { const logger = require('../../modules/colorfulLogger');
const commands = new Collection();
const commandFolders = fs.readdirSync(path.join(__dirname, '..', 'commands'));
for (const folder of commandFolders) {
const commandFiles = fs.readdirSync(path.join(__dirname, '..', 'commands', folder)).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
try {
const command = require(path.join(__dirname, '..', 'commands', folder, file));
if (command.data && command.execute) {
commands.set(command.data.name, command);
} else {
logger.warn(`Command file ${file} is missing a data or execute export.`);
}
} catch (error) {
logger.error(`Error loading command from ${file}:`, error);
}
}
}
return commands;
}
async function onSlashCommand(interaction) { async function onSlashCommand(interaction) {
const command = interaction.client.commands.get(interaction.commandName); const slashCommands = cacheManager.get('slashCommands');
if (!slashCommands) {
logger.error('Slash commands not found in cache.');
return;
}
const command = slashCommands.get(interaction.commandName);
if (!command) { if (!command) {
logger.warn(`No command matching ${interaction.commandName} was found.`); logger.warn(`No command matching ${interaction.commandName} was found.`);
@@ -43,4 +25,4 @@ async function onSlashCommand(interaction) {
} }
} }
module.exports = { onSlashCommand, loadCommands }; module.exports = { onSlashCommand };

54
src/utils/cacheManager.js Normal file
View File

@@ -0,0 +1,54 @@
const logger = require('../../modules/colorfulLogger');
class CacheManager {
constructor() {
this.cache = new Map();
logger.info('CacheManager initialized.');
}
get(key) {
const cachedItem = this.cache.get(key);
if (!cachedItem) {
logger.debug(`Cache MISS for key: ${key}`);
return null;
}
if (cachedItem.expire && Date.now() > cachedItem.expire) {
logger.info(`Cache EXPIRED for key: ${key}`);
this.cache.delete(key);
return null;
}
logger.debug(`Cache HIT for key: ${key}`);
return cachedItem.value;
}
set(key, value, ttl = 0) {
logger.debug(`Caching data for key: ${key} with TTL: ${ttl}ms`);
this.cache.set(key, {
value: value,
expire: ttl > 0 ? Date.now() + ttl : null,
});
}
has(key) {
return this.cache.has(key);
}
delete(key) {
logger.debug(`Deleting cache for key: ${key}`);
return this.cache.delete(key);
}
flush() {
logger.info('Flushing all caches.');
this.cache.clear();
}
get size() {
return this.cache.size;
}
}
const cacheManager = new CacheManager();
module.exports = cacheManager;