Merge branch 'fix/optimization-detailSuperAdmin' into develop
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
* fix/optimization-detailSuperAdmin:
fix: api /super-admin/{id} memory cache
fix: query use Promise all
fix: Api GET /super-admin/{id}
fix: เพิ่ม Graceful Shutdown - ป้องกัน connection in app file, Log Mnddleware + Memory Store
fix: connection pool settings
feat: optimize detailSuperAdmin API to fix database connection issue
This commit is contained in:
commit
07535c9c53
7 changed files with 934 additions and 796 deletions
50
src/app.ts
50
src/app.ts
|
|
@ -11,6 +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/LogMemoryStore";
|
||||
import { orgStructureCache } from "./utils/OrgStructureCache";
|
||||
import { CommandController } from "./controllers/CommandController";
|
||||
import { ProfileSalaryController } from "./controllers/ProfileSalaryController";
|
||||
import { DateSerializer } from "./interfaces/date-serializer";
|
||||
|
|
@ -20,6 +22,12 @@ import { initWebSocket } from "./services/webSocket";
|
|||
async function main() {
|
||||
await AppDataSource.initialize();
|
||||
|
||||
// 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();
|
||||
|
||||
|
|
@ -93,7 +101,7 @@ async function main() {
|
|||
});
|
||||
|
||||
// app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));
|
||||
app.listen(
|
||||
const server = app.listen(
|
||||
APP_PORT,
|
||||
APP_HOST,
|
||||
() => (
|
||||
|
|
@ -111,6 +119,46 @@ async function main() {
|
|||
}
|
||||
|
||||
runMessageQueue();
|
||||
|
||||
// Graceful Shutdown
|
||||
const gracefulShutdown = async (signal: string) => {
|
||||
console.log(`\n[APP] ${signal} received. Starting graceful shutdown...`);
|
||||
|
||||
// Stop accepting new connections
|
||||
server.close(() => {
|
||||
console.log("[APP] HTTP server closed");
|
||||
});
|
||||
|
||||
// Force close after timeout
|
||||
const shutdownTimeout = setTimeout(() => {
|
||||
console.error("[APP] Forced shutdown after timeout");
|
||||
process.exit(1);
|
||||
}, 30000); // 30 seconds timeout
|
||||
|
||||
try {
|
||||
// Close database connections
|
||||
if (AppDataSource.isInitialized) {
|
||||
await AppDataSource.destroy();
|
||||
console.log("[APP] Database connections closed");
|
||||
}
|
||||
|
||||
// Destroy LogMemoryStore
|
||||
logMemoryStore.destroy();
|
||||
console.log("[APP] LogMemoryStore destroyed");
|
||||
|
||||
clearTimeout(shutdownTimeout);
|
||||
console.log("[APP] Graceful shutdown completed");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("[APP] Error during shutdown:", error);
|
||||
clearTimeout(shutdownTimeout);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for shutdown signals
|
||||
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -47,8 +47,8 @@ export const AppDataSource = new DataSource({
|
|||
logging: true,
|
||||
// timezone: "Z",
|
||||
entities:
|
||||
process.env.NODE_ENV !== "production"
|
||||
? ["src/entities/**/*.ts"]
|
||||
process.env.NODE_ENV !== "production"
|
||||
? ["src/entities/**/*.ts"]
|
||||
: ["dist/entities/**/*{.ts,.js}"],
|
||||
migrations:
|
||||
process.env.NODE_ENV !== "production"
|
||||
|
|
@ -56,6 +56,16 @@ export const AppDataSource = new DataSource({
|
|||
: ["dist/migration/**/*{.ts,.js}"],
|
||||
subscribers: [],
|
||||
logger: new MyCustomLogger(),
|
||||
// Connection pool settings to prevent connection exhaustion
|
||||
extra: {
|
||||
connectionLimit: +(process.env.DB_CONNECTION_LIMIT || 50),
|
||||
maxIdle: +(process.env.DB_MAX_IDLE || 10),
|
||||
idleTimeout: +(process.env.DB_IDLE_TIMEOUT || 60000),
|
||||
timezone: "+07:00", // Bangkok timezone (UTC+7)
|
||||
},
|
||||
// TypeORM pool settings
|
||||
poolSize: +(process.env.DB_POOL_SIZE || 10),
|
||||
maxQueryExecutionTime: +(process.env.DB_MAX_QUERY_TIME || 3000),
|
||||
});
|
||||
|
||||
// export default database;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import { Client } from "@elastic/elasticsearch";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import { PosMaster } from "../entities/PosMaster";
|
||||
import { OrgRevision } from "../entities/OrgRevision";
|
||||
import { Profile } from "../entities/Profile";
|
||||
import { logMemoryStore } from "../utils/LogMemoryStore";
|
||||
|
||||
if (!process.env.ELASTICSEARCH_INDEX) {
|
||||
throw new Error("Require ELASTICSEARCH_INDEX to store log.");
|
||||
|
|
@ -27,9 +24,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
|||
if (!req.url.startsWith("/api/")) return next();
|
||||
|
||||
let data: any;
|
||||
const repoPosmaster = AppDataSource.getRepository(PosMaster);
|
||||
const repoProfile = AppDataSource.getRepository(Profile);
|
||||
const repoRevision = AppDataSource.getRepository(OrgRevision);
|
||||
|
||||
const originalJson = res.json;
|
||||
|
||||
|
|
@ -42,13 +36,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
|||
|
||||
req.app.locals.logData = {};
|
||||
|
||||
const revision = await repoRevision.findOne({
|
||||
where: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
});
|
||||
|
||||
res.on("finish", async () => {
|
||||
try {
|
||||
if (!req.url.startsWith("/api/")) return;
|
||||
|
|
@ -69,7 +56,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
|||
if (req.url.startsWith("/api/v1/org/profile/")) system = "registry";
|
||||
if (req.url.startsWith("/api/v1/org/profile-employee/")) system = "registry";
|
||||
if (req.url.startsWith("/api/v1/org/profile-temp/")) system = "registry";
|
||||
|
||||
|
||||
if (req.url.startsWith("/api/v1/org/commandType/admin")) system = "admin";
|
||||
if (req.url.startsWith("/api/v1/org/commandSys/")) system = "admin";
|
||||
if (req.url.startsWith("/api/v1/org/commandSalary/")) system = "admin";
|
||||
|
|
@ -79,16 +66,15 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
|||
if (req.url.startsWith("/api/v1/org/keycloak/")) system = "registry";
|
||||
|
||||
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4;
|
||||
const profileByKeycloak = await repoProfile.findOne({
|
||||
where: { keycloak: req.app.locals.logData.userId },
|
||||
});
|
||||
const rootId = await repoPosmaster.findOne({
|
||||
where: {
|
||||
current_holderId: profileByKeycloak?.id,
|
||||
orgRevisionId: revision?.id,
|
||||
},
|
||||
select: ["orgRootId"],
|
||||
});
|
||||
|
||||
// Get profile from cache
|
||||
const profileByKeycloak = await logMemoryStore.getProfileByKeycloak(
|
||||
req.app.locals.logData.userId,
|
||||
);
|
||||
|
||||
// Get rootId from cache
|
||||
const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id);
|
||||
// console.log("ancestorDNA:", rootId);
|
||||
|
||||
if (level === 1 && res.statusCode < 500) return;
|
||||
if (level === 2 && res.statusCode < 400) return;
|
||||
|
|
@ -96,7 +82,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
|
|||
const obj = {
|
||||
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
|
||||
ip: req.ip,
|
||||
rootId: rootId?.orgRootId ?? null,
|
||||
rootId: rootId ?? null,
|
||||
systemName: system,
|
||||
startTimeStamp: timestamp,
|
||||
endTimeStamp: new Date().toISOString(),
|
||||
|
|
|
|||
278
src/services/OrganizationService.ts
Normal file
278
src/services/OrganizationService.ts
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
import { AppDataSource } from "../database/data-source";
|
||||
import { PosMaster } from "../entities/PosMaster";
|
||||
|
||||
// Helper function to get aggregated position counts
|
||||
export async function getPositionCounts(orgRevisionId: string) {
|
||||
// Query all posMaster data for this revision with aggregation
|
||||
const rawData = await AppDataSource.getRepository(PosMaster)
|
||||
.createQueryBuilder("pos")
|
||||
.select([
|
||||
"pos.orgRootId",
|
||||
"pos.orgChild1Id",
|
||||
"pos.orgChild2Id",
|
||||
"pos.orgChild3Id",
|
||||
"pos.orgChild4Id",
|
||||
"pos.current_holderId",
|
||||
"pos.next_holderId",
|
||||
])
|
||||
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
|
||||
.getMany();
|
||||
|
||||
// Helper to check if value is null or empty string
|
||||
const isNull = (val: any) => val === null || val === "";
|
||||
|
||||
// Build maps for each level
|
||||
const orgRootMap = new Map<string, any>();
|
||||
const orgChild1Map = new Map<string, any>();
|
||||
const orgChild2Map = new Map<string, any>();
|
||||
const orgChild3Map = new Map<string, any>();
|
||||
const orgChild4Map = new Map<string, any>();
|
||||
|
||||
// Nested maps for root positions (positions at specific levels)
|
||||
const rootPosMap = new Map<string, any>();
|
||||
|
||||
for (const pos of rawData) {
|
||||
const orgRootId = pos.orgRootId || "NULL";
|
||||
const orgChild1Id = pos.orgChild1Id || "NULL";
|
||||
const orgChild2Id = pos.orgChild2Id || "NULL";
|
||||
const orgChild3Id = pos.orgChild3Id || "NULL";
|
||||
const orgChild4Id = pos.orgChild4Id || "NULL";
|
||||
|
||||
// Level 0 (orgRoot) counts
|
||||
if (!orgRootMap.has(orgRootId)) {
|
||||
orgRootMap.set(orgRootId, {
|
||||
totalPosition: 0,
|
||||
totalPositionCurrentUse: 0,
|
||||
totalPositionCurrentVacant: 0,
|
||||
totalPositionNextUse: 0,
|
||||
totalPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
const rootCounts = orgRootMap.get(orgRootId);
|
||||
rootCounts.totalPosition++;
|
||||
|
||||
if (!isNull(pos.current_holderId)) rootCounts.totalPositionCurrentUse++;
|
||||
else rootCounts.totalPositionCurrentVacant++;
|
||||
|
||||
if (!isNull(pos.next_holderId)) rootCounts.totalPositionNextUse++;
|
||||
else rootCounts.totalPositionNextVacant++;
|
||||
|
||||
// Level 1 (orgChild1) counts
|
||||
if (!isNull(pos.orgChild1Id)) {
|
||||
if (!orgChild1Map.has(orgChild1Id)) {
|
||||
orgChild1Map.set(orgChild1Id, {
|
||||
totalPosition: 0,
|
||||
totalPositionCurrentUse: 0,
|
||||
totalPositionCurrentVacant: 0,
|
||||
totalPositionNextUse: 0,
|
||||
totalPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
const child1Counts = orgChild1Map.get(orgChild1Id);
|
||||
child1Counts.totalPosition++;
|
||||
if (!isNull(pos.current_holderId)) child1Counts.totalPositionCurrentUse++;
|
||||
else child1Counts.totalPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child1Counts.totalPositionNextUse++;
|
||||
else child1Counts.totalPositionNextVacant++;
|
||||
}
|
||||
|
||||
// Level 2 (orgChild2) counts
|
||||
if (!isNull(pos.orgChild2Id)) {
|
||||
if (!orgChild2Map.has(orgChild2Id)) {
|
||||
orgChild2Map.set(orgChild2Id, {
|
||||
totalPosition: 0,
|
||||
totalPositionCurrentUse: 0,
|
||||
totalPositionCurrentVacant: 0,
|
||||
totalPositionNextUse: 0,
|
||||
totalPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
const child2Counts = orgChild2Map.get(orgChild2Id);
|
||||
child2Counts.totalPosition++;
|
||||
if (!isNull(pos.current_holderId)) child2Counts.totalPositionCurrentUse++;
|
||||
else child2Counts.totalPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child2Counts.totalPositionNextUse++;
|
||||
else child2Counts.totalPositionNextVacant++;
|
||||
}
|
||||
|
||||
// Level 3 (orgChild3) counts
|
||||
if (!isNull(pos.orgChild3Id)) {
|
||||
if (!orgChild3Map.has(orgChild3Id)) {
|
||||
orgChild3Map.set(orgChild3Id, {
|
||||
totalPosition: 0,
|
||||
totalPositionCurrentUse: 0,
|
||||
totalPositionCurrentVacant: 0,
|
||||
totalPositionNextUse: 0,
|
||||
totalPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
const child3Counts = orgChild3Map.get(orgChild3Id);
|
||||
child3Counts.totalPosition++;
|
||||
if (!isNull(pos.current_holderId)) child3Counts.totalPositionCurrentUse++;
|
||||
else child3Counts.totalPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child3Counts.totalPositionNextUse++;
|
||||
else child3Counts.totalPositionNextVacant++;
|
||||
}
|
||||
|
||||
// Level 4 (orgChild4) counts
|
||||
if (!isNull(pos.orgChild4Id)) {
|
||||
if (!orgChild4Map.has(orgChild4Id)) {
|
||||
orgChild4Map.set(orgChild4Id, {
|
||||
totalPosition: 0,
|
||||
totalPositionCurrentUse: 0,
|
||||
totalPositionCurrentVacant: 0,
|
||||
totalPositionNextUse: 0,
|
||||
totalPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
const child4Counts = orgChild4Map.get(orgChild4Id);
|
||||
child4Counts.totalPosition++;
|
||||
if (!isNull(pos.current_holderId)) child4Counts.totalPositionCurrentUse++;
|
||||
else child4Counts.totalPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child4Counts.totalPositionNextUse++;
|
||||
else child4Counts.totalPositionNextVacant++;
|
||||
}
|
||||
|
||||
// Root position counts (positions at specific hierarchy levels)
|
||||
// For orgRoot level (all children are null)
|
||||
const rootLevelKey = orgRootId;
|
||||
if (!rootPosMap.has(rootLevelKey)) {
|
||||
rootPosMap.set(rootLevelKey, {
|
||||
totalRootPosition: 0,
|
||||
totalRootPositionCurrentUse: 0,
|
||||
totalRootPositionCurrentVacant: 0,
|
||||
totalRootPositionNextUse: 0,
|
||||
totalRootPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
if (isNull(pos.orgChild1Id) && isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
|
||||
const rootLevelCounts = rootPosMap.get(rootLevelKey);
|
||||
rootLevelCounts.totalRootPosition++;
|
||||
if (!isNull(pos.current_holderId)) rootLevelCounts.totalRootPositionCurrentUse++;
|
||||
else rootLevelCounts.totalRootPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) rootLevelCounts.totalRootPositionNextUse++;
|
||||
else rootLevelCounts.totalRootPositionNextVacant++;
|
||||
}
|
||||
|
||||
// For orgChild1 level
|
||||
if (!isNull(pos.orgChild1Id)) {
|
||||
const child1LevelKey = `${orgRootId}-${pos.orgChild1Id}`;
|
||||
if (!rootPosMap.has(child1LevelKey)) {
|
||||
rootPosMap.set(child1LevelKey, {
|
||||
totalRootPosition: 0,
|
||||
totalRootPositionCurrentUse: 0,
|
||||
totalRootPositionCurrentVacant: 0,
|
||||
totalRootPositionNextUse: 0,
|
||||
totalRootPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
if (isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
|
||||
const child1LevelCounts = rootPosMap.get(child1LevelKey);
|
||||
child1LevelCounts.totalRootPosition++;
|
||||
if (!isNull(pos.current_holderId)) child1LevelCounts.totalRootPositionCurrentUse++;
|
||||
else child1LevelCounts.totalRootPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child1LevelCounts.totalRootPositionNextUse++;
|
||||
else child1LevelCounts.totalRootPositionNextVacant++;
|
||||
}
|
||||
}
|
||||
|
||||
// For orgChild2 level
|
||||
if (!isNull(pos.orgChild2Id)) {
|
||||
const child2LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}`;
|
||||
if (!rootPosMap.has(child2LevelKey)) {
|
||||
rootPosMap.set(child2LevelKey, {
|
||||
totalRootPosition: 0,
|
||||
totalRootPositionCurrentUse: 0,
|
||||
totalRootPositionCurrentVacant: 0,
|
||||
totalRootPositionNextUse: 0,
|
||||
totalRootPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
if (isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
|
||||
const child2LevelCounts = rootPosMap.get(child2LevelKey);
|
||||
child2LevelCounts.totalRootPosition++;
|
||||
if (!isNull(pos.current_holderId)) child2LevelCounts.totalRootPositionCurrentUse++;
|
||||
else child2LevelCounts.totalRootPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child2LevelCounts.totalRootPositionNextUse++;
|
||||
else child2LevelCounts.totalRootPositionNextVacant++;
|
||||
}
|
||||
}
|
||||
|
||||
// For orgChild3 level
|
||||
if (!isNull(pos.orgChild3Id)) {
|
||||
const child3LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}`;
|
||||
if (!rootPosMap.has(child3LevelKey)) {
|
||||
rootPosMap.set(child3LevelKey, {
|
||||
totalRootPosition: 0,
|
||||
totalRootPositionCurrentUse: 0,
|
||||
totalRootPositionCurrentVacant: 0,
|
||||
totalRootPositionNextUse: 0,
|
||||
totalRootPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
if (isNull(pos.orgChild4Id)) {
|
||||
const child3LevelCounts = rootPosMap.get(child3LevelKey);
|
||||
child3LevelCounts.totalRootPosition++;
|
||||
if (!isNull(pos.current_holderId)) child3LevelCounts.totalRootPositionCurrentUse++;
|
||||
else child3LevelCounts.totalRootPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child3LevelCounts.totalRootPositionNextUse++;
|
||||
else child3LevelCounts.totalRootPositionNextVacant++;
|
||||
}
|
||||
}
|
||||
|
||||
// For orgChild4 level
|
||||
if (!isNull(pos.orgChild4Id)) {
|
||||
const child4LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}-${pos.orgChild4Id}`;
|
||||
if (!rootPosMap.has(child4LevelKey)) {
|
||||
rootPosMap.set(child4LevelKey, {
|
||||
totalRootPosition: 0,
|
||||
totalRootPositionCurrentUse: 0,
|
||||
totalRootPositionCurrentVacant: 0,
|
||||
totalRootPositionNextUse: 0,
|
||||
totalRootPositionNextVacant: 0,
|
||||
});
|
||||
}
|
||||
const child4LevelCounts = rootPosMap.get(child4LevelKey);
|
||||
child4LevelCounts.totalRootPosition++;
|
||||
if (!isNull(pos.current_holderId)) child4LevelCounts.totalRootPositionCurrentUse++;
|
||||
else child4LevelCounts.totalRootPositionCurrentVacant++;
|
||||
if (!isNull(pos.next_holderId)) child4LevelCounts.totalRootPositionNextUse++;
|
||||
else child4LevelCounts.totalRootPositionNextVacant++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
orgRootMap,
|
||||
orgChild1Map,
|
||||
orgChild2Map,
|
||||
orgChild3Map,
|
||||
orgChild4Map,
|
||||
rootPosMap,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to get counts from maps with defaults
|
||||
export function getCounts(map: Map<string, any>, key: string) {
|
||||
return (
|
||||
map.get(key) || {
|
||||
totalPosition: 0,
|
||||
totalPositionCurrentUse: 0,
|
||||
totalPositionCurrentVacant: 0,
|
||||
totalPositionNextUse: 0,
|
||||
totalPositionNextVacant: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to get root position counts
|
||||
export function getRootCounts(map: Map<string, any>, key: string) {
|
||||
return (
|
||||
map.get(key) || {
|
||||
totalRootPosition: 0,
|
||||
totalRootPositionCurrentUse: 0,
|
||||
totalRootPositionCurrentVacant: 0,
|
||||
totalRootPositionNextUse: 0,
|
||||
totalRootPositionNextVacant: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
158
src/utils/LogMemoryStore.ts
Normal file
158
src/utils/LogMemoryStore.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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<string, Profile>; // keycloak → Profile
|
||||
rootIdCache: Map<string, string>; // profileId → rootId
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
class LogMemoryStore {
|
||||
private cache: LogCacheData = {
|
||||
currentRevision: null,
|
||||
profileCache: new Map(),
|
||||
rootIdCache: new Map(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
||||
private isRefreshing = false;
|
||||
private isInitialized = false;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor() {
|
||||
// ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready
|
||||
}
|
||||
|
||||
// เริ่มต้น cache หลังจาก TypeORM initialize เสร็จ
|
||||
initialize() {
|
||||
if (this.isInitialized) {
|
||||
console.log("[LogMemoryStore] Already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
this.refreshCache();
|
||||
this.refreshTimer = setInterval(() => {
|
||||
this.refreshCache();
|
||||
}, this.REFRESH_INTERVAL);
|
||||
|
||||
console.log(
|
||||
"[LogMemoryStore] Initialized with",
|
||||
this.REFRESH_INTERVAL / 1000,
|
||||
"second refresh interval",
|
||||
);
|
||||
}
|
||||
|
||||
private async refreshCache() {
|
||||
if (this.isRefreshing) {
|
||||
console.log("[LogMemoryStore] Already refreshing, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
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);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentRevision(): OrgRevision | null {
|
||||
return this.cache.currentRevision;
|
||||
}
|
||||
|
||||
getLastUpdateTime(): Date {
|
||||
return this.cache.updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Profile by keycloak ID with caching
|
||||
*/
|
||||
async getProfileByKeycloak(keycloak: string): Promise<Profile | null> {
|
||||
// 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<string | null> {
|
||||
if (!profileId) return null;
|
||||
|
||||
// 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() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const logMemoryStore = new LogMemoryStore();
|
||||
96
src/utils/OrgStructureCache.ts
Normal file
96
src/utils/OrgStructureCache.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
interface CacheEntry {
|
||||
data: any;
|
||||
cachedAt: Date;
|
||||
}
|
||||
|
||||
class OrgStructureCache {
|
||||
private cache: Map<string, CacheEntry> = 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<any | null> {
|
||||
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<void> {
|
||||
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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue