diff --git a/src/app.ts b/src/app.ts index 0225f162..a44cace5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,8 @@ import { AppDataSource } from "./database/data-source"; import { RegisterRoutes } from "./routes"; import { OrganizationController } from "./controllers/OrganizationController"; import logMiddleware from "./middlewares/logs"; -import { logMemoryStore } from "./utils/log-memory-store"; +import { logMemoryStore } from "./utils/LogMemoryStore"; +import { orgStructureCache } from "./utils/OrgStructureCache"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; import { DateSerializer } from "./interfaces/date-serializer"; @@ -24,6 +25,9 @@ async function main() { // Initialize LogMemoryStore after database is ready logMemoryStore.initialize(); + // Initialize OrgStructureCache after database is ready + orgStructureCache.initialize(); + // Setup custom Date serialization for local timezone DateSerializer.setupDateSerialization(); diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8b027cdb..ad1f71d5 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -51,6 +51,7 @@ import { CreatePosMasterHistoryEmployee, CreatePosMasterHistoryOfficer, } from "../services/PositionService"; +import { orgStructureCache } from "../utils/OrgStructureCache"; @Route("api/v1/org") @Tags("Organization") @@ -1196,6 +1197,12 @@ export class OrganizationController extends Controller { rootId = posMaster.orgRootId; } + // OPTIMIZED: Check cache first + const cachedResponse = await orgStructureCache.get(id, rootId); + if (cachedResponse) { + return new HttpSuccess(cachedResponse); + } + // OPTIMIZED: Get all position counts in ONE query (closed) // const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } = // await getPositionCounts(id); @@ -1527,6 +1534,9 @@ export class OrganizationController extends Controller { }; }); + // OPTIMIZED: Cache the result + await orgStructureCache.set(id, rootId, formattedData); + return new HttpSuccess(formattedData); } diff --git a/src/middlewares/logs.ts b/src/middlewares/logs.ts index 99e7e31a..1bff2060 100644 --- a/src/middlewares/logs.ts +++ b/src/middlewares/logs.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from "express"; import { Client } from "@elastic/elasticsearch"; -import { logMemoryStore } from "../utils/log-memory-store"; +import { logMemoryStore } from "../utils/LogMemoryStore"; if (!process.env.ELASTICSEARCH_INDEX) { throw new Error("Require ELASTICSEARCH_INDEX to store log."); diff --git a/src/utils/log-memory-store.ts b/src/utils/LogMemoryStore.ts similarity index 100% rename from src/utils/log-memory-store.ts rename to src/utils/LogMemoryStore.ts diff --git a/src/utils/OrgStructureCache.ts b/src/utils/OrgStructureCache.ts new file mode 100644 index 00000000..33fa19e2 --- /dev/null +++ b/src/utils/OrgStructureCache.ts @@ -0,0 +1,96 @@ +interface CacheEntry { + data: any; + cachedAt: Date; +} + +class OrgStructureCache { + private cache: Map = new Map(); + private readonly CACHE_TTL = 10 * 60 * 1000; // 10 minutes + private isInitialized = false; + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor() { + // Don't auto-initialize - wait for AppDataSource to be ready + } + + initialize() { + if (this.isInitialized) return; + + this.isInitialized = true; + // Cleanup expired entries every 10 minutes + this.cleanupTimer = setInterval(() => { + this.cleanup(); + }, this.CACHE_TTL); + + console.log("[OrgStructureCache] Initialized"); + } + + private generateKey(revisionId: string, rootId?: string): string { + return `org-structure-${revisionId}-${rootId || "all"}`; + } + + private cleanup() { + const now = Date.now(); + let cleaned = 0; + + for (const [key, entry] of this.cache.entries()) { + const age = now - entry.cachedAt.getTime(); + if (age > this.CACHE_TTL) { + this.cache.delete(key); + cleaned++; + } + } + + if (cleaned > 0) { + console.log(`[OrgStructureCache] Cleaned ${cleaned} expired entries`); + } + } + + async get(revisionId: string, rootId?: string): Promise { + const key = this.generateKey(revisionId, rootId); + const entry = this.cache.get(key); + + if (!entry) { + return null; + } + + // Check if expired + const age = Date.now() - entry.cachedAt.getTime(); + if (age > this.CACHE_TTL) { + this.cache.delete(key); + return null; + } + + console.log(`[OrgStructureCache] HIT: ${key}`); + return entry.data; + } + + async set(revisionId: string, rootId: string | undefined, data: any): Promise { + const key = this.generateKey(revisionId, rootId); + this.cache.set(key, { + data, + cachedAt: new Date(), + }); + console.log(`[OrgStructureCache] SET: ${key}`); + } + + invalidate(revisionId: string): void { + // Invalidate all entries for this revision + for (const key of this.cache.keys()) { + if (key.startsWith(`org-structure-${revisionId}`)) { + this.cache.delete(key); + } + } + console.log(`[OrgStructureCache] INVALIDATED: ${revisionId}`); + } + + destroy() { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + this.cache.clear(); + } +} + +export const orgStructureCache = new OrgStructureCache();