UNPKG

tero

Version:

tero is a JSON document Manager, with ACID complaint and backup recovery mechanism.

451 lines (450 loc) 16.6 kB
import { existsSync, mkdirSync } from "fs"; import { ACIDStorageEngine } from "./acid-engine.js"; import { SchemaValidator } from "./schema.js"; import { BackupManager } from "./backup.js"; import { DataRecovery } from "./recovery.js"; export class Tero { teroDirectory = "TeroDB"; cacheSize = 100; cache = new Map(); cacheHits = 0; cacheRequests = 0; acidEngine; schemaValidator; backupManager; dataRecovery; constructor(config) { try { const { directory, cacheSize } = config || {}; if (typeof directory === "string" && directory.trim()) { // Sanitize directory path to prevent directory traversal this.teroDirectory = directory.replace(/[^a-zA-Z0-9_\-\/]/g, ''); } if (typeof cacheSize === "number" && cacheSize > 0) { this.cacheSize = Math.min(cacheSize, 10000); // Cap at 10k entries } // Create directories with proper error handling this.initializeDirectories(); // Initialize ACID storage engine (primary system) this.acidEngine = new ACIDStorageEngine(this.teroDirectory); // Initialize schema validator this.schemaValidator = new SchemaValidator(); } catch (error) { throw new Error(`Failed to initialize Tero: ${error instanceof Error ? error.message : 'Unknown error'}`); } } initializeDirectories() { try { if (!existsSync(this.teroDirectory)) { mkdirSync(this.teroDirectory, { recursive: true }); } const backupDir = `${this.teroDirectory}/.backup`; if (!existsSync(backupDir)) { mkdirSync(backupDir, { recursive: true }); } } catch (error) { throw new Error(`Failed to create directories: ${error instanceof Error ? error.message : 'Unknown error'}`); } } validateKey(key) { if (!key || typeof key !== 'string') { throw new Error('Key must be a non-empty string'); } // Sanitize key to prevent path traversal if (key.includes('..') || key.includes('/') || key.includes('\\')) { throw new Error('Key contains invalid characters'); } } evictOldestCacheEntry() { if (this.cache.size === 0) return; let oldestKey = ''; let oldestTime = Date.now(); for (const [key, entry] of this.cache.entries()) { if (entry.lastAccessed < oldestTime) { oldestTime = entry.lastAccessed; oldestKey = key; } } if (oldestKey) { this.cache.delete(oldestKey); } } invalidateCacheKeys(keys) { for (const key of keys) { this.cache.delete(key); } } updateCache(key, data, transactionId) { if (this.cache.size >= this.cacheSize) { this.evictOldestCacheEntry(); } this.cache.set(key, { data: { ...data }, lastAccessed: Date.now(), transactionId }); } // Core ACID Operations beginTransaction() { try { return this.acidEngine.beginTransaction(); } catch (error) { throw new Error(`Failed to begin transaction: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async write(transactionId, key, data, options) { try { this.validateKey(key); if (data === undefined || data === null) { throw new Error('Data cannot be null or undefined'); } // Perform schema validation if requested if (options?.validate || options?.schemaName) { const schemaName = options.schemaName || key; const validationResult = this.schemaValidator.validate(schemaName, data); if (!validationResult.valid) { if (options.strict) { const errorMessages = validationResult.errors.map(e => `${e.field}: ${e.message}`).join(', '); throw new Error(`Schema validation failed: ${errorMessages}`); } else { return validationResult; } } // Use sanitized data from validation data = validationResult.data || data; } await this.acidEngine.write(transactionId, key, data); // Update cache with transaction context this.updateCache(key, data, transactionId); if (options?.validate || options?.schemaName) { return { valid: true, errors: [], data }; } } catch (error) { throw new Error(`Write failed for key '${key}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } async read(transactionId, key) { try { this.validateKey(key); this.cacheRequests++; // Check cache first, but only if it's from the same transaction or committed const cachedEntry = this.cache.get(key); if (cachedEntry && (!cachedEntry.transactionId || cachedEntry.transactionId === transactionId)) { this.cacheHits++; cachedEntry.lastAccessed = Date.now(); return cachedEntry.data; } // Read from ACID engine const data = await this.acidEngine.read(transactionId, key); if (data !== null) { this.updateCache(key, data, transactionId); } return data; } catch (error) { throw new Error(`Read failed for key '${key}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } async delete(transactionId, key) { try { this.validateKey(key); await this.acidEngine.delete(transactionId, key); // Remove from cache this.cache.delete(key); } catch (error) { throw new Error(`Delete failed for key '${key}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } async commit(transactionId) { try { // Get affected keys before commit const transaction = this.acidEngine.getActiveTransactions().includes(transactionId); if (!transaction) { throw new Error(`Transaction ${transactionId} not found or not active`); } await this.acidEngine.commitTransaction(transactionId); // Clean up cache entries for this transaction for (const [key, entry] of this.cache.entries()) { if (entry.transactionId === transactionId) { // Remove transaction ID to mark as committed entry.transactionId = undefined; } } } catch (error) { throw new Error(`Failed to commit transaction: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async rollback(transactionId) { try { await this.acidEngine.rollbackTransaction(transactionId); // Remove cache entries for this transaction const keysToRemove = []; for (const [key, entry] of this.cache.entries()) { if (entry.transactionId === transactionId) { keysToRemove.push(key); } } this.invalidateCacheKeys(keysToRemove); } catch (error) { throw new Error(`Failed to rollback transaction: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Convenience Methods (Auto-transaction) async create(key, initialData, options) { const transactionId = this.beginTransaction(); try { // Check if file already exists const existing = await this.read(transactionId, key); if (existing !== null) { await this.rollback(transactionId); return false; // File already exists } // Create with initial data or empty object const result = await this.write(transactionId, key, initialData || {}, options); await this.commit(transactionId); return result || true; } catch (error) { await this.rollback(transactionId); throw new Error(`Create failed for key '${key}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } async update(key, data, options) { const transactionId = this.beginTransaction(); try { const result = await this.write(transactionId, key, data, options); await this.commit(transactionId); return result; } catch (error) { await this.rollback(transactionId); throw error; } } async get(key) { const transactionId = this.beginTransaction(); try { const data = await this.read(transactionId, key); await this.commit(transactionId); return data; } catch (error) { await this.rollback(transactionId); throw error; } } async remove(key) { const transactionId = this.beginTransaction(); try { await this.delete(transactionId, key); await this.commit(transactionId); } catch (error) { await this.rollback(transactionId); throw error; } } exists(key) { try { this.validateKey(key); return existsSync(`${this.teroDirectory}/${key}.json`); } catch (error) { return false; } } // Batch Operations async batchWrite(operations, options) { const transactionId = this.beginTransaction(); try { for (const op of operations) { await this.write(transactionId, op.key, op.data, options); } await this.commit(transactionId); } catch (error) { await this.rollback(transactionId); throw new Error(`Batch write failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async batchRead(keys) { const transactionId = this.beginTransaction(); const results = {}; try { for (const key of keys) { results[key] = await this.read(transactionId, key); } await this.commit(transactionId); return results; } catch (error) { await this.rollback(transactionId); throw new Error(`Batch read failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Money transfer example demonstrating ACID properties async transferMoney(fromKey, toKey, amount) { if (amount <= 0) { throw new Error('Transfer amount must be positive'); } const transactionId = this.beginTransaction(); try { // Read current balances const fromAccount = await this.read(transactionId, fromKey); const toAccount = await this.read(transactionId, toKey); if (!fromAccount || !toAccount) { throw new Error('One or both accounts do not exist'); } if (fromAccount.balance < amount) { throw new Error('Insufficient funds'); } // Update balances await this.write(transactionId, fromKey, { ...fromAccount, balance: fromAccount.balance - amount }); await this.write(transactionId, toKey, { ...toAccount, balance: toAccount.balance + amount }); // Commit the transaction await this.commit(transactionId); } catch (error) { await this.rollback(transactionId); throw new Error(`Money transfer failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Schema Management setSchema(collectionName, schema) { try { this.schemaValidator.setSchema(collectionName, schema); } catch (error) { throw new Error(`Failed to set schema: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getSchema(collectionName) { return this.schemaValidator.getSchema(collectionName); } removeSchema(collectionName) { return this.schemaValidator.removeSchema(collectionName); } validateData(collectionName, data) { return this.schemaValidator.validate(collectionName, data); } // Backup Management configureBackup(config) { try { this.backupManager = new BackupManager(this.teroDirectory, config); } catch (error) { throw new Error(`Failed to configure backup: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async performBackup() { if (!this.backupManager) { throw new Error('Backup not configured. Call configureBackup() first.'); } return await this.backupManager.performBackup(); } // Data Recovery configureDataRecovery(config) { try { this.dataRecovery = new DataRecovery(config); } catch (error) { throw new Error(`Failed to configure data recovery: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async recoverFromCloud(key) { if (!this.dataRecovery) { throw new Error('Data recovery not configured. Call configureDataRecovery() first.'); } const recovered = await this.dataRecovery.recoverSingleFile(key); if (recovered) { this.cache.delete(key); // Invalidate cache } return recovered; } async recoverAllFromCloud() { if (!this.dataRecovery) { throw new Error('Data recovery not configured. Call configureDataRecovery() first.'); } const result = await this.dataRecovery.recoverIndividualFiles(); // Clear cache for recovered files if (result.recovered.length > 0) { this.invalidateCacheKeys(result.recovered); } return result; } // Utility Methods getCacheStats() { const hitRate = this.cacheRequests > 0 ? (this.cacheHits / this.cacheRequests) * 100 : 0; return { size: this.cache.size, maxSize: this.cacheSize, hitRate: Math.round(hitRate * 100) / 100 }; } getActiveTransactions() { return this.acidEngine.getActiveTransactions(); } forceCheckpoint() { this.acidEngine.forceCheckpoint(); } async verifyDataIntegrity() { const result = { totalFiles: 0, corruptedFiles: [], missingFiles: [], healthy: true }; try { const { readdirSync } = await import('fs'); const files = readdirSync(this.teroDirectory) .filter((file) => file.endsWith('.json')); result.totalFiles = files.length; for (const file of files) { const key = file.replace('.json', ''); try { const data = await this.get(key); if (data === null) { result.missingFiles.push(key); result.healthy = false; } } catch (error) { result.corruptedFiles.push(key); result.healthy = false; } } return result; } catch (error) { throw new Error(`Data integrity verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } clearCache() { this.cache.clear(); } // Cleanup method destroy() { if (this.acidEngine) { this.acidEngine.destroy(); } if (this.backupManager) { this.backupManager.destroy(); } this.clearCache(); } }