UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

527 lines (447 loc) 13.2 kB
import { CacheEntry, CacheLevel, CacheProvider, ContentType, CacheStatus, } from "./types"; export abstract class BaseCacheProvider { protected level: CacheLevel; protected provider: CacheProvider; constructor(level: CacheLevel, provider: CacheProvider) { this.level = level; this.provider = provider; } abstract get<T>(key: string): Promise<CacheEntry<T> | null>; abstract set<T>(entry: CacheEntry<T>): Promise<void>; abstract delete(key: string): Promise<boolean>; abstract clear(): Promise<void>; abstract keys(pattern?: string): Promise<string[]>; abstract exists(key: string): Promise<boolean>; abstract size(): Promise<number>; abstract healthCheck(): Promise<boolean>; } /** * 브라우저 캐시 프로바이더 (localStorage/sessionStorage) */ export class BrowserCacheProvider extends BaseCacheProvider { private storage: Storage; constructor(useSessionStorage = false) { super("browser", "browser"); this.storage = useSessionStorage ? sessionStorage : localStorage; } async get<T>(key: string): Promise<CacheEntry<T> | null> { try { const item = this.storage.getItem(`cache:${key}`); if (!item) return null; const entry = JSON.parse(item); entry.createdAt = new Date(entry.createdAt); entry.expiresAt = new Date(entry.expiresAt); entry.lastAccessed = new Date(entry.lastAccessed); // 만료 확인 if (entry.expiresAt < new Date()) { await this.delete(key); return null; } // 액세스 업데이트 entry.hits++; entry.lastAccessed = new Date(); this.storage.setItem(`cache:${key}`, JSON.stringify(entry)); return entry; } catch (error) { console.error("브라우저 캐시 조회 오류:", error); return null; } } async set<T>(entry: CacheEntry<T>): Promise<void> { try { this.storage.setItem(`cache:${entry.key}`, JSON.stringify(entry)); } catch (error) { // 저장 공간 부족 시 오래된 항목 정리 if (error.name === "QuotaExceededError") { await this.evictOldEntries(); this.storage.setItem(`cache:${entry.key}`, JSON.stringify(entry)); } else { throw error; } } } async delete(key: string): Promise<boolean> { this.storage.removeItem(`cache:${key}`); return true; } async clear(): Promise<void> { const keys = await this.keys(); keys.forEach((key) => this.storage.removeItem(key)); } async keys(pattern?: string): Promise<string[]> { const keys: string[] = []; for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); if (key?.startsWith("cache:")) { const cacheKey = key.replace("cache:", ""); if (!pattern || cacheKey.includes(pattern)) { keys.push(key); } } } return keys; } async exists(key: string): Promise<boolean> { return this.storage.getItem(`cache:${key}`) !== null; } async size(): Promise<number> { return (await this.keys()).length; } async healthCheck(): Promise<boolean> { try { const testKey = "health-check"; this.storage.setItem(testKey, "test"); this.storage.removeItem(testKey); return true; } catch { return false; } } private async evictOldEntries(): Promise<void> { const keys = await this.keys(); const entries: Array<{ key: string; lastAccessed: Date }> = []; // 모든 엔트리의 최종 액세스 시간 수집 for (const key of keys) { try { const item = this.storage.getItem(key); if (item) { const entry = JSON.parse(item); entries.push({ key, lastAccessed: new Date(entry.lastAccessed), }); } } catch { // 파싱 오류 시 해당 키 삭제 this.storage.removeItem(key); } } // 오래된 순으로 정렬하여 25% 제거 entries.sort((a, b) => a.lastAccessed.getTime() - b.lastAccessed.getTime()); const toEvict = Math.ceil(entries.length * 0.25); for (let i = 0; i < toEvict; i++) { this.storage.removeItem(entries[i].key); } } } /** * 메모리 캐시 프로바이더 (인메모리) */ export class MemoryCacheProvider extends BaseCacheProvider { private cache = new Map<string, CacheEntry<any>>(); private maxSize: number; constructor(maxSize = 1000) { super("memory", "memory"); this.maxSize = maxSize; this.startCleanupTimer(); } async get<T>(key: string): Promise<CacheEntry<T> | null> { const entry = this.cache.get(key); if (!entry) return null; // 만료 확인 if (entry.expiresAt < new Date()) { this.cache.delete(key); return null; } // 액세스 업데이트 entry.hits++; entry.lastAccessed = new Date(); return entry; } async set<T>(entry: CacheEntry<T>): Promise<void> { // 크기 제한 확인 if (this.cache.size >= this.maxSize) { await this.evictLRU(); } this.cache.set(entry.key, entry); } async delete(key: string): Promise<boolean> { return this.cache.delete(key); } async clear(): Promise<void> { this.cache.clear(); } async keys(pattern?: string): Promise<string[]> { const keys = Array.from(this.cache.keys()); return pattern ? keys.filter((key) => key.includes(pattern)) : keys; } async exists(key: string): Promise<boolean> { return this.cache.has(key); } async size(): Promise<number> { return this.cache.size; } async healthCheck(): Promise<boolean> { return true; // 메모리 캐시는 항상 건강함 } private async evictLRU(): Promise<void> { // LRU 방식으로 오래된 항목 제거 let oldestKey = ""; let oldestTime = Date.now(); for (const [key, entry] of this.cache.entries()) { if (entry.lastAccessed.getTime() < oldestTime) { oldestTime = entry.lastAccessed.getTime(); oldestKey = key; } } if (oldestKey) { this.cache.delete(oldestKey); } } private startCleanupTimer(): void { // 5분마다 만료된 항목 정리 setInterval(() => { const now = new Date(); for (const [key, entry] of this.cache.entries()) { if (entry.expiresAt < now) { this.cache.delete(key); } } }, 5 * 60 * 1000); } } /** * Redis 캐시 프로바이더 (서버 사이드) */ export class RedisCacheProvider extends BaseCacheProvider { private client: any; // Redis 클라이언트 private connected = false; constructor(redisClient?: any) { super("server", "redis"); this.client = redisClient; } async connect(config?: { host: string; port: number; password?: string; db?: number; }): Promise<void> { if (!this.client && config) { // Redis 클라이언트 생성 (실제 구현에서는 redis 라이브러리 사용) console.log("Redis 연결 설정:", config); this.connected = true; } } async get<T>(key: string): Promise<CacheEntry<T> | null> { if (!this.connected) return null; try { const data = await this.client?.get(`cache:${key}`); if (!data) return null; const entry = JSON.parse(data); entry.createdAt = new Date(entry.createdAt); entry.expiresAt = new Date(entry.expiresAt); entry.lastAccessed = new Date(entry.lastAccessed); // 만료 확인 if (entry.expiresAt < new Date()) { await this.delete(key); return null; } // 액세스 업데이트 entry.hits++; entry.lastAccessed = new Date(); await this.client?.set(`cache:${key}`, JSON.stringify(entry)); return entry; } catch (error) { console.error("Redis 캐시 조회 오류:", error); return null; } } async set<T>(entry: CacheEntry<T>): Promise<void> { if (!this.connected) return; try { const ttl = Math.ceil((entry.expiresAt.getTime() - Date.now()) / 1000); await this.client?.setex( `cache:${entry.key}`, ttl, JSON.stringify(entry) ); } catch (error) { console.error("Redis 캐시 저장 오류:", error); throw error; } } async delete(key: string): Promise<boolean> { if (!this.connected) return false; try { const result = await this.client?.del(`cache:${key}`); return result > 0; } catch (error) { console.error("Redis 캐시 삭제 오류:", error); return false; } } async clear(): Promise<void> { if (!this.connected) return; try { const keys = await this.keys(); if (keys.length > 0) { await this.client?.del(...keys); } } catch (error) { console.error("Redis 캐시 클리어 오류:", error); } } async keys(pattern = "*"): Promise<string[]> { if (!this.connected) return []; try { return (await this.client?.keys(`cache:${pattern}`)) || []; } catch (error) { console.error("Redis 키 조회 오류:", error); return []; } } async exists(key: string): Promise<boolean> { if (!this.connected) return false; try { const result = await this.client?.exists(`cache:${key}`); return result > 0; } catch (error) { console.error("Redis 존재 확인 오류:", error); return false; } } async size(): Promise<number> { if (!this.connected) return 0; try { const keys = await this.keys(); return keys.length; } catch (error) { console.error("Redis 크기 조회 오류:", error); return 0; } } async healthCheck(): Promise<boolean> { if (!this.connected) return false; try { await this.client?.ping(); return true; } catch { return false; } } } /** * CDN 캐시 프로바이더 (Cloudflare API) */ export class CDNCacheProvider extends BaseCacheProvider { private apiToken: string; private zoneId: string; constructor(apiToken: string, zoneId: string) { super("cdn", "cloudflare"); this.apiToken = apiToken; this.zoneId = zoneId; } async get<T>(_key: string): Promise<CacheEntry<T> | null> { // CDN 캐시는 직접 조회할 수 없음 return null; } async set<T>(_entry: CacheEntry<T>): Promise<void> { // CDN 캐시는 요청에 따라 자동으로 설정됨 } async delete(key: string): Promise<boolean> { try { // Cloudflare API를 사용하여 캐시 무효화 const response = await fetch( `https://api.cloudflare.com/client/v4/zones/${this.zoneId}/purge_cache`, { method: "POST", headers: { Authorization: `Bearer ${this.apiToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ files: [key], }), } ); return response.ok; } catch (error) { console.error("CDN 캐시 무효화 오류:", error); return false; } } async clear(): Promise<void> { try { // 전체 캐시 무효화 await fetch( `https://api.cloudflare.com/client/v4/zones/${this.zoneId}/purge_cache`, { method: "POST", headers: { Authorization: `Bearer ${this.apiToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ purge_everything: true, }), } ); } catch (error) { console.error("CDN 전체 캐시 클리어 오류:", error); } } async keys(_pattern?: string): Promise<string[]> { // CDN에서는 키 목록을 조회할 수 없음 return []; } async exists(_key: string): Promise<boolean> { // CDN에서는 존재 여부를 직접 확인할 수 없음 return false; } async size(): Promise<number> { // CDN에서는 크기를 직접 측정할 수 없음 return 0; } async healthCheck(): Promise<boolean> { try { // Cloudflare API 상태 확인 const response = await fetch( `https://api.cloudflare.com/client/v4/zones/${this.zoneId}`, { headers: { Authorization: `Bearer ${this.apiToken}`, }, } ); return response.ok; } catch { return false; } } } /** * 프로바이더 팩토리 */ export class CacheProviderFactory { static createProvider( level: CacheLevel, provider: CacheProvider, options?: any ): BaseCacheProvider { switch (provider) { case "browser": return new BrowserCacheProvider(options?.useSessionStorage); case "memory": return new MemoryCacheProvider(options?.maxSize); case "redis": return new RedisCacheProvider(options?.client); case "cloudflare": return new CDNCacheProvider(options?.apiToken, options?.zoneId); default: throw new Error(`지원하지 않는 캐시 프로바이더: ${provider}`); } } } export { BaseCacheProvider, BrowserCacheProvider, MemoryCacheProvider, RedisCacheProvider, CDNCacheProvider, };