// LevelDB-based persistent key-value database service // High performance alternative to JSON files import { Level } from 'level'; import * as path from 'path'; interface DatabaseObject { storeName: string; object: any; key: string | null; } interface BatchWriteOperation { storeName: string; objects: Array<{ key: string; object: any }>; } export default class Database { private static instance: Database; private db!: Level; private dataDir: string; private initialized: boolean = false; private constructor() { this.dataDir = path.join(process.cwd(), 'data'); console.log('🔧 Database service initialized (LevelDB)'); } public static async getInstance(): Promise { if (!Database.instance) { Database.instance = new Database(); await Database.instance.init(); } return Database.instance; } private async init(): Promise { if (this.initialized) return; try { // Initialize LevelDB with performance optimizations this.db = new Level(this.dataDir, { valueEncoding: 'json', maxFileSize: 2 * 1024 * 1024, // 2MB blockSize: 4096, cacheSize: 8 * 1024 * 1024 // 8MB cache }); this.initialized = true; console.log('✅ LevelDB database initialized with persistent storage'); } catch (error) { console.error('❌ Failed to initialize LevelDB database:', error); throw error; } } // Key encoding: "storeName:key" private getKey(storeName: string, key: string): string { return `${storeName}:${key}`; } private parseKey(fullKey: string): { storeName: string; key: string } | null { const colonIndex = fullKey.indexOf(':'); if (colonIndex === -1) return null; const storeName = fullKey.substring(0, colonIndex); const key = fullKey.substring(colonIndex + 1); return { storeName, key }; } /** * Get a single object from a store * O(log n) operation - only reads specific key */ public async getObject(storeName: string, key: string, isBuffer: boolean = false): Promise { try { const fullKey = this.getKey(storeName, key); if (isBuffer) { return await this.db.get(fullKey, { valueEncoding: 'buffer' }); } else { return await this.db.get(fullKey); } } catch (error) { if ((error as any).code === 'LEVEL_NOT_FOUND') { return null; } throw error; } } /** * Add or update an object in a store * O(log n) operation - only writes specific key-value pair */ public async addObject(operation: DatabaseObject, isBuffer: boolean = false): Promise { const { storeName, object, key } = operation; if (key) { const fullKey = this.getKey(storeName, key); if (isBuffer) { await this.db.put(fullKey, object, { valueEncoding: 'buffer' }); } else { await this.db.put(fullKey, object); } } else { // Auto-generate key if none provided const autoKey = Date.now().toString() + Math.random().toString(36).substr(2, 9); const fullKey = this.getKey(storeName, autoKey); await this.db.put(fullKey, object); } } /** * Delete an object from a store * O(log n) operation - only deletes specific key */ public async deleteObject(storeName: string, key: string): Promise { try { const fullKey = this.getKey(storeName, key); await this.db.del(fullKey); } catch (error) { if ((error as any).code === 'LEVEL_NOT_FOUND') { // Key doesn't exist, that's fine return; } throw error; } } /** * Get all objects from a store as a record * Efficient range query - only reads keys in the store */ public async dumpStore(storeName: string): Promise> { const result: Record = {}; const prefix = `${storeName}:`; try { // Use LevelDB's range queries for efficient store dumping for await (const [key, value] of this.db.iterator({ gte: prefix, lt: prefix + '\xff' // '\xff' is higher than any valid character })) { const parsed = this.parseKey(key); if (parsed && parsed.storeName === storeName) { result[parsed.key] = value; } } } catch (error) { console.error(`Failed to dump store ${storeName}:`, error); } return result; } /** * Clear all objects from a store * Efficient store clearing using range deletes */ public async clearStore(storeName: string): Promise { const prefix = `${storeName}:`; const batch = this.db.batch(); // Collect all keys in the store for await (const [key] of this.db.iterator({ gte: prefix, lt: prefix + '\xff' })) { batch.del(key); } await batch.write(); } /** * Batch write operations * Atomic batch operations */ public async batchWriting(operation: BatchWriteOperation): Promise { const { storeName, objects } = operation; // Use LevelDB's batch operations for atomic writes const batch = this.db.batch(); for (const { key, object } of objects) { const fullKey = this.getKey(storeName, key); batch.put(fullKey, object); } await batch.write(); } /** * Get all store names (for debugging) */ public async getStoreNames(): Promise { const storeNames = new Set(); for await (const [key] of this.db.iterator()) { const parsed = this.parseKey(key); if (parsed) { storeNames.add(parsed.storeName); } } return Array.from(storeNames); } /** * Get store size (for debugging) */ public async getStoreSize(storeName: string): Promise { let count = 0; const prefix = `${storeName}:`; for await (const [key] of this.db.iterator({ gte: prefix, lt: prefix + '\xff' })) { count++; } return count; } /** * Clear all data (for testing/reset) */ public async clearAll(): Promise { // Close and reopen database to clear all data await this.db.close(); // Remove the data directory const fs = require('fs'); if (fs.existsSync(this.dataDir)) { fs.rmSync(this.dataDir, { recursive: true, force: true }); } // Reinitialize this.initialized = false; await this.init(); } /** * Get data directory path (for debugging) */ public getDataDirectory(): string { return this.dataDir; } /** * Get database statistics (LevelDB specific) */ public async getStats(): Promise { let totalKeys = 0; let totalStores = 0; const storeSizes: Record = {}; for await (const [key] of this.db.iterator()) { totalKeys++; const parsed = this.parseKey(key); if (parsed) { if (!storeSizes[parsed.storeName]) { storeSizes[parsed.storeName] = 0; totalStores++; } storeSizes[parsed.storeName]++; } } return { totalKeys, totalStores, storeSizes, dataDirectory: this.dataDir }; } /** * Close database connection (should be called on app shutdown) */ public async close(): Promise { if (this.db) { await this.db.close(); } } }