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
JavaScript
"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