UNPKG

velocedb

Version:

High-performance, secure, and robust local database

390 lines 15.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Veloce = void 0; const node_path_1 = __importDefault(require("node:path")); const node_fs_1 = __importDefault(require("node:fs")); const secure_json_parse_1 = __importDefault(require("secure-json-parse")); const json_stringify_safe_1 = __importDefault(require("json-stringify-safe")); /** * Veloce is a lightweight JSON database that uses proxies to simplify data manipulation. * It provides automatic saving (both synchronous and asynchronous), custom configurations, * and flexible data handling. * * @template TData The type of data stored in the database */ class Veloce { /** * Creates nested proxies for objects within the database structure * to track changes at all levels of the object hierarchy. * * @private * @template T The type of the target object * @param target - The target object to proxy * @param handler - The proxy handler containing trap methods * @param proxyCache - The cache for storing proxied objects * @returns A proxied version of the target object */ static _createNestedProxies(target, handler, proxyCache) { if (proxyCache.has(target)) { return proxyCache.get(target); } const proxy = new Proxy(target, handler); proxyCache.set(target, proxy); return proxy; } /** * Triggers an automatic save operation based on configuration settings. * If auto-save is enabled, it will use either synchronous or asynchronous * save methods depending on the configuration. * * @private */ _triggerAutoSave() { if (this.configuration.autoSave) { if (this.configuration.useSync) { this.save(); } else { void this.saveAsync(); } } } /** * Creates a new Veloce database instance. * * @public * @param filePath - The path to the database file * @param baseData - The base data to use as fallback when no data exists * @param configuration - Configuration options for the database */ constructor(filePath, baseData, configuration = {}) { /** Cache for storing proxied objects * * @private */ this._proxyCache = new WeakMap(); /** Flag to track if the initial directory check has been performed * * @private */ this._isInitialCheckComplete = false; /** Flag to indicate if a save operation is in progress * * @private */ this._isSaving = false; /** Counter for tracking consecutive auto-save timeout operations * * @private */ this._saveTimeoutCount = 0; /** Queue for handling save operations * * @private */ this._saveQueue = Promise.resolve(); /** Flag to indicate if the database is closed * * @private */ this._isClosed = false; this._filePath = filePath; this.configuration = { indentation: 2, autoSave: true, noProxy: false, autoSaveDelayMs: 750, saveRetryTimeoutMs: 100, onUpdate: undefined, maxAutoSaveTimeouts: 10, fileOptions: { encoding: "utf-8" }, useSync: false, ...configuration, }; this._proxyHandler = this._createProxyHandler(); this.data = this._initializeData(baseData); } /** * Initializes the database data, either from an existing file or with the provided base data. * * @private * @param baseData - The base data to use as fallback when no data exists * @returns The initialized data */ _initializeData(baseData) { const fileExists = node_fs_1.default.existsSync(this._filePath); const initialData = fileExists ? secure_json_parse_1.default.parse(node_fs_1.default.readFileSync(this._filePath, this.configuration.fileOptions)) : baseData; return this.configuration.noProxy ? initialData : Veloce._createNestedProxies(initialData, this._proxyHandler, this._proxyCache); } /** * Creates the proxy handler for reactive data operations. * * @private * @returns A proxy handler object with trap methods */ _createProxyHandler() { return { get: (target, property, receiver) => { var _a, _b; const result = Reflect.get(target, property, receiver); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "get", result); return result instanceof Object ? Veloce._createNestedProxies(result, this._proxyHandler, this._proxyCache) : result; }, set: (target, property, value, receiver) => { var _a, _b; const result = Reflect.set(target, property, value, receiver); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "set", result); this._triggerAutoSave(); return result; }, deleteProperty: (target, property) => { var _a, _b; const result = Reflect.deleteProperty(target, property); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "deleteProperty", result); this._triggerAutoSave(); return result; }, defineProperty: (target, property, descriptor) => { var _a, _b; const result = Reflect.defineProperty(target, property, descriptor); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "defineProperty", result); this._triggerAutoSave(); return result; }, setPrototypeOf: (target, prototype) => { var _a, _b; const result = Reflect.setPrototypeOf(target, prototype); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "setPrototypeOf", result); this._triggerAutoSave(); return result; }, apply: (target, thisArg, argumentsList) => { var _a, _b; const result = Reflect.apply(target, thisArg, argumentsList); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "apply", result); this._triggerAutoSave(); return result; }, construct: (target, argumentsList, newTarget) => { var _a, _b; const result = Reflect.construct(target, argumentsList, newTarget); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "construct", result); this._triggerAutoSave(); return result instanceof Object ? Veloce._createNestedProxies(result, this._proxyHandler, this._proxyCache) : result; }, has: (obj, prop) => { var _a, _b; const result = Reflect.has(obj, prop); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "has", result); return result; }, ownKeys: (obj) => { var _a, _b; const result = Reflect.ownKeys(obj); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "ownKeys", result); return result; }, getOwnPropertyDescriptor: (obj, prop) => { var _a, _b; const result = Reflect.getOwnPropertyDescriptor(obj, prop); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "getOwnPropertyDescriptor", result); return result; }, preventExtensions: (obj) => { var _a, _b; const result = Reflect.preventExtensions(obj); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "preventExtensions", result); return result; }, isExtensible: (obj) => { var _a, _b; const result = Reflect.isExtensible(obj); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "isExtensible", result); return result; }, getPrototypeOf: (obj) => { var _a, _b; const result = Reflect.getPrototypeOf(obj); (_b = (_a = this.configuration).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, "getPrototypeOf", result); return result; }, }; } /** * Saves the current state of the database to the file synchronously. * @param force - If true, bypasses all checks and immediately saves the data * * @public */ save(force = false) { const performSave = () => { const dir = node_path_1.default.dirname(this._filePath); if (!this._isInitialCheckComplete) { if (!node_fs_1.default.existsSync(dir)) { node_fs_1.default.mkdirSync(dir, { recursive: true }); } this._isInitialCheckComplete = true; } this._isSaving = true; node_fs_1.default.writeFileSync(this._filePath, (0, json_stringify_safe_1.default)(this.data, null, this.configuration.indentation), this.configuration.fileOptions); this._cleanupSaveState(); }; this._handleSaveOperation(performSave, force); } /** * Saves the current state of the database to the file asynchronously. * @param force - If true, bypasses all checks and immediately saves the data * * @public */ async saveAsync(force = false) { const performSave = async () => { const dir = node_path_1.default.dirname(this._filePath); if (!this._isInitialCheckComplete) { try { await node_fs_1.default.promises.access(dir); } catch (_a) { await node_fs_1.default.promises.mkdir(dir, { recursive: true }); } this._isInitialCheckComplete = true; } this._isSaving = true; await node_fs_1.default.promises.writeFile(this._filePath, (0, json_stringify_safe_1.default)(this.data, null, this.configuration.indentation), this.configuration.fileOptions); this._cleanupSaveState(); }; this._handleSaveOperation(performSave, force); } /** * Handles the save operation with proper timing and retry logic. * * @private */ _handleSaveOperation(saveFunction, force) { var _a; if (this._isClosed) { return; } if (force) { this._saveQueue = this._saveQueue.then(async () => { await saveFunction(); }); return; } if (this._isSaving) { setTimeout(() => this._handleSaveOperation(saveFunction, force), this.configuration.saveRetryTimeoutMs); return; } if (!this.configuration.autoSave) { this._saveQueue = this._saveQueue.then(async () => { await saveFunction(); }); return; } if (this._saveTimeout) { clearTimeout(this._saveTimeout); this._saveTimeoutCount++; if (this._saveTimeoutCount >= ((_a = this.configuration.maxAutoSaveTimeouts) !== null && _a !== void 0 ? _a : 0)) { this._saveQueue = this._saveQueue.then(async () => { await saveFunction(); }); return; } } this._saveTimeout = setTimeout(() => { this._saveQueue = this._saveQueue.then(async () => { await saveFunction(); }); }, this.configuration.autoSaveDelayMs); } /** * Cleans up the save state after a save operation. * * @private */ _cleanupSaveState() { this._isSaving = false; if (this._saveTimeout) { clearTimeout(this._saveTimeout); } this._saveTimeout = undefined; this._saveTimeoutCount = 0; } /** * Deletes the database file from the filesystem synchronously. * This operation cannot be undone. * * @public */ delete() { node_fs_1.default.unlinkSync(this._filePath); } /** * Deletes the database file from the filesystem asynchronously. * This operation cannot be undone. * * @public */ async deleteAsync() { await node_fs_1.default.promises.unlink(this._filePath); } /** * Reloads the data from the file synchronously. * * @public */ reload() { if (node_fs_1.default.existsSync(this._filePath)) { const newData = secure_json_parse_1.default.parse(node_fs_1.default.readFileSync(this._filePath, { encoding: "utf-8" })); this.data = this.configuration.noProxy ? newData : Veloce._createNestedProxies(newData, this._proxyHandler, this._proxyCache); } } /** * Reloads the data from the file asynchronously. * * @public */ async reloadAsync() { await node_fs_1.default.promises.access(this._filePath); const content = await node_fs_1.default.promises.readFile(this._filePath, { encoding: "utf-8", }); const newData = secure_json_parse_1.default.parse(content); this.data = this.configuration.noProxy ? newData : Veloce._createNestedProxies(newData, this._proxyHandler, this._proxyCache); } /** * Closes the database instance, cancelling any pending saves and cleaning up resources. * After closing, no further operations will be performed. * * @public */ async close() { if (this._isClosed) { return; } this._isClosed = true; if (this._saveTimeout) { clearTimeout(this._saveTimeout); this._saveTimeout = undefined; } await this._saveQueue; this._proxyCache = new WeakMap(); } } exports.Veloce = Veloce; //# sourceMappingURL=index.js.map