integratedhub/services/dbCleanup.ts
2026-01-20 09:02:26 -06:00

210 lines
6.3 KiB
TypeScript

// services/dbCleanup.ts
// Database cleanup module for state keys
import { eq } from "npm:drizzle-orm@^0.44.2";
import { handleDatabaseError, kvStore } from "../modules/KvCacheStore.ts";
import {
sqlDeleteStateKey,
sqlGetAllStateKeys,
} from "../db/sqlUserRepository.ts";
const CLEANUP_MINUTE = 10 * 1000; // 1 minute
const AGE_OUT_TIME = 60 * 60 * 1000; // 1 hour
/**
* 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 moduleCreationTime: number;
// Private constructor to prevent direct instantiation
private constructor() {
this.moduleCreationTime = Date.now();
// setInterval(async () => {
// const timeDiff = Date.now() - this.moduleCreationTime;
// const hours = Math.floor(timeDiff / (1000 * 60 * 60));
// const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
// const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000);
// const formattedTime = `${hours.toString().padStart(2, "0")}:${
// minutes.toString().padStart(2, "0")
// }:${seconds.toString().padStart(2, "0")}`;
// // Count state_keys in KV store
// let count = 0;
// for await (const _ of kvStore.list({ prefix: ["state_keys"] })) {
// count++;
// }
// console.log(`Time since module creation: ${formattedTime} ${count}`);
// }, 3000);
}
/**
* 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();
// Calculate and log time difference
// 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 sqlStateKeys = await sqlGetAllStateKeys();
// Get all state keys from KV store
const kvStateKeys: Array<{
seal_key: string;
state_id: string;
cookie_hash: string;
created_on: number;
}> = [];
for await (const entry of kvStore.list({ prefix: ["state_keys"] })) {
const value = entry.value as {
seal_key: string;
state_id: string;
cookie_hash: string;
created_on: number;
};
kvStateKeys.push(value);
}
// Merge SQL and KV state keys, keeping unique by seal_key
const stateKeyMap = new Map<string, {
seal_key: string;
state_id: string;
cookie_hash: string;
created_on: number;
}>();
// Add SQL keys first
for (const key of sqlStateKeys) {
stateKeyMap.set(key.seal_key, {
seal_key: key.seal_key,
state_id: key.state_id,
cookie_hash: key.cookie_hash,
created_on: key.created_on,
});
}
// Add KV keys (will overwrite if seal_key already exists)
for (const key of kvStateKeys) {
if (!stateKeyMap.has(key.seal_key)) {
stateKeyMap.set(key.seal_key, key);
}
}
// Convert map to array and sort by created_on (newest first)
const allStateKeys = Array.from(stateKeyMap.values());
allStateKeys.sort((a, b) => b.created_on - a.created_on);
if (allStateKeys.length === 0) {
return;
}
const uniqueStateIds = new Set<string>();
for (const entry of allStateKeys) {
const timeDiff = now - entry.created_on;
const hours = Math.floor(timeDiff / (1000 * 60 * 60));
const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000);
const formattedTime = `${hours.toString().padStart(2, "0")}:${
minutes.toString().padStart(2, "0")
}:${seconds.toString().padStart(2, "0")}`;
const stateId = entry.state_id;
const sealKey = entry.seal_key;
const age = now - entry.created_on;
if (age > AGE_OUT_TIME) {
try {
// Delete from SQL database
sqlDeleteStateKey(sealKey);
} catch (error) {
console.error("Error deleting state key:", error);
}
// Delete from KV store
await kvStore.delete(["state_keys", sealKey]);
continue;
}
if (!uniqueStateIds.has(stateId)) {
uniqueStateIds.add(stateId);
continue;
}
// Delete expired keys (older than CLEANUP_MINUTE)
if (age > CLEANUP_MINUTE) {
try {
// Delete from SQL database
sqlDeleteStateKey(sealKey);
} catch (error) {
console.error("Error deleting state key:", error);
}
// Delete from KV store
await kvStore.delete(["state_keys", sealKey]);
}
}
} catch (error) {
console.error("Error during state key cleanup:", error);
// Reset the flag on error so cleanup can be retried
this.lastCleanupTimestamp = 0;
console.error("KV/SQL get failed:", error);
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 async function runStateKeyCleanup(): Promise<void> {
const manager = StateKeyCleanupManager.getInstance();
return manager.runCleanup();
}