sdk_signer/src/database.service.ts
2025-09-03 15:08:54 +02:00

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();
}
}
}