UNPKG

nerandb.fast

Version:

Nerandb benzeri async JSON/LevelDB tabanlı veritabanı modülü

329 lines (296 loc) 9.51 kB
const { Level } = require("level"); const { join } = require("path"); const { Mutex } = require("async-mutex"); const db = new Level(join("database"), { valueEncoding: "json" }); const _locks = new Map(); function _lockFor(key) { if (!_locks.has(key)) _locks.set(key, new Mutex()); return _locks.get(key); } function splitPath(path) { if (typeof path !== "string") return []; const parts = []; let cur = ""; let escape = false; for (let i = 0; i < path.length; i++) { const ch = path[i]; if (escape) { cur += ch; escape = false; continue; } if (ch === "\\") { escape = true; continue; } if (ch === ".") { if (cur.length > 0) parts.push(cur); cur = ""; continue; } cur += ch; } if (cur.length > 0) parts.push(cur); return parts; } function getByPath(obj, path) { if (obj === null || obj === undefined) return undefined; const parts = splitPath(path); return parts.reduce((o, p) => (o ? o[p] : undefined), obj); } function setByPath(obj, path, value) { if (!obj || typeof obj !== "object" || Array.isArray(obj)) obj = {}; const parts = splitPath(path); if (parts.length === 0) return value; let ref = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (typeof ref[part] !== "object" || ref[part] === null || Array.isArray(ref[part])) ref[part] = {}; ref = ref[part]; } ref[parts[parts.length - 1]] = value; return obj; } function deleteByPath(obj, path) { if (!obj || typeof obj !== "object") return obj; const parts = splitPath(path); if (parts.length === 0) return obj; let ref = obj; for (let i = 0; i < parts.length - 1; i++) { if (!ref[parts[i]] || typeof ref[parts[i]] !== "object") return obj; ref = ref[parts[i]]; } delete ref[parts[parts.length - 1]]; return obj; } function removeEmptyData(obj) { if (obj === null || obj === undefined) return obj; if (Array.isArray(obj)) { for (let i = obj.length - 1; i >= 0; i--) { if (obj[i] && typeof obj[i] === "object") removeEmptyData(obj[i]); if ( obj[i] === null || obj[i] === "" || (typeof obj[i] === "object" && (Array.isArray(obj[i]) ? obj[i].length === 0 : Object.keys(obj[i]).length === 0)) ) { obj.splice(i, 1); } } return obj; } for (const key of Object.keys(obj)) { if (obj[key] && typeof obj[key] === "object") removeEmptyData(obj[key]); if (obj[key] === null || obj[key] === "") delete obj[key]; if (typeof obj[key] === "object") { if (Array.isArray(obj[key])) { if (obj[key].length === 0) delete obj[key]; } else if (Object.keys(obj[key]).length === 0) delete obj[key]; } } return obj; } function deepEqual(a, b) { if (a === b) return true; if (typeof a !== typeof b) return false; if (a && b && typeof a === "object") { if (Array.isArray(a)) { if (!Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false; return true; } const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) return false; for (const k of aKeys) if (!deepEqual(a[k], b[k])) return false; return true; } return false; } const database = { noBlankData: true, async set(key, value, options = { skipLock: false }) { const parts = splitPath(key); const topKey = parts.shift() || ""; if (value === null || value === undefined) { return this.delete(key, options); } const runner = async () => { let row; try { row = await db.get(topKey); } catch (e) { if (e && e.code === "LEVEL_NOT_FOUND") { row = parts.length > 0 ? {} : undefined; } else throw e; } if (parts.length > 0 && (typeof row !== "object" || row === null)) row = {}; const finalValue = parts.length > 0 ? setByPath(row, parts.join("."), value) : value; if (this.noBlankData && typeof finalValue === "object" && finalValue !== null) { const isEmpty = Array.isArray(finalValue) ? finalValue.length === 0 : Object.keys(finalValue).length === 0; if (isEmpty) { await db.del(topKey).catch(e => { if (!(e && e.code === "LEVEL_NOT_FOUND")) throw e; }); return value; } } await db.put(topKey, finalValue); return value; }; if (options.skipLock) return runner(); const lock = _lockFor(topKey); return lock.runExclusive(runner); }, async get(key) { const parts = splitPath(key); const topKey = parts.shift() || ""; try { const row = await db.get(topKey); if (parts.length > 0 && (typeof row !== "object" || row === null)) return null; return parts.length > 0 ? getByPath(row, parts.join(".")) : row; } catch (e) { if (e && e.code === "LEVEL_NOT_FOUND") return null; throw e; } }, async fetch(key) { return this.get(key); }, async has(key) { const value = await this.get(key); return value !== null && value !== undefined; }, async delete(key, options = { skipLock: false }) { const parts = splitPath(key); const topKey = parts.shift() || ""; const runner = async () => { try { if (parts.length === 0) { await db.del(topKey); return true; } let row = await db.get(topKey); if (typeof row !== "object" || row === null) return false; row = deleteByPath(row, parts.join(".")); if (this.noBlankData) { row = removeEmptyData(row); } if (typeof row === "object" && (Array.isArray(row) ? row.length === 0 : Object.keys(row).length === 0)) { await db.del(topKey).catch(e => { if (!(e && e.code === "LEVEL_NOT_FOUND")) throw e; }); } else { await db.put(topKey, row); } return true; } catch (e) { if (e && e.code === "LEVEL_NOT_FOUND") return false; throw e; } }; if (options.skipLock) return runner(); const lock = _lockFor(topKey); return lock.runExclusive(runner); }, async push(key, value) { const parts = splitPath(key); const topKey = parts.shift() || ""; const lock = _lockFor(topKey); return lock.runExclusive(async () => { const current = await this.get(key); const arr = Array.isArray(current) ? current.slice() : []; arr.push(value); await this.set(key, arr, { skipLock: true }); return arr; }); }, async unpush(key, value, options = { mode: "strict" }) { const parts = splitPath(key); const topKey = parts.shift() || ""; const lock = _lockFor(topKey); return lock.runExclusive(async () => { const current = await this.get(key); let arr = Array.isArray(current) ? current.slice() : []; if (options && options.mode === "deep") { arr = arr.filter(item => !deepEqual(item, value)); } else { arr = arr.filter(item => item !== value); } await this.set(key, arr, { skipLock: true }); return arr; }); }, async add(key, number) { if (typeof number !== "number" || Number.isNaN(number)) throw new TypeError("number param must be a valid number"); const parts = splitPath(key); const topKey = parts.shift() || ""; const lock = _lockFor(topKey); return lock.runExclusive(async () => { const current = await this.get(key); let num = typeof current === "number" ? current : 0; num += number; await this.set(key, num, { skipLock: true }); return num; }); }, async subtract(key, number) { return this.add(key, -number); }, async all() { const results = []; for await (const [key, value] of db.iterator()) { results.push({ ID: key, data: value }); } return results; }, async deleteAll() { await db.clear(); return true; }, async type(key) { const val = await this.get(key); if (val === null) return "null"; if (Array.isArray(val)) return "array"; return typeof val; }, async startsWith(prefix) { const results = []; const gte = prefix; const lte = prefix + "\xff"; for await (const [key, value] of db.iterator({ gte, lte })) { results.push({ ID: key, data: value }); } return results; }, async find(fn) { const allData = await this.all(); return allData.find(fn) ?? null; }, async delByPriority(key, index) { const parts = splitPath(key); const topKey = parts.shift() || ""; const lock = _lockFor(topKey); return lock.runExclusive(async () => { let arr = await this.get(key); if (!Array.isArray(arr)) return []; if (index < 1 || index > arr.length) return arr; arr.splice(index - 1, 1); await this.set(key, arr, { skipLock: true }); return arr; }); }, async setByPriority(key, value, index) { const parts = splitPath(key); const topKey = parts.shift() || ""; const lock = _lockFor(topKey); return lock.runExclusive(async () => { let arr = await this.get(key); if (!Array.isArray(arr)) arr = []; if (index < 1) index = 1; if (index > arr.length + 1) index = arr.length + 1; arr.splice(index - 1, 0, value); await this.set(key, arr, { skipLock: true }); return arr; }); }, async close() { await db.close(); } }; module.exports = database;