UNPKG

@nano-sql/adapter-leveldb

Version:

Easily Run LevelDB in NodeJS with nanoSQL 2!

417 lines (363 loc) 14.9 kB
import { InanoSQLAdapter, InanoSQLDataModel, InanoSQLTable, InanoSQLPlugin, InanoSQLInstance, VERSION, postConnectFilter } from "@nano-sql/core/lib/interfaces"; import { allAsync, _nanoSQLQueue, generateID, maybeAssign, setFast, deepSet, deepGet, nan, blankTableDefinition } from "@nano-sql/core/lib/utilities"; import { nanoSQLMemoryIndex } from "@nano-sql/core/lib/adapters/memoryIndex"; import * as wasm from "./wasm-index"; import * as levelup from "levelup"; import * as encode from "encoding-down"; import * as lexint from "lexicographic-integer-encoding"; import * as fs from "fs"; import * as path from "path"; import * as leveldown from "leveldown"; const encoding = lexint("hex", {strict: true}); export const rimraf = (dir_path: string) => { if (fs.existsSync(dir_path)) { fs.readdirSync(dir_path).forEach(function(entry) { const entry_path = path.join(dir_path, entry); if (fs.lstatSync(entry_path).isDirectory()) { rimraf(entry_path); } else { fs.unlinkSync(entry_path); } }); fs.rmdirSync(dir_path); } }; export class LevelDB extends nanoSQLMemoryIndex { plugin: InanoSQLPlugin = { name: "LevelDB Adapter", version: 2.05 }; nSQL: InanoSQLInstance; private _id: string; private _lvlDown: (dbID: string, tableName: string, tableData: InanoSQLTable) => { lvld: any, args?: any }; private _levelDBs: { [key: string]: any; }; private _ai: { [key: string]: number; }; private _tableConfigs: { [tableName: string]: InanoSQLTable; } private _indexNum: { [tableName: string]: number; } = {}; constructor( public dbPath?: string | ((dbID: string, tableName: string, tableData: InanoSQLTable) => { lvld: any, args?: any }), public indexCache?: boolean ) { super(false, false); this._levelDBs = {}; this._ai = {}; this._tableConfigs = {}; if (typeof this.dbPath === "string" || typeof this.dbPath === "undefined") { this._lvlDown = ((dbId: string, tableName: string, tableData: InanoSQLTable) => { const basePath = path.join((this.dbPath as string) || ".", "db_" + dbId); if (!fs.existsSync(basePath)) { fs.mkdirSync(basePath); } return { lvld: leveldown(path.join(basePath, tableName)), args: { cacheSize: 64 * 1024 * 1024, writeBufferSize: 64 * 1024 * 1024 } }; }); } else { this._lvlDown = this.dbPath; } } connect(id: string, complete: () => void, error: (err: any) => void) { this._id = id; const tableName = "_ai_store_"; const lvlDownAI = this._lvlDown(this._id, tableName, { ...blankTableDefinition, pkType: "string" }); const keyEncoding = "binary"; levelup(encode(lvlDownAI.lvld, { valueEncoding: "json", keyEncoding: keyEncoding }), lvlDownAI.args, (err, db) => { if (err) { error(err); return; } if (this.indexCache) { const checkWasm = () => { if (wasm.loaded) { this._levelDBs[tableName] = db; complete(); } else { setTimeout(checkWasm, 10); } } checkWasm(); } else { this._levelDBs[tableName] = db; complete(); } }); } createTable(tableName: string, tableData: InanoSQLTable, complete: () => void, error: (err: any) => void) { if (this._levelDBs[tableName]) { error(new Error(`Table ${tableName} already exists and is open!`)); return; } this._tableConfigs[tableName] = tableData; const keyEncoding = { "int": encoding }[tableData.pkType] || "binary"; const lvlDown = this._lvlDown(this._id, tableName, tableData); levelup(encode(lvlDown.lvld, { valueEncoding: "json", keyEncoding: keyEncoding }), lvlDown.args, (err, db) => { if (err) { error(err); return; } this._levelDBs[tableName] = db; this._indexNum[tableName] = tableData.isPkNum ? wasm.new_index() : wasm.new_index_str(); this._levelDBs["_ai_store_"].get(Buffer.from(tableName, "utf-8"), (err, value) => { this._ai[tableName] = value ? value.ai || 0 : 0; if (this.indexCache && tableData) { this._levelDBs[tableName] .createKeyStream() .on("data", (data) => { if (tableData.isPkNum) { wasm.add_to_index(this._indexNum[tableName], isNaN(data) ? 0 : parseFloat(data)); } else { wasm.add_to_index_str(this._indexNum[tableName], String(data || "")); } }) .on("end", () => { complete(); }) .on("error", error); } else { complete(); } }); }); } dropTable(table: string, complete: () => void, error: (err: any) => void) { this._levelDBs["_ai_store_"].del(Buffer.from(table, "utf-8")).then(() => { this._levelDBs[table].close((err) => { try { rimraf(path.join((this.dbPath as string || "."), "db_" + this._id, table)); } catch (e) { error(e); return; } if (this.indexCache && this._tableConfigs[table]) { if (this._tableConfigs[table].isPkNum) { wasm.empty_index(this._indexNum[table]); } else { wasm.empty_index_str(this._indexNum[table]); } } delete this._levelDBs[table]; complete(); }, error); }).catch(error); } disconnect(complete: () => void, error: (err: any) => void) { allAsync(Object.keys(this._levelDBs), (table, i, next, error) => { if (this.indexCache && this._tableConfigs[table]) { this._tableConfigs[table].isPkNum ? wasm.empty_index(this._indexNum[table]) : wasm.empty_index_str(this._indexNum[table]); } this._levelDBs[table].close((err) => { if (err) { error(err); return; } delete this._tableConfigs[table]; next(null); }); }).then(complete).catch(error); } write(table: string, pk: any, row: { [key: string]: any }, complete: (pk: any) => void, error: (err: any) => void) { pk = pk || generateID(this._tableConfigs[table].pkType, this._ai[table] + 1); if (typeof pk === "undefined") { error(new Error("Can't add a row without a primary key!")); return; } if (this._tableConfigs[table].ai) { this._ai[table] = Math.max(pk, this._ai[table]); } deepSet(this._tableConfigs[table].pkCol, row, pk); if (this.indexCache) { if (this._tableConfigs[table].isPkNum) { wasm.add_to_index(this._indexNum[table], isNaN(pk) ? 0 : parseFloat(pk)); } else { wasm.add_to_index_str(this._indexNum[table], String(pk || "")); } } this._levelDBs[table].put(this._encodePk(table, pk), row, (err) => { if (err) { error(err); } else { if (this._tableConfigs[table].ai) { this._levelDBs["_ai_store_"].put(Buffer.from(table, "utf-8"), {ai: this._ai[table]}).then(() => { complete(pk); }).catch(error); } else { complete(pk); } } }); } read(table: string, pk: any, complete: (row: { [key: string]: any } | undefined) => void, error: (err: any) => void) { this._levelDBs[table].get(this._encodePk(table, pk), (err, row) => { if (err) { complete(undefined); } else { complete(row); } }); } readMulti(table: string, type: "range" | "offset" | "all", offsetOrLow: any, limitOrHigh: any, reverse: boolean, onRow: (row: { [key: string]: any }, i: number) => void, complete: () => void, error: (err: any) => void) { if (this.indexCache && type === "offset") { const ptrFn = this._tableConfigs[table].isPkNum ? wasm.read_index_offset : wasm.read_index_offset_str; const nextFn = this._tableConfigs[table].isPkNum ? wasm.read_index_offset_next : wasm.read_index_offset_str_next; const it = ptrFn(this._indexNum[table], reverse ? 1 : 0, reverse ? offsetOrLow + 2 : offsetOrLow + 1); if (it === 0) { complete(); return; } let nextKey: any = 0; let count = 0; const nextRow = () => { nextKey = nextFn(this._indexNum[table], it, reverse ? 1 : 0, limitOrHigh, count); if (count < limitOrHigh) { this.read(table, nextKey, (row) => { if (row) { onRow(row, count); } if (count % 500 === 0) { setTimeout(nextRow, 0); } else { nextRow(); } }, error); } else { complete(); } count++; } nextRow(); return; } let i = 0; this._levelDBs[table] .createValueStream(type === "range" ? { gte: type === "range" ? this._encodePk(table, offsetOrLow) : undefined, lte: type === "range" ? this._encodePk(table, limitOrHigh) : undefined, reverse: reverse } : type === "offset" ? { reverse: reverse, limit: type === "offset" ? (offsetOrLow + limitOrHigh + (reverse ? 1 : 0)) : undefined } : { reverse: reverse, }) .on("data", (data) => { if (type === "offset" && (reverse ? i < offsetOrLow + 1 : i < offsetOrLow)) { i++; return; } onRow(data, i); i++; }) .on("end", () => { complete(); }) .on("error", error); } _writeNumberBuffer(table: string, num: number): number|Buffer { switch (this._tableConfigs[table].pkType) { case "int": return num; // case "float": // case "number": default: return Buffer.from(String(num), "utf-8"); } } _readNumberBuffer(table: string, buff: any): number { switch (this._tableConfigs[table].pkType) { case "int": return buff; // case "float": // case "number": default: const buffer = new Buffer(buff); return parseFloat(buffer.toString("utf-8")); } } _encodePk(table: string, pk: any): number|Buffer { return this._tableConfigs[table].isPkNum ? this._writeNumberBuffer(table, pk) : Buffer.from(pk, "utf-8"); } _decodePK(table: string, pk: any): any { return this._tableConfigs[table].isPkNum ? this._readNumberBuffer(table, pk) : new Buffer(pk).toString("utf-8"); } delete(table: string, pk: any, complete: () => void, error: (err: any) => void) { this._levelDBs[table].del(this._encodePk(table, pk), (err) => { if (err) { throw Error(err); } else { if (this.indexCache && this._tableConfigs[table]) { if (this._tableConfigs[table].isPkNum) { wasm.del_key(this._indexNum[table], isNaN(pk) ? 0 : parseFloat(pk)); } else { wasm.del_key_str(this._indexNum[table], String(pk || "")); } } complete(); } }); } getTableIndex(table: string, complete: (index: any[]) => void, error: (err: any) => void) { if (this.indexCache && this._tableConfigs[table]) { const ptrFn = this._tableConfigs[table].isPkNum ? wasm.read_index : wasm.read_index_str; const nextFn = this._tableConfigs[table].isPkNum ? wasm.read_index_next : wasm.read_index_str_next; let it = ptrFn(this._indexNum[table], 0) if (!it) { complete([]); return; } it = it.split(",").map(s => parseInt(s)); let nextKey: any = 0; let count = 0; let keys: any[] = []; while(count < it[1]) { nextKey = nextFn(this._indexNum[table], it[0], 0, count); count++; keys.push(nextKey); } complete(keys); return; } let index: any[] = []; this._levelDBs[table] .createKeyStream() .on("data", (pk) => { index.push(this._decodePK(table, pk)); }) .on("end", () => { complete(index); }) .on("error", error); } getTableIndexLength(table: string, complete: (length: number) => void, error: (err: any) => void) { if (this.indexCache && this._tableConfigs[table]) { complete(this._tableConfigs[table].isPkNum ? wasm.get_total(this._indexNum[table]) : wasm.get_total_str(this._indexNum[table])); return; } let count = 0; this._levelDBs[table] .createKeyStream() .on("data", (pk) => { count++; }) .on("end", () => { complete(count); }) .on("error", error); } }