UNPKG

@rmw/mdbx

Version:

Simple, efficient, scalable data store wrapper for libmdbx

1,092 lines (1,080 loc) 35.6 kB
const { sync: mkdirpSync } = require("mkdirp"); const fs = require("fs"); const { extname, basename, dirname } = require("path"); const { ArrayLikeIterable } = require("./util/ArrayLikeIterable"); const when = require("./util/when"); const EventEmitter = require("events"); Object.assign(exports, require("node-gyp-build")(__dirname)); const { Env, Cursor, Compression, getLastVersion, setLastVersion } = exports; const { CachingStore, setGetLastVersion } = require("./caching"); setGetLastVersion(getLastVersion); const DEFAULT_SYNC_BATCH_THRESHOLD = 200000000; // 200MB const DEFAULT_IMMEDIATE_BATCH_THRESHOLD = 10000000; // 10MB const DEFAULT_COMMIT_DELAY = 0; const READING_TNX = { readOnly: true, }; const allDbs = (exports.allDbs = new Map()); const SYNC_PROMISE_RESULT = Promise.resolve(true); const SYNC_PROMISE_FAIL = Promise.resolve(false); SYNC_PROMISE_RESULT.isSync = true; SYNC_PROMISE_FAIL.isSync = true; const LAST_KEY = String.fromCharCode(0xffff); const LAST_BUFFER_KEY = Buffer.from([255, 255, 255, 255]); const FIRST_BUFFER_KEY = Buffer.from([0]); let env; let defaultCompression; let lastSize; exports.open = open; function open(path, options) { let env = new Env(); let committingWrites; let scheduledWrites; let scheduledOperations; let readTxn, writeTxn, pendingBatch, currentCommit, runNextBatch, readTxnRenewed, cursorTxns = []; if (typeof path == "object" && !options) { options = path; path = options.path; } let extension = extname(path); let name = basename(path, extension); options = Object.assign( { path, noSubdir: Boolean(extension), isRoot: true, maxDbs: 12, mapSize: 0x1000000000, }, options ); if (!fs.existsSync(options.noSubdir ? dirname(path) : path)) mkdirpSync(options.noSubdir ? dirname(path) : path); if (options.compression) { let setDefault; if (options.compression == true) { if (defaultCompression) options.compression = defaultCompression; else defaultCompression = options.compression = new Compression({ threshold: 1000, dictionary: fs.readFileSync(require.resolve("./dict/dict.txt")), }); } else options.compression = new Compression( Object.assign({ threshold: 1000, dictionary: fs.readFileSync(require.resolve("./dict/dict.txt")), }), options.compression ); } if (options && options.clearOnStart) { console.info("Removing", path); fs.removeSync(path); console.info("Removed", path); } try { env.open(options); } catch (error) { if (error.message.startsWith("MDBX_INVALID")) { require("./util/upgrade-lmdb").upgrade(path, options, open); env = new Env(); env.open(options); } else throw error; } function renewReadTxn() { if (readTxn) readTxn.renew(); else readTxn = env.beginTxn(READING_TNX); readTxnRenewed = setImmediate(resetReadTxn); return readTxn; } function resetReadTxn() { if (readTxnRenewed) { readTxnRenewed = null; readTxn.abort(); readTxn.isAborted = true; readTxn = null; } } let stores = []; class LMDBXStore extends EventEmitter { constructor(dbName, dbOptions) { super(); if (dbName === undefined) throw new Error( "Database name must be supplied in name property (may be null for root database)" ); const openDB = () => { try { this.db = env.openDbi( Object.assign( { name: dbName, create: true, txn: writeTxn, }, dbOptions ) ); this.db.name = dbName || null; } catch (error) { handleError(error, null, null, openDB); } }; if ( dbOptions.compression && !(dbOptions.compression instanceof Compression) ) { if (dbOptions.compression == true && options.compression) dbOptions.compression = options.compression; // use the parent compression if available else dbOptions.compression = new Compression( Object.assign({ threshold: 1000, dictionary: fs.readFileSync(require.resolve("./dict/dict.txt")), }), dbOptions.compression ); } if (dbOptions.dupSort && (dbOptions.useVersions || dbOptions.cache)) { throw new Error( "The dupSort flag can not be combined with versions or caching" ); } openDB(); resetReadTxn(); // a read transaction becomes invalid after opening another db this.name = dbName; this.env = env; this.reads = 0; this.writes = 0; this.transactions = 0; this.averageTransactionTime = 5; this.syncBatchThreshold = DEFAULT_SYNC_BATCH_THRESHOLD; this.immediateBatchThreshold = DEFAULT_IMMEDIATE_BATCH_THRESHOLD; this.commitDelay = DEFAULT_COMMIT_DELAY; Object.assign( this, { // these are the options that are inherited path: options.path, encoding: options.encoding, }, dbOptions ); if ( !this.encoding || this.encoding == "msgpack" || this.encoding == "cbor" ) { this.encoder = this.decoder = new ( this.encoding == "cbor" ? require("cbor-x").Encoder : require("msgpackr").Encoder )( Object.assign( this.sharedStructuresKey ? this.setupSharedStructures() : { copyBuffers: true, // need to copy any embedded buffers that are found since we use unsafe buffers }, options, dbOptions ) ); } else if (this.encoding == "json") { this.encoder = { encode: JSON.stringify, }; } allDbs.set(dbName ? name + "-" + dbName : name, this); stores.push(this); } openDB(dbName, dbOptions) { if (typeof dbName == "object" && !dbOptions) { dbOptions = dbName; dbName = options.name; } else dbOptions = dbOptions || {}; try { return dbOptions.cache ? new (CachingStore(LMDBXStore))(dbName, dbOptions) : new LMDBXStore(dbName, dbOptions); } catch (error) { if (error.message.indexOf("MDBX_DBS_FULL") > -1) { error.message += " (increase your maxDbs option)"; } throw error; } } transaction(execute, abort) { let result; if (writeTxn) { // already nested in a transaction, just execute and return result = execute(); return result; } let txn; try { this.transactions++; resetReadTxn(); txn = writeTxn = env.beginTxn(); /*if (scheduledOperations && runNextBatch) { runNextBatch((operations, callback) => { try { callback(null, this.commitBatchNow(operations)) } catch (error) { callback(error) } }) } TODO: To reenable forced sequential writes, we need to re-execute the operations if we get an env resize */ return when( execute(), (result) => { try { resetReadTxn(); if (abort) { txn.abort(); } else { txn.commit(); } writeTxn = null; return result; } catch (error) { if (error.message == "The transaction is already closed.") { return result; } return handleError(error, this, txn, () => this.transaction(execute) ); } }, (error) => { return handleError(error, this, txn, () => this.transaction(execute) ); } ); } catch (error) { return handleError(error, this, txn, () => this.transaction(execute)); } } get(id) { let txn; try { if (writeTxn) { txn = writeTxn; } else { txn = readTxnRenewed ? readTxn : renewReadTxn(); } let result; if (this.decoder) { this.lastSize = result = txn.getBinaryUnsafe(this.db, id); return result && this.decoder.decode(this.db.unsafeBuffer, result); } if (this.encoding == "binary") { result = txn.getBinary(this.db, id); this.lastSize = result; return result; } result = txn.getUtf8(this.db, id); if (result) { this.lastSize = result.length; if (this.encoding == "json") return JSON.parse(result); } return result; } catch (error) { return handleError(error, this, txn, () => this.get(id)); } } getEntry(id) { let value = this.get(id); if (value !== undefined) { if (this.useVersions) return { value, version: getLastVersion(), //size: lastSize }; else return { value, //size: lastSize }; } } resetReadTxn() { resetReadTxn(); } ifNoExists(key, callback) { return this.ifVersion(key, null, callback); } ifVersion(key, version, callback) { if (typeof version != "number") { if (version == null) { if (version === null) version = -4.2434325325532e-199; // NO_EXIST_VERSION else { // if undefined, just do callback without any condition being added callback(); // TODO: if we are inside another ifVersion, use that promise, or use ANY_VERSION return pendingBatch ? pendingBatch.unconditionalResults : Promise.resolve(true); // be consistent in returning a promise, indicate success } } else { throw new Error("Version must be a number or null"); } } let scheduledOperations = this.getScheduledOperations(); let index = scheduledOperations.push([key, version]) - 1; try { callback(); let commit = this.scheduleCommit(); return commit.results.then((writeResults) => { if (writeResults[index] === 0) return true; if (writeResults[index] === 3) { throw new Error("The key size was 0 or too large"); } return false; }); } finally { scheduledOperations.push(false); // reset condition } } getScheduledOperations() { if (!scheduledOperations) { scheduledOperations = []; scheduledOperations.bytes = 0; } if (scheduledOperations.store != this) { // issue action to switch dbs scheduledOperations.store = this; scheduledOperations.push(this.db); } return scheduledOperations; } put(id, value, version, ifVersion) { if (id.length > 511) { throw new Error("Key is larger than maximum key size (511)"); } this.writes++; if (writeTxn) { if (ifVersion !== undefined) { this.get(id); let previousVersion = this.get(id) ? getLastVersion() : null; if (!matches(previousVersion, ifVersion)) { return SYNC_PROMISE_FAIL; } } this.putSync(id, value, version); return SYNC_PROMISE_RESULT; } if (this.encoder) value = this.encoder.encode(value); else if (typeof value != "string" && !(value && value.buffer)) throw new Error( "Invalid value to put in database " + value + " (" + typeof value + "), consider using encoder" ); let operations = this.getScheduledOperations(); let index = operations.push( ifVersion == null ? version == null ? [id, value] : [id, value, version] : [id, value, version, ifVersion] ) - 1; // track the size of the scheduled operations (and include the approx size of the array structure too) operations.bytes += (id.length || 6) + ((value && value.length) || 0) + 100; let commit = this.scheduleCommit(); return ifVersion === undefined ? commit.unconditionalResults // TODO: Technically you can get a bad key if an array is passed in there is no ifVersion and still fail : commit.results.then((writeResults) => { if (writeResults[index] === 0) return true; if (writeResults[index] === 3) { throw new Error("The key size was 0 or too large"); } return false; }); } putSync(id, value, version) { if (id.length > 511) { throw new Error("Key is larger than maximum key size (511)"); } let localTxn, hadWriteTxn = writeTxn; try { this.writes++; if (!writeTxn) { localTxn = writeTxn = env.beginTxn(); } if (this.encoder) value = this.encoder.encode(value); if (typeof value == "string") { writeTxn.putUtf8(this.db, id, value, version); } else { if (!(value && value.buffer)) { throw new Error( "Invalid value type " + typeof value + " used " + value ); } writeTxn.putBinary(this.db, id, value, version); } if (localTxn) { writeTxn.commit(); writeTxn = null; } } catch (error) { if (hadWriteTxn) throw error; // if we are in a transaction, the whole transaction probably needs to restart return handleError(error, this, localTxn, () => this.putSync(id, value, version) ); } } removeSync(id, ifVersionOrValue) { if (id.length > 511) { throw new Error("Key is larger than maximum key size (511)"); } let localTxn, hadWriteTxn = writeTxn; try { if (!writeTxn) { localTxn = writeTxn = env.beginTxn(); } let deleteValue; if (ifVersionOrValue !== undefined) { if (this.useVersions) { let previousVersion = this.get(id) ? getLastVersion() : null; if (!matches(previousVersion, ifVersionOrValue)) return false; } else if (this.encoder) deleteValue = this.encoder.encode(ifVersionOrValue); } this.writes++; let result; if (deleteValue) result = writeTxn.del(this.db, id, deleteValue); else result = writeTxn.del(this.db, id); if (localTxn) { writeTxn.commit(); writeTxn = null; resetReadTxn(); } return result; // object found and deleted } catch (error) { if (hadWriteTxn) throw error; // if we are in a transaction, the whole transaction probably needs to restart return handleError(error, this, localTxn, () => this.removeSync(id)); } } remove(id, ifVersionOrValue) { if (id.length > 511) { throw new Error("Key is larger than maximum key size (511)"); } this.writes++; if (writeTxn) { if (this.removeSync(id, ifVersionOrValue) === false) return SYNC_PROMISE_FAIL; return SYNC_PROMISE_RESULT; } let scheduledOperations = this.getScheduledOperations(); let operation; if (ifVersionOrValue === undefined) operation = [id]; else if (this.useVersions) operation = [id, undefined, undefined, ifVersionOrValue]; // version condition else { if (this.encoder) operation = [id, this.encoder.encode(ifVersionOrValue), true]; else operation = [id, ifVersionOrValue, true]; } let index = scheduledOperations.push(operation) - 1; // remove specific values scheduledOperations.bytes += (id.length || 6) + 100; let commit = this.scheduleCommit(); return ifVersionOrValue === undefined ? commit.unconditionalResults : commit.results.then((writeResults) => { if (writeResults[index] === 0) return true; if (writeResults[index] === 3) { throw new Error("The key size was 0 or too large"); } return false; }); } getValues(key, options) { let defaultOptions = { start: key, valuesForKey: true, }; return this.getRange( options ? Object.assign(defaultOptions, options) : defaultOptions ); } getKeys(options) { if (!options) options = {}; options.values = false; return this.getRange(options); } getRange(options) { let iterable = new ArrayLikeIterable(); if (!options) options = {}; let includeValues = options.values !== false; let includeVersions = options.versions; let valuesForKey = options.valuesForKey; let db = this.db; iterable[Symbol.iterator] = () => { let currentKey = options.start !== undefined ? options.start : options.reverse ? this.keyIsUint32 ? 0xffffffff : this.keyIsBuffer ? LAST_BUFFER_KEY : LAST_KEY : this.keyIsUint32 ? 0 : this.keyIsBuffer ? FIRST_BUFFER_KEY : false; let endKey = options.end !== undefined ? options.end : options.reverse ? this.keyIsUint32 ? 0 : this.keyIsBuffer ? FIRST_BUFFER_KEY : false : this.keyIsUint32 ? 0xffffffff : this.keyIsBuffer ? LAST_BUFFER_KEY : LAST_KEY; const reverse = options.reverse; let count = 0; let cursor; let txn; function resetCursor() { try { txn = writeTxn || (readTxnRenewed ? readTxn : renewReadTxn()); cursor = new Cursor(txn, db); txn.cursorCount = (txn.cursorCount || 0) + 1; if (reverse) { if (valuesForKey) { // position at key currentKey = cursor.goToKey(currentKey); // now move to next key and then previous entry to get to last value if (currentKey) { cursor.goToNextNoDup(); cursor.goToPrev(); } } else { // for reverse retrieval, goToRange is backwards because it positions at the key equal or *greater than* the provided key let nextKey = cursor.goToRange(currentKey); if (nextKey) { if (compareKey(nextKey, currentKey)) { // goToRange positioned us at a key after the provided key, so we need to go the previous key to be less than the provided key currentKey = cursor.goToPrev(); } else currentKey = nextKey; // they match, we are good, and currentKey is already correct } else { // likewise, we have been position beyond the end of the index, need to go to last currentKey = cursor.goToLast(); } } } else { // for forward retrieval, goToRange does what we want currentKey = valuesForKey ? cursor.goToKey(currentKey) : cursor.goToRange(currentKey); } // TODO: Make a makeCompare(endKey) } catch (error) { if (cursor) { try { cursor.close(); } catch (error) {} } return handleError(error, this, txn, () => iterable[Symbol.iterator]() ); } } resetCursor(); let offset = options.offset; while (offset-- > 0 && currentKey !== undefined) { currentKey = reverse ? valuesForKey ? cursor.goToPrevDup() : includeValues ? cursor.goToPrev() : cursor.goToPrevNoDup() : valuesForKey ? cursor.goToNextDup() : includeValues ? cursor.goToNext() : cursor.goToNextNoDup(); } let store = this; function finishCursor() { cursor.close(); if (--txn.cursorCount <= 0 && txn.onlyCursor) { let index = cursorTxns.indexOf(txn); if (index > -1) cursorTxns.splice(index, 1); txn.abort(); // this is no longer main read txn, abort it now that we are done } return { done: true }; } return { next() { if (txn.isAborted) resetCursor(); if (count > 0) currentKey = reverse ? valuesForKey ? cursor.goToPrevDup() : includeValues ? cursor.goToPrev() : cursor.goToPrevNoDup() : valuesForKey ? cursor.goToNextDup() : includeValues ? cursor.goToNext() : cursor.goToNextNoDup(); if ( currentKey === undefined || (reverse ? compareKey(currentKey, endKey) <= 0 : compareKey(currentKey, endKey) >= 0) || count++ >= options.limit ) { return finishCursor(); } if (includeValues) { let value; if (store.decoder) { lastSize = value = cursor.getCurrentBinaryUnsafe(); if (value) value = store.decoder.decode(store.db.unsafeBuffer, value); } else if (store.encoding == "binary") value = cursor.getCurrentBinary(); else { value = cursor.getCurrentUtf8(); if (store.encoding == "json" && value) value = JSON.parse(value); } if (includeVersions) return { value: { key: currentKey, value, version: getLastVersion(), }, }; else if (valuesForKey) return { value, }; else return { value: { key: currentKey, value, }, }; } else if (includeVersions) { cursor.getCurrentBinaryUnsafe(); return { value: { key: currentKey, version: getLastVersion(), }, }; } else { return { value: currentKey, }; } }, return() { return finishCursor(); }, throw() { return finishCursor(); }, }; }; return iterable; } scheduleCommit() { if (!pendingBatch) { // pendingBatch promise represents the completion of the transaction let whenCommitted = new Promise((resolve, reject) => { runNextBatch = (sync) => { if (!whenCommitted) return; runNextBatch = null; if (pendingBatch) { for (const store of stores) { store.emit("beforecommit", { scheduledOperations }); } } clearTimeout(timeout); currentCommit = whenCommitted; whenCommitted = null; pendingBatch = null; if (scheduledOperations) { // operations to perform, collect them as an array and start doing them let operations = scheduledOperations; scheduledOperations = null; const writeBatch = () => { let start = Date.now(); let results = Buffer.alloc(operations.length); let callback = (error) => { let duration = Date.now() - start; this.averageTransactionTime = (this.averageTransactionTime * 3 + duration) / 4; //console.log('did batch', (duration) + 'ms', name, operations.length/*map(o => o[1].toString('binary')).join(',')*/) resetReadTxn(); if (error) { try { // see if we can recover from recoverable error (like full map with a resize) handleError(error, this, null, writeBatch); } catch (error) { currentCommit = null; for (const store of stores) { store.emit("aftercommit", { operations }); } reject(error); } } else { currentCommit = null; for (const store of stores) { store.emit("aftercommit", { operations, results }); } resolve(results); } }; try { if (sync === true) { env.batchWrite(operations, results); callback(); } else env.batchWrite(operations, results, callback); } catch (error) { callback(error); } }; try { writeBatch(); } catch (error) { reject(error); } } else { resolve([]); } }; let timeout; if (this.commitDelay > 0) { timeout = setTimeout(() => { when( currentCommit, () => whenCommitted && runNextBatch(), () => whenCommitted && runNextBatch() ); }, this.commitDelay); } else { timeout = runNextBatch.immediate = setImmediate(() => { when( currentCommit, () => whenCommitted && runNextBatch(), () => whenCommitted && runNextBatch() ); }); } }); pendingBatch = { results: whenCommitted, unconditionalResults: whenCommitted.then(() => true), // for returning from non-conditional operations }; } if ( scheduledOperations && scheduledOperations.bytes >= this.immediateBatchThreshold && runNextBatch ) { if ( scheduledOperations && scheduledOperations.bytes >= this.syncBatchThreshold ) { // past a certain threshold, run it immediately and synchronously let batch = pendingBatch; console.warn( "Performing synchronous commit in database " + this.name + " because over " + this.syncBatchThreshold + " bytes were included in one transaction, should run transactions over separate event turns to avoid this or increase syncBatchThreshold" ); runNextBatch(true); return batch; } else if (!runNextBatch.immediate) { let thisNextBatch = runNextBatch; runNextBatch.immediate = setImmediate(() => when(currentCommit, () => thisNextBatch()) ); } } return pendingBatch; } batch(operations) { /*if (writeTxn) { this.commitBatchNow(operations.map(operation => [this.db, operation.key, operation.value])) return Promise.resolve(true) }*/ let scheduledOperations = this.getScheduledOperations(); for (let operation of operations) { let value = operation.value; scheduledOperations.push([operation.key, value]); scheduledOperations.bytes += operation.key.length + ((value && value.length) || 0) + 200; } return this.scheduleCommit().unconditionalResults; } backup(path, compact) { return new Promise((resolve, reject) => env.copy(path, compact, (error) => { if (error) { reject(error); } else { resolve(); } }) ); } close() { this.db.close(); if (this.isRoot) { readTxnRenewed = null; env.close(); } } getStats() { try { let stats = this.db.stat(readTxnRenewed ? readTxn : renewReadTxn()); return stats; } catch (error) { return handleError(error, this, readTxn, () => this.getStats()); } } sync(callback) { return env.sync( callback || function (error) { if (error) { console.error(error); } } ); } deleteDB() { //console.log('clearing db', name) try { this.db.drop({ justFreePages: false, txn: writeTxn, }); } catch (error) { handleError(error, this, null, () => this.deleteDB()); } } clear() { //console.log('clearing db', name) try { this.db.drop({ justFreePages: true, txn: writeTxn, }); } catch (error) { handleError(error, this, null, () => this.clear()); } if (this.encoder && this.encoder.structures) this.encoder.structures = []; } setupSharedStructures() { const getStructures = () => { let lastVersion; // because we are doing a read here, we may need to save and restore the lastVersion from the last read if (this.useVersions) lastVersion = getLastVersion(); try { let buffer = ( writeTxn || (readTxnRenewed ? readTxn : renewReadTxn()) ).getBinary(this.db, this.sharedStructuresKey); if (this.useVersions) setLastVersion(lastVersion); return buffer ? this.encoder.decode(buffer) : []; } catch (error) { return handleError(error, this, null, getStructures); } }; return { saveStructures: (structures, previousLength) => { return this.transaction(() => { let existingStructuresBuffer = writeTxn.getBinary( this.db, this.sharedStructuresKey ); let existingStructures = existingStructuresBuffer ? this.encoder.decode(existingStructuresBuffer) : []; if (existingStructures.length != previousLength) return false; // it changed, we need to indicate that we couldn't update resetReadTxn(); writeTxn.putBinary( this.db, this.sharedStructuresKey, this.encoder.encode(structures) ); }); }, getStructures, copyBuffers: true, // need to copy any embedded buffers that are found since we use unsafe buffers }; } } return options.cache ? new (CachingStore(LMDBXStore))(options.name || null, options) : new LMDBXStore(options.name || null, options); function handleError(error, store, txn, retry) { try { if (writeTxn) writeTxn.abort(); } catch (error) {} if (writeTxn) writeTxn = null; if (error.message == "The transaction is already closed.") { try { if (readTxn) readTxn.abort(); } catch (error) {} try { readTxn = env.beginTxn(READING_TNX); } catch (error) { return handleError(error, store, null, retry); } return retry(); } if ( error.message.startsWith("MDBX_") && !( error.message.startsWith("MDBX_KEYEXIST") || error.message.startsWith("MDBX_NOTFOUND") ) ) { resetReadTxn(); // separate out cursor-based read txns try { if (readTxn) readTxn.abort(); } catch (error) {} readTxnRenewed = null; readTxn = null; } /*if (error.message.startsWith('MDBX_MAP_FULL') || error.message.startsWith('MDBX_MAP_RESIZED')) { const oldSize = env.info().mapSize const newSize = error.message.startsWith('MDBX_MAP_FULL') ? Math.floor(((1.06 + 3000 / Math.sqrt(oldSize)) * oldSize) / 0x100000) * 0x100000 : // increase size, more rapidly at first, and round to nearest 1 MB 0 // for resized notifications, we simply want to match the existing size of other processes for (const store of stores) { store.emit('remap') } env.resize(newSize) let result = retry() return result }/* else if (error.message.startsWith('MDBX_PAGE_NOTFOUND') || error.message.startsWith('MDBX_CURSOR_FULL') || error.message.startsWith('MDBX_CORRUPTED') || error.message.startsWith('MDBX_INVALID')) { // the noSync setting means that we can have partial corruption and we need to be able to recover for (const store of stores) { store.emit('remap') } try { env.close() } catch (error) {} console.warn('Corrupted database,', path, 'attempting to delete the store file and restart', error) fs.removeSync(path + '.mdb') env = new Env() env.open(options) openDB() return retry() }*/ error.message = "In database " + name + ": " + error.message; throw error; } } function matches(previousVersion, ifVersion) { let matches; if (previousVersion) { if (ifVersion) { matches = previousVersion == ifVersion; } else { matches = false; } } else { matches = !ifVersion; } return matches; } function compareKey(a, b) { // compare with type consistency that matches ordered-binary if (typeof a == "object") { if (!a) { return b == null ? 0 : -1; } if (a.compare) { if (b == null) { return 1; } else if (b.compare) { return a.compare(b); } else { return -1; } } let arrayComparison; if (b instanceof Array) { let i = 0; while ((arrayComparison = compareKey(a[i], b[i])) == 0 && i <= a.length) { i++; } return arrayComparison; } arrayComparison = compareKey(a[0], b); if (arrayComparison == 0 && a.length > 1) return 1; return arrayComparison; } else if (typeof a == typeof b) { if (typeof a === "symbol") { a = Symbol.keyFor(a); b = Symbol.keyFor(b); } return a < b ? -1 : a === b ? 0 : 1; } else if (typeof b == "object") { if (b instanceof Array) return -compareKey(b, a); return 1; } else { return typeOrder[typeof a] < typeOrder[typeof b] ? -1 : 1; } } class Entry { constructor(value, version, db) { this.value = value; this.version = version; this.db = db; } ifSamePut() {} ifSameRemove() {} } exports.compareKey = compareKey; const typeOrder = { symbol: 0, undefined: 1, boolean: 2, number: 3, string: 4, }; exports.getLastEntrySize = function () { return lastSize; };