ERP-node/frontend/lib/services/dataflowCache.ts

285 lines
7.6 KiB
TypeScript
Raw Normal View History

2025-09-18 10:05:50 +09:00
/**
* 🔥 최적화: 데이터플로우
*
*
* 1ms .
*/
import { ButtonDataflowConfig } from "@/types/screen";
import { apiClient } from "@/lib/api/client";
export interface CachedDataflowConfig {
config: ButtonDataflowConfig;
timestamp: number;
hits: number; // 캐시 히트 횟수 (통계용)
}
export interface CacheMetrics {
totalRequests: number;
cacheHits: number;
cacheMisses: number;
hitRate: number; // 히트율 (%)
averageResponseTime: number; // 평균 응답 시간 (ms)
}
/**
* 🔥 L1 (1ms )
*
* - TTL: 5분 (300)
* -
* -
*/
export class DataflowConfigCache {
private memoryCache = new Map<string, CachedDataflowConfig>();
private readonly TTL = 5 * 60 * 1000; // 5분 TTL
private metrics: CacheMetrics = {
totalRequests: 0,
cacheHits: 0,
cacheMisses: 0,
hitRate: 0,
averageResponseTime: 0,
};
/**
* 🔥 ( )
*/
async getConfig(buttonId: string): Promise<ButtonDataflowConfig | null> {
const startTime = performance.now();
this.metrics.totalRequests++;
const cacheKey = `button_dataflow_${buttonId}`;
try {
// L1: 메모리 캐시 확인 (1ms)
if (this.memoryCache.has(cacheKey)) {
const cached = this.memoryCache.get(cacheKey)!;
// TTL 확인
if (Date.now() - cached.timestamp < this.TTL) {
cached.hits++;
this.metrics.cacheHits++;
this.updateHitRate();
const responseTime = performance.now() - startTime;
this.updateAverageResponseTime(responseTime);
console.log(`⚡ Cache hit: ${buttonId} (${responseTime.toFixed(2)}ms)`);
return cached.config;
} else {
// TTL 만료된 캐시 제거
this.memoryCache.delete(cacheKey);
}
}
// L2: 서버에서 로드 (100-300ms)
console.log(`🌐 Loading from server: ${buttonId}`);
const serverConfig = await this.loadFromServer(buttonId);
// 캐시에 저장
if (serverConfig) {
this.memoryCache.set(cacheKey, {
config: serverConfig,
timestamp: Date.now(),
hits: 1,
});
}
this.metrics.cacheMisses++;
this.updateHitRate();
const responseTime = performance.now() - startTime;
this.updateAverageResponseTime(responseTime);
console.log(`📡 Server response: ${buttonId} (${responseTime.toFixed(2)}ms)`);
return serverConfig;
} catch (error) {
console.error(`❌ Failed to get config for button ${buttonId}:`, error);
const responseTime = performance.now() - startTime;
this.updateAverageResponseTime(responseTime);
return null;
}
}
/**
* 🔥
*/
private async loadFromServer(buttonId: string): Promise<ButtonDataflowConfig | null> {
try {
const response = await apiClient.get(`/api/button-dataflow/config/${buttonId}`);
if (response.data.success) {
return response.data.data as ButtonDataflowConfig;
}
return null;
} catch (error) {
// 404는 정상 상황 (설정이 없는 버튼)
if (error.response?.status === 404) {
return null;
}
throw error;
}
}
/**
* 🔥 ( )
*/
async updateConfig(buttonId: string, config: ButtonDataflowConfig): Promise<void> {
const cacheKey = `button_dataflow_${buttonId}`;
try {
// 서버에 저장
await apiClient.put(`/api/button-dataflow/config/${buttonId}`, config);
// 캐시 업데이트
this.memoryCache.set(cacheKey, {
config,
timestamp: Date.now(),
hits: 0,
});
console.log(`💾 Config updated: ${buttonId}`);
} catch (error) {
console.error(`❌ Failed to update config for button ${buttonId}:`, error);
throw error;
}
}
/**
* 🔥
*/
invalidateCache(buttonId: string): void {
const cacheKey = `button_dataflow_${buttonId}`;
this.memoryCache.delete(cacheKey);
console.log(`🗑️ Cache invalidated: ${buttonId}`);
}
/**
* 🔥
*/
clearAllCache(): void {
this.memoryCache.clear();
console.log(`🗑️ All cache cleared`);
}
/**
* 🔥 ( )
*/
invalidateDiagramCache(diagramId: number): void {
let invalidatedCount = 0;
for (const [key, cached] of this.memoryCache.entries()) {
if (cached.config.selectedDiagramId === diagramId) {
this.memoryCache.delete(key);
invalidatedCount++;
}
}
if (invalidatedCount > 0) {
console.log(`🗑️ Invalidated ${invalidatedCount} caches for diagram ${diagramId}`);
}
}
/**
* 🔥
*/
getMetrics(): CacheMetrics {
return { ...this.metrics };
}
/**
* 🔥 ()
*/
getCacheInfo(): Array<{
buttonId: string;
config: ButtonDataflowConfig;
age: number; // 캐시된지 몇 분 경과
hits: number;
ttlRemaining: number; // 남은 TTL (초)
}> {
const now = Date.now();
const result: Array<any> = [];
for (const [key, cached] of this.memoryCache.entries()) {
const buttonId = key.replace("button_dataflow_", "");
const age = Math.floor((now - cached.timestamp) / 1000 / 60); // 분
const ttlRemaining = Math.max(0, Math.floor((this.TTL - (now - cached.timestamp)) / 1000)); // 초
result.push({
buttonId,
config: cached.config,
age,
hits: cached.hits,
ttlRemaining,
});
}
return result.sort((a, b) => b.hits - a.hits); // 히트 수 기준 내림차순
}
/**
* 🔥 TTL ( )
*/
cleanupExpiredCache(): number {
const now = Date.now();
let cleanedCount = 0;
for (const [key, cached] of this.memoryCache.entries()) {
if (now - cached.timestamp >= this.TTL) {
this.memoryCache.delete(key);
cleanedCount++;
}
}
if (cleanedCount > 0) {
console.log(`🧹 Cleaned up ${cleanedCount} expired cache entries`);
}
return cleanedCount;
}
/**
*
*/
private updateHitRate(): void {
this.metrics.hitRate =
this.metrics.totalRequests > 0 ? (this.metrics.cacheHits / this.metrics.totalRequests) * 100 : 0;
}
/**
* ( )
*/
private updateAverageResponseTime(responseTime: number): void {
if (this.metrics.averageResponseTime === 0) {
this.metrics.averageResponseTime = responseTime;
} else {
// 이동 평균 (기존 90% + 새로운 값 10%)
this.metrics.averageResponseTime = this.metrics.averageResponseTime * 0.9 + responseTime * 0.1;
}
}
}
// 🔥 전역 싱글톤 인스턴스
export const dataflowConfigCache = new DataflowConfigCache();
// 🔥 5분마다 만료된 캐시 정리
if (typeof window !== "undefined") {
setInterval(
() => {
dataflowConfigCache.cleanupExpiredCache();
},
5 * 60 * 1000,
); // 5분
}
// 🔥 개발 모드에서 캐시 정보를 전역 객체에 노출
if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
(window as any).dataflowCache = {
getMetrics: () => dataflowConfigCache.getMetrics(),
getCacheInfo: () => dataflowConfigCache.getCacheInfo(),
clearCache: () => dataflowConfigCache.clearAllCache(),
};
}