UNPKG

axiodb

Version:

The Pure JavaScript Alternative to SQLite. Embedded NoSQL database for Node.js with MongoDB-style queries, zero native dependencies, built-in InMemoryCache, and web GUI. Perfect for desktop apps, CLI tools, and embedded systems. No compilation, no platfor

300 lines 13.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable @typescript-eslint/no-explicit-any */ const crypto_1 = require("crypto"); const promises_1 = require("fs/promises"); const FileManager_1 = __importDefault(require("../../engine/Filesystem/FileManager")); const FolderManager_1 = __importDefault(require("../../engine/Filesystem/FolderManager")); const Converter_helper_1 = __importDefault(require("../../Helper/Converter.helper")); const response_helper_1 = __importDefault(require("../../Helper/response.helper")); const Crypto_helper_1 = require("../../Helper/Crypto.helper"); class WriteAheadLog { constructor(collectionPath, transactionId, isEncrypted = false, encryptionKey) { // Batch write buffer for optimized I/O this.pendingEntries = []; this.batchSize = 10; this.flushTimeout = null; this.flushDelayMs = 50; this.collectionPath = collectionPath; this.transactionDir = `${collectionPath}/.transactions`; this.walPath = `${this.transactionDir}/${transactionId}.wal`; this.FileManager = new FileManager_1.default(); this.FolderManager = new FolderManager_1.default(); this.Converter = new Converter_helper_1.default(); this.ResponseHelper = new response_helper_1.default(); this.isEncrypted = isEncrypted; if (this.isEncrypted && encryptionKey) { this.cryptoInstance = new Crypto_helper_1.CryptoHelper(encryptionKey); } } createWAL() { return __awaiter(this, void 0, void 0, function* () { try { const dirExists = yield this.FolderManager.DirectoryExists(this.transactionDir); if (!dirExists.status) { yield this.FolderManager.CreateDirectory(this.transactionDir); } const fileExists = yield this.FileManager.FileExists(this.walPath); if (!fileExists.status) { yield this.FileManager.WriteFile(this.walPath, ''); } return this.ResponseHelper.Success({ message: "WAL created successfully" }); } catch (error) { return this.ResponseHelper.Error(error); } }); } /** * Appends a single log entry. For better performance, use appendLogBatch. */ appendLog(entry) { return __awaiter(this, void 0, void 0, function* () { try { const checksum = this.calculateChecksum(entry); const entryWithChecksum = Object.assign(Object.assign({}, entry), { checksum }); let logLine = this.Converter.ToString(entryWithChecksum) + '\n'; if (this.isEncrypted && this.cryptoInstance) { logLine = yield this.cryptoInstance.encrypt(logLine); logLine += '\n'; } const fileHandle = yield (0, promises_1.open)(this.walPath, 'a'); try { yield fileHandle.write(logLine); yield fileHandle.sync(); } finally { yield fileHandle.close(); } return this.ResponseHelper.Success({ message: "Log entry appended" }); } catch (error) { return this.ResponseHelper.Error(error); } }); } /** * Appends multiple log entries in a single disk write operation. * Much more efficient than calling appendLog multiple times. * * @param entries - Array of WAL entries to write * @returns Success/Error result */ appendLogBatch(entries) { return __awaiter(this, void 0, void 0, function* () { if (entries.length === 0) { return this.ResponseHelper.Success({ message: "No entries to append" }); } try { const lines = []; for (const entry of entries) { const checksum = this.calculateChecksum(entry); const entryWithChecksum = Object.assign(Object.assign({}, entry), { checksum }); let logLine = this.Converter.ToString(entryWithChecksum) + '\n'; if (this.isEncrypted && this.cryptoInstance) { logLine = yield this.cryptoInstance.encrypt(logLine); logLine += '\n'; } lines.push(logLine); } // Single write operation for all entries const batchContent = lines.join(''); const fileHandle = yield (0, promises_1.open)(this.walPath, 'a'); try { yield fileHandle.write(batchContent); yield fileHandle.sync(); // Single sync for entire batch } finally { yield fileHandle.close(); } return this.ResponseHelper.Success({ message: "Batch log entries appended", count: entries.length }); } catch (error) { return this.ResponseHelper.Error(error); } }); } /** * Queues an entry for batched write. Flushes automatically when batch is full * or after a short delay. * * @param entry - WAL entry to queue */ queueEntry(entry) { const checksum = this.calculateChecksum(entry); this.pendingEntries.push(Object.assign(Object.assign({}, entry), { checksum })); // Flush if batch is full if (this.pendingEntries.length >= this.batchSize) { this.flushPendingEntries(); } else { // Schedule flush after delay if not already scheduled if (!this.flushTimeout) { this.flushTimeout = setTimeout(() => { this.flushPendingEntries(); }, this.flushDelayMs); } } } /** * Flushes all pending entries to disk immediately. */ flushPendingEntries() { return __awaiter(this, void 0, void 0, function* () { if (this.flushTimeout) { clearTimeout(this.flushTimeout); this.flushTimeout = null; } if (this.pendingEntries.length === 0) { return this.ResponseHelper.Success({ message: "No pending entries" }); } const entriesToFlush = [...this.pendingEntries]; this.pendingEntries = []; return this.appendLogBatch(entriesToFlush); }); } getLogEntries() { return __awaiter(this, void 0, void 0, function* () { try { const fileExists = yield this.FileManager.FileExists(this.walPath); if (!fileExists.status) { return []; } const readResult = yield this.FileManager.ReadFile(this.walPath); if (!readResult.status) { return []; } const content = readResult.data; if (!content || content.trim().length === 0) { return []; } const lines = content.split('\n').filter((line) => line.trim().length > 0); const entries = []; for (const line of lines) { try { let decryptedLine = line; if (this.isEncrypted && this.cryptoInstance) { decryptedLine = yield this.cryptoInstance.decrypt(line); } const entry = this.Converter.ToObject(decryptedLine); const calculatedChecksum = this.calculateChecksum(entry); if (calculatedChecksum === entry.checksum) { entries.push(entry); } } catch (_a) { continue; } } return entries; } catch (_b) { return []; } }); } deleteWAL() { return __awaiter(this, void 0, void 0, function* () { try { // Clear any pending entries this.pendingEntries = []; if (this.flushTimeout) { clearTimeout(this.flushTimeout); this.flushTimeout = null; } const fileExists = yield this.FileManager.FileExists(this.walPath); if (fileExists.status) { yield this.FileManager.DeleteFile(this.walPath); } return this.ResponseHelper.Success({ message: "WAL deleted successfully" }); } catch (error) { return this.ResponseHelper.Error(error); } }); } redo() { return __awaiter(this, void 0, void 0, function* () { try { const entries = yield this.getLogEntries(); for (const entry of entries) { if (entry.operationType === 'INSERT' || entry.operationType === 'UPDATE') { if (entry.afterData) { const filePath = `${this.collectionPath}/${entry.fileName}`; yield this.FileManager.WriteFile(filePath, entry.afterData); } } else if (entry.operationType === 'DELETE') { const filePath = `${this.collectionPath}/${entry.fileName}`; const fileExists = yield this.FileManager.FileExists(filePath); if (fileExists.status) { yield this.FileManager.DeleteFile(filePath); } } } return this.ResponseHelper.Success({ message: "REDO completed", entriesProcessed: entries.length }); } catch (error) { return this.ResponseHelper.Error(error); } }); } undo() { return __awaiter(this, void 0, void 0, function* () { try { const entries = yield this.getLogEntries(); for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; if (entry.operationType === 'INSERT') { const filePath = `${this.collectionPath}/${entry.fileName}`; const fileExists = yield this.FileManager.FileExists(filePath); if (fileExists.status) { yield this.FileManager.DeleteFile(filePath); } } else if (entry.operationType === 'UPDATE' || entry.operationType === 'DELETE') { if (entry.beforeData) { const filePath = `${this.collectionPath}/${entry.fileName}`; yield this.FileManager.WriteFile(filePath, entry.beforeData); } } } return this.ResponseHelper.Success({ message: "UNDO completed", entriesProcessed: entries.length }); } catch (error) { return this.ResponseHelper.Error(error); } }); } calculateChecksum(entry) { const data = { transactionId: entry.transactionId, timestamp: entry.timestamp, operationType: entry.operationType, documentId: entry.documentId, fileName: entry.fileName, beforeData: entry.beforeData, afterData: entry.afterData, savepointName: entry.savepointName, }; const dataString = this.Converter.ToString(data); return (0, crypto_1.createHash)('sha256').update(dataString).digest('hex'); } } exports.default = WriteAheadLog; //# sourceMappingURL=WriteAheadLog.service.js.map