velocedb
Version:
High-performance, secure, and robust local database
390 lines • 15.2 kB
JavaScript
"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