nerandb.fast
Version:
Nerandb benzeri async JSON/LevelDB tabanlı veritabanı modülü
329 lines (296 loc) • 9.51 kB
JavaScript
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;