integratedhub/services/dbCleanup.ts
2026-02-14 02:20:19 +00:00

161 lines
4.8 KiB
TypeScript

// services/dbCleanup.ts
// Database cleanup module for state keys - SQL-only version
import { handleDatabaseError } from "../modules/KvCacheStore.ts";
import {
sqlDeleteStateKey,
sqlGetAllStateKeys,
} from "../db/sqlUserRepository.ts";
const CLEANUP_MINUTE = 10 * 1000; // 10 seconds between cleanup runs
const AGE_OUT_TIME = 60 * 60 * 1000; // 1 hour - delete keys older than this
const DUPLICATE_KEY_AGE = 5 * 60 * 1000; // 5 minutes - keep only newest key per state_id within this window
/**
* Singleton class to ensure only one cleanup instance runs at a time
* across all user requests on the server.
*/
class StateKeyCleanupManager {
private static instance: StateKeyCleanupManager | null = null;
private lastCleanupTimestamp = 0;
private isRunning = false;
private cleanupPromise: Promise<void> | null = null;
// Private constructor to prevent direct instantiation
private constructor() {
}
/**
* Get the singleton instance of the cleanup manager
*/
public static getInstance(): StateKeyCleanupManager {
if (!StateKeyCleanupManager.instance) {
StateKeyCleanupManager.instance = new StateKeyCleanupManager();
}
return StateKeyCleanupManager.instance;
}
public async runCleanup(): Promise<void> {
const now = Date.now();
// Check if cleanup was run recently
if (
this.lastCleanupTimestamp &&
now - this.lastCleanupTimestamp < CLEANUP_MINUTE
) {
return;
}
// Mutex lock: If cleanup is already running, return the existing promise
if (this.isRunning && this.cleanupPromise) {
return this.cleanupPromise;
}
// Set mutex lock
this.isRunning = true;
try {
// Create cleanup promise
this.cleanupPromise = this.performCleanup(now);
await this.cleanupPromise;
} finally {
// Release mutex lock
this.isRunning = false;
this.cleanupPromise = null;
}
}
private async performCleanup(now: number): Promise<void> {
this.lastCleanupTimestamp = now;
try {
// Get all state keys from SQL database
const allStateKeys = await sqlGetAllStateKeys();
if (allStateKeys.length === 0) {
return;
}
// Group by state_id to identify multiple keys per session
const stateIdMap = new Map<string, typeof allStateKeys>();
for (const key of allStateKeys) {
const stateId = key.state_id;
if (!stateIdMap.has(stateId)) {
stateIdMap.set(stateId, []);
}
stateIdMap.get(stateId)!.push(key);
}
let deletedCount = 0;
// Process each state_id group
for (const [stateId, keys] of stateIdMap.entries()) {
// Sort keys by created_on (newest first)
keys.sort((a, b) => b.created_on - a.created_on);
// Keep the newest key, delete older ones for this state_id
// This handles state key rotation - old rotated keys are cleaned up
for (let i = 1; i < keys.length; i++) {
const oldKey = keys[i];
const age = now - oldKey.created_on;
// Delete old rotated keys (older than 5 minutes)
if (age > DUPLICATE_KEY_AGE) {
try {
await sqlDeleteStateKey(oldKey.seal_key);
deletedCount++;
console.log(
`Cleaned up old state key for ${stateId}, age: ${
Math.floor(age / 1000)
}s`,
);
} catch (error) {
console.error("Error deleting old state key:", error);
}
}
}
// Check the newest (current) key
const currentKey = keys[0];
const currentAge = now - currentKey.created_on;
// Delete if older than 1 hour (expired session)
if (currentAge > AGE_OUT_TIME) {
try {
await sqlDeleteStateKey(currentKey.seal_key);
deletedCount++;
console.log(
`Cleaned up expired state key for ${stateId}, age: ${
Math.floor(currentAge / 1000)
}s`,
);
} catch (error) {
console.error("Error deleting expired state key:", error);
}
}
}
if (deletedCount > 0) {
console.log(
`State key cleanup completed: ${deletedCount} keys deleted`,
);
}
} catch (error) {
console.error("Error during state key cleanup:", error);
// Reset the flag on error so cleanup can be retried
this.lastCleanupTimestamp = 0;
handleDatabaseError(error);
return;
}
}
}
/**
* Exported function that uses the singleton instance to run cleanup.
* This ensures only one cleanup runs at a time across all user requests.
*/
export function runStateKeyCleanup(): Promise<void> {
const manager = StateKeyCleanupManager.getInstance();
return manager.runCleanup();
}