import { AppDataSource } from "../database/data-source"; import { OrgRevision } from "../entities/OrgRevision"; import { Profile } from "../entities/Profile"; import { PosMaster } from "../entities/PosMaster"; interface LogCacheData { currentRevision: OrgRevision | null; profileCache: Map; // keycloak → Profile rootIdCache: Map; // profileId → rootId updatedAt: Date; } class LogMemoryStore { private cache: LogCacheData = { currentRevision: null, profileCache: new Map(), rootIdCache: new Map(), updatedAt: new Date(), }; private readonly CACHE_TTL = 60 * 60 * 1000; // 60 minutes private isInitialized = false; constructor() { // ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready } // เริ่มต้น cache หลังจาก TypeORM initialize เสร็จ initialize() { if (this.isInitialized) { console.log("[LogMemoryStore] Already initialized"); return; } this.isInitialized = true; this.refreshCache(); console.log( "[LogMemoryStore] Initialized with", this.CACHE_TTL / 1000 / 60, "minute TTL (passive cleanup on access)", ); } private async refreshCache() { try { // Refresh revision cache const repoRevision = AppDataSource.getRepository(OrgRevision); const revision = await repoRevision.findOne({ where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false, }, }); this.cache.currentRevision = revision; // Clear on-demand caches (they will be rebuilt as needed) this.cache.profileCache.clear(); this.cache.rootIdCache.clear(); this.cache.updatedAt = new Date(); console.log("[LogMemoryStore] Cache refreshed at", this.cache.updatedAt.toISOString()); } catch (error) { console.error("[LogMemoryStore] Error refreshing cache:", error); } } // Check if cache is stale and refresh if needed private async checkAndRefreshIfNeeded() { const now = new Date(); const age = now.getTime() - this.cache.updatedAt.getTime(); if (age > this.CACHE_TTL) { console.log( "[LogMemoryStore] Cache is stale (age:", Math.round(age / 1000 / 60), "minutes), refreshing...", ); await this.refreshCache(); } } getCurrentRevision(): OrgRevision | null { // Check for stale data (fire and forget) this.checkAndRefreshIfNeeded(); return this.cache.currentRevision; } getLastUpdateTime(): Date { return this.cache.updatedAt; } /** * Get Profile by keycloak ID with caching */ async getProfileByKeycloak(keycloak: string): Promise { // Check for stale data await this.checkAndRefreshIfNeeded(); // Check cache first if (this.cache.profileCache.has(keycloak)) { return this.cache.profileCache.get(keycloak)!; } // Fetch from database const repoProfile = AppDataSource.getRepository(Profile); const profile = await repoProfile.findOne({ where: { keycloak }, }); // Cache the result if (profile) { this.cache.profileCache.set(keycloak, profile); } return profile; } /** * Get RootId by profileId with caching */ async getRootIdByProfileId(profileId: string | undefined): Promise { if (!profileId) return null; // Check for stale data await this.checkAndRefreshIfNeeded(); // Check cache first if (this.cache.rootIdCache.has(profileId)) { return this.cache.rootIdCache.get(profileId)!; } // Fetch from database const repoPosmaster = AppDataSource.getRepository(PosMaster); const revision = this.getCurrentRevision(); // const posMaster = await repoPosmaster.findOne({ where: { current_holderId: profileId, orgRevisionId: revision?.id, }, relations: ["orgRoot"], select: { orgRoot: { ancestorDNA: true, }, }, }); const rootId = posMaster?.orgRoot?.ancestorDNA ?? null; // Cache the result if (rootId) { this.cache.rootIdCache.set(profileId, rootId); } return rootId; } // สำหรับ shutdown destroy() { // No active timer to clear console.log("[LogMemoryStore] Destroyed"); } } export const logMemoryStore = new LogMemoryStore();