packmate
Version:
Your smart and friendly interactive assistant for dependency updates, security advisories, and cleanup.
346 lines (287 loc) • 10.1 kB
JavaScript
/**
* Enhanced Caching System for Packmate v2.2.0
* 3단계 캐싱: Memory (LRU) → Disk (JSON Files) → Registry (Network)
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
// LRU Cache 구현
class LRUCache {
constructor(maxSize = 1000) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
// 접근된 항목을 맨 뒤로 이동 (LRU)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return null;
}
set(key, value) {
// 이미 존재하면 갱신
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 크기 초과시 가장 오래된 항목 제거
else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
has(key) {
return this.cache.has(key);
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
// 캐시 통계
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
usage: `${Math.round(this.cache.size / this.maxSize * 100)}%`
};
}
}
// JSON 파일 기반 디스크 캐시
class DiskCache {
constructor(cacheDir = null) {
this.cacheDir = cacheDir || path.join(os.tmpdir(), 'packmate-cache');
this.indexPath = path.join(this.cacheDir, 'cache-index.json');
this.isInitialized = false;
this.index = {};
}
async init() {
if (this.isInitialized) return;
try {
// 캐시 디렉터리 생성
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true });
}
// 인덱스 파일 로드
if (fs.existsSync(this.indexPath)) {
const indexData = fs.readFileSync(this.indexPath, 'utf-8');
this.index = JSON.parse(indexData);
} else {
this.index = {};
this.saveIndex();
}
this.isInitialized = true;
} catch (error) {
console.warn(`⚠️ Disk cache init failed: ${error.message}`);
this.index = {};
}
}
saveIndex() {
try {
fs.writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2));
} catch (error) {
console.warn(`Index save failed: ${error.message}`);
}
}
getCacheFilePath(packageName) {
// 패키지명을 파일시스템 안전한 이름으로 변환
const safeName = packageName.replace(/[@/]/g, '_');
return path.join(this.cacheDir, `${safeName}.json`);
}
async get(packageName, ttl = 3600000) { // 1시간 기본 TTL
if (!this.isInitialized) await this.init();
try {
const cacheFile = this.getCacheFilePath(packageName);
const indexEntry = this.index[packageName];
if (!indexEntry || !fs.existsSync(cacheFile)) {
return null;
}
const now = Date.now();
if ((now - indexEntry.createdAt) > ttl) {
// 만료된 캐시 삭제
this.delete(packageName);
return null;
}
// 접근 시간 업데이트
indexEntry.accessedAt = now;
this.saveIndex();
const cacheData = fs.readFileSync(cacheFile, 'utf-8');
return JSON.parse(cacheData);
} catch (error) {
console.warn(`Disk cache get error for ${packageName}:`, error.message);
return null;
}
}
async set(packageName, versionData) {
if (!this.isInitialized) await this.init();
try {
const cacheFile = this.getCacheFilePath(packageName);
const now = Date.now();
// 데이터 파일 저장
fs.writeFileSync(cacheFile, JSON.stringify(versionData, null, 2));
// 인덱스 업데이트
this.index[packageName] = {
createdAt: now,
accessedAt: now,
filePath: cacheFile
};
this.saveIndex();
} catch (error) {
console.warn(`Disk cache set error for ${packageName}:`, error.message);
}
}
delete(packageName) {
try {
const cacheFile = this.getCacheFilePath(packageName);
// 파일 삭제
if (fs.existsSync(cacheFile)) {
fs.unlinkSync(cacheFile);
}
// 인덱스에서 삭제
delete this.index[packageName];
this.saveIndex();
} catch (error) {
console.warn(`Disk cache delete error for ${packageName}:`, error.message);
}
}
async cleanup(maxAge = 7 * 24 * 60 * 60 * 1000) { // 7일
if (!this.isInitialized) await this.init();
try {
const cutoff = Date.now() - maxAge;
let deletedCount = 0;
for (const [packageName, entry] of Object.entries(this.index)) {
if (entry.accessedAt < cutoff) {
this.delete(packageName);
deletedCount++;
}
}
if (deletedCount > 0) {
console.log(`🧹 Cleaned ${deletedCount} old cache entries`);
}
} catch (error) {
console.warn(`Disk cache cleanup error:`, error.message);
}
}
async getStats() {
if (!this.isInitialized) await this.init();
try {
const entries = Object.keys(this.index).length;
let totalSize = 0;
// 캐시 디렉터리 크기 계산
if (fs.existsSync(this.cacheDir)) {
const files = fs.readdirSync(this.cacheDir);
for (const file of files) {
const filePath = path.join(this.cacheDir, file);
if (fs.existsSync(filePath)) {
totalSize += fs.statSync(filePath).size;
}
}
}
return {
entries,
diskSize: `${Math.round(totalSize / 1024)} KB`,
cacheDir: this.cacheDir
};
} catch (error) {
return { entries: 0, diskSize: '0 KB', error: error.message };
}
}
async close() {
// JSON 파일 기반이므로 특별히 닫을 것 없음
this.saveIndex();
}
}
// Smart Cache Manager (통합 관리)
export class SmartCacheManager {
constructor(options = {}) {
this.memoryCache = new LRUCache(options.memoryCacheSize || 1000);
this.diskCache = new DiskCache(options.cacheDir);
this.networkHits = 0;
this.diskHits = 0;
this.memoryHits = 0;
this.totalRequests = 0;
// 통계
this.stats = {
networkHits: 0,
diskHits: 0,
memoryHits: 0,
totalRequests: 0
};
}
async init() {
await this.diskCache.init();
}
async get(packageName, ttl = 3600000) {
this.stats.totalRequests++;
// 1단계: Memory 캐시 확인
const memoryResult = this.memoryCache.get(packageName);
if (memoryResult) {
this.stats.memoryHits++;
return { data: memoryResult, source: 'memory' };
}
// 2단계: Disk 캐시 확인
const diskResult = await this.diskCache.get(packageName, ttl);
if (diskResult) {
this.stats.diskHits++;
// Memory 캐시에 저장
this.memoryCache.set(packageName, diskResult);
return { data: diskResult, source: 'disk' };
}
// 3단계: Network에서 가져와야 함
this.stats.networkHits++;
return { data: null, source: 'network' };
}
async set(packageName, versionData) {
// Memory와 Disk 양쪽에 저장
this.memoryCache.set(packageName, versionData);
await this.diskCache.set(packageName, versionData);
}
async cleanup() {
await this.diskCache.cleanup();
}
getHitRate() {
if (this.stats.totalRequests === 0) return 0;
const cacheHits = this.stats.memoryHits + this.stats.diskHits;
return Math.round(cacheHits / this.stats.totalRequests * 100);
}
async getDetailedStats() {
const memoryStats = this.memoryCache.getStats();
const diskStats = await this.diskCache.getStats();
return {
memory: memoryStats,
disk: diskStats,
performance: {
totalRequests: this.stats.totalRequests,
memoryHits: this.stats.memoryHits,
diskHits: this.stats.diskHits,
networkHits: this.stats.networkHits,
hitRate: `${this.getHitRate()}%`,
breakdown: {
memory: `${Math.round(this.stats.memoryHits / (this.stats.totalRequests || 1) * 100)}%`,
disk: `${Math.round(this.stats.diskHits / (this.stats.totalRequests || 1) * 100)}%`,
network: `${Math.round(this.stats.networkHits / (this.stats.totalRequests || 1) * 100)}%`
}
}
};
}
async close() {
await this.diskCache.close();
}
}
// 기본 인스턴스 (싱글톤 패턴)
let globalCacheManager = null;
export function getCacheManager() {
if (!globalCacheManager) {
globalCacheManager = new SmartCacheManager();
}
return globalCacheManager;
}
export async function initializeCache() {
const cache = getCacheManager();
await cache.init();
return cache;
}