210 lines
6.3 KiB
TypeScript
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();
|
|
}
|