289 lines
7.3 KiB
TypeScript
289 lines
7.3 KiB
TypeScript
// 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<Database> {
|
|
if (!Database.instance) {
|
|
Database.instance = new Database();
|
|
await Database.instance.init();
|
|
}
|
|
return Database.instance;
|
|
}
|
|
|
|
private async init(): Promise<void> {
|
|
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<any | null> {
|
|
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<void> {
|
|
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<void> {
|
|
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<Record<string, any>> {
|
|
const result: Record<string, any> = {};
|
|
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<void> {
|
|
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<void> {
|
|
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<string[]> {
|
|
const storeNames = new Set<string>();
|
|
|
|
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<number> {
|
|
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<void> {
|
|
// 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<any> {
|
|
let totalKeys = 0;
|
|
let totalStores = 0;
|
|
const storeSizes: Record<string, number> = {};
|
|
|
|
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<void> {
|
|
if (this.db) {
|
|
await this.db.close();
|
|
}
|
|
}
|
|
}
|