// 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 | 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 { 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 { 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(); // 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(); 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 { const manager = StateKeyCleanupManager.getInstance(); return manager.runCleanup(); }