123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- import { Compression, getAddress, arch, fs, path as pathModule, lmdbError, EventEmitter, MsgpackrEncoder, Env,
- Dbi, tmpdir, os, nativeAddon, version } from './native.js';
- import { CachingStore, setGetLastVersion } from './caching.js';
- import { addReadMethods, makeReusableBuffer } from './read.js';
- import { addWriteMethods } from './write.js';
- import { applyKeyHandling } from './keys.js';
- let moduleRequire = typeof require == 'function' && require;
- export function setRequire(require) {
- moduleRequire = require;
- }
- setGetLastVersion(getLastVersion, getLastTxnId);
- let keyBytes, keyBytesView;
- const buffers = [];
- const { onExit, getEnvsPointer, setEnvsPointer, getEnvFlags, setJSFlags } = nativeAddon;
- if (globalThis.__lmdb_envs__)
- setEnvsPointer(globalThis.__lmdb_envs__);
- else
- globalThis.__lmdb_envs__ = getEnvsPointer();
- // this is hard coded as an upper limit because it is important assumption of the fixed buffers in writing instructions
- // this corresponds to the max key size for 8KB pages
- const MAX_KEY_SIZE = 4026;
- // this is used as the key size by default because default page size is OS page size, which is usually
- // 4KB (but is 16KB on M-series MacOS), and this keeps a consistent max key size when no page size specified.
- const DEFAULT_MAX_KEY_SIZE = 1978;
- const DEFAULT_COMMIT_DELAY = 0;
- export const allDbs = new Map();
- let defaultCompression;
- let lastSize;
- let hasRegisteredOnExit;
- export function open(path, options) {
- if (nativeAddon.open) {
- if (nativeAddon.open !== open) {
- // this is the case when lmdb-js has been opened in both ESM and CJS mode, which means that there are two
- // separate JS modules, but they are both using the same native module.
- getLastVersion = nativeAddon.getLastVersion;
- getLastTxnId = nativeAddon.getLastTxnId;
- setGetLastVersion(getLastVersion, getLastTxnId);
- return nativeAddon.open(path, options);
- }
- } else {
- nativeAddon.open = open;
- nativeAddon.getLastVersion = getLastVersion;
- nativeAddon.getLastTxnId = getLastTxnId;
- }
- if (!keyBytes) // TODO: Consolidate get buffer and key buffer (don't think we need both)
- allocateFixedBuffer();
- if (typeof path == 'object' && !options) {
- options = path;
- path = options.path;
- }
- options = options || {};
- let noFSAccess = options.noFSAccess; // this can only be configured on open, can't let users change it
- let userOptions = options;
- if (path == null) {
- options = Object.assign({
- deleteOnClose: true,
- noSync: true,
- }, options);
- path = tmpdir() + '/' + Math.floor(Math.random() * 2821109907455).toString(36) + '.mdb'
- } else if (!options)
- options = {};
- let extension = pathModule.extname(path);
- let name = pathModule.basename(path, extension);
- let is32Bit = arch().endsWith('32');
- let isLegacyLMDB = version.patch < 90;
- let remapChunks = (options.remapChunks || options.encryptionKey || (options.mapSize ?
- (is32Bit && options.mapSize > 0x100000000) : // larger than fits in address space, must use dynamic maps
- is32Bit)) && !isLegacyLMDB; // without a known map size, we default to being able to handle large data correctly/well*/
- let userMapSize = options.mapSize;
- options = Object.assign({
- noSubdir: Boolean(extension),
- isRoot: true,
- maxDbs: 12,
- remapChunks,
- keyBytes,
- overlappingSync: (options.noSync || options.readOnly) ? false : (os != 'win32'),
- // default map size limit of 4 exabytes when using remapChunks, since it is not preallocated and we can
- // make it super huge.
- mapSize: remapChunks ? 0x10000000000000 :
- isLegacyLMDB ? is32Bit ? 0x1000000 : 0x100000000 : 0x20000, // Otherwise we start small with 128KB
- safeRestore: process.env.LMDB_RESTORE == 'safe',
- }, options);
- options.path = path;
- if (options.asyncTransactionOrder == 'strict') {
- options.strictAsyncOrder = true;
- }
- if (nativeAddon.version.major + nativeAddon.version.minor / 100 + nativeAddon.version.patch / 10000 < 0.0980) {
- options.overlappingSync = false; // not support on older versions
- options.trackMetrics = false;
- options.usePreviousSnapshot = false;
- options.safeRestore = false;
- options.remapChunks = false;
- if (!userMapSize) options.mapSize = 0x40000000; // 1 GB
- }
- if (!exists(options.noSubdir ? pathModule.dirname(path) : path))
- fs.mkdirSync(options.noSubdir ? pathModule.dirname(path) : path, { recursive: true }
- );
- function makeCompression(compressionOptions) {
- if (compressionOptions instanceof Compression)
- return compressionOptions;
- let useDefault = typeof compressionOptions != 'object';
- if (useDefault && defaultCompression)
- return defaultCompression;
- compressionOptions = Object.assign({
- threshold: 1000,
- dictionary: fs.readFileSync(new URL('./dict/dict.txt', import.meta.url.replace(/dist[\\\/]index.cjs$/, ''))),
- getValueBytes: makeReusableBuffer(0),
- }, compressionOptions);
- let compression = Object.assign(new Compression(compressionOptions), compressionOptions);
- if (useDefault)
- defaultCompression = compression;
- return compression;
- }
- if (isLegacyLMDB) {
- // legacy LMDB, turn off these options
- Object.assign(options, { overlappingSync: false, remapChunks: false, safeRestore: false });
- }
- if (options.compression)
- options.compression = makeCompression(options.compression);
- let flags =
- (options.overlappingSync ? 0x1000 : 0) |
- (options.noSubdir ? 0x4000 : 0) |
- (options.noSync ? 0x10000 : 0) |
- (options.readOnly ? 0x20000 : 0) |
- (options.noMetaSync ? 0x40000 : 0) |
- (options.useWritemap ? 0x80000 : 0) |
- (options.mapAsync ? 0x100000 : 0) |
- (options.noReadAhead ? 0x800000 : 0) |
- (options.noMemInit ? 0x1000000 : 0) |
- (options.usePreviousSnapshot ? 0x2000000 : 0) |
- (options.remapChunks ? 0x4000000 : 0) |
- (options.safeRestore ? 0x800 : 0) |
- (options.trackMetrics ? 0x400 : 0);
- let env = new Env();
- let jsFlags = (options.overlappingSync ? 0x1000 : 0) |
- (options.separateFlushed ? 1 : 0) |
- (options.deleteOnClose ? 2 : 0);
- let rc = env.open(options, flags, jsFlags);
- env.path = path;
- if (rc)
- lmdbError(rc);
- delete options.keyBytes // no longer needed, don't copy to stores
- let maxKeySize = env.getMaxKeySize();
- maxKeySize = Math.min(maxKeySize, options.pageSize ? MAX_KEY_SIZE : DEFAULT_MAX_KEY_SIZE);
- flags = getEnvFlags(env.address); // re-retrieve them, they are not necessarily the same if we are connecting to an existing env
- if (flags & 0x1000) {
- if (userOptions.noSync) {
- env.close();
- throw new Error('Can not set noSync on a database that was opened with overlappingSync');
- }
- } else if (options.overlappingSync) {
- if (userOptions.overlappingSync) {
- env.close();
- throw new Error('Can not enable overlappingSync on a database that was opened without this flag');
- }
- options.overlappingSync = false;
- jsFlags = jsFlags & 0xff; // clear overlapping sync
- setJSFlags(env.address, jsFlags);
- }
- env.readerCheck(); // clear out any stale entries
- if ((options.overlappingSync || options.deleteOnClose) && !hasRegisteredOnExit && process.on) {
- hasRegisteredOnExit = true;
- process.on('exit', onExit);
- }
- class LMDBStore 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)');
- if (options.compression && dbOptions.compression !== false && typeof dbOptions.compression != 'object')
- dbOptions.compression = options.compression; // use the parent compression if available
- else if (dbOptions.compression)
- dbOptions.compression = makeCompression(dbOptions.compression);
- if (dbOptions.dupSort && (dbOptions.useVersions || dbOptions.cache)) {
- throw new Error('The dupSort flag can not be combined with versions or caching');
- }
- let keyIsBuffer = dbOptions.keyIsBuffer
- if (dbOptions.keyEncoding == 'uint32') {
- dbOptions.keyIsUint32 = true;
- } else if (dbOptions.keyEncoder) {
- if (dbOptions.keyEncoder.enableNullTermination) {
- dbOptions.keyEncoder.enableNullTermination()
- } else
- keyIsBuffer = true;
- } else if (dbOptions.keyEncoding == 'binary') {
- keyIsBuffer = true;
- }
- let flags = (dbOptions.reverseKey ? 0x02 : 0) |
- (dbOptions.dupSort ? 0x04 : 0) |
- (dbOptions.dupFixed ? 0x10 : 0) |
- (dbOptions.integerDup ? 0x20 : 0) |
- (dbOptions.reverseDup ? 0x40 : 0) |
- (!options.readOnly && dbOptions.create !== false ? 0x40000 : 0) |
- (dbOptions.useVersions ? 0x100 : 0);
- let keyType = (dbOptions.keyIsUint32 || dbOptions.keyEncoding == 'uint32') ? 2 : keyIsBuffer ? 3 : 0;
- if (keyType == 2)
- flags |= 0x08; // integer key
- if (options.readOnly) {
- // in read-only mode we use a read-only txn to open the database
- // TODO: LMDB is actually not entirely thread-safe when it comes to opening databases with
- // read-only transactions since there is a race condition on setting the update dbis that
- // occurs outside the lock
- // make sure we are using a fresh read txn, so we don't want to share with a cursor txn
- this.resetReadTxn();
- this.ensureReadTxn();
- this.db = new Dbi(env, flags, dbName, keyType, dbOptions.compression);
- } else {
- this.transactionSync(() => {
- this.db = new Dbi(env, flags, dbName, keyType, dbOptions.compression);
- }, options.overlappingSync ? 0x10002 : 2); // no flush-sync, but synchronously commit
- }
- this._commitReadTxn(); // current read transaction becomes invalid after opening another db
- if (!this.db || this.db.dbi == 0xffffffff) {// not found
- throw new Error('Database not found')
- }
- this.dbAddress = this.db.address
- this.db.name = dbName || null;
- this.name = dbName;
- this.status = 'open';
- this.env = env;
- this.reads = 0;
- this.writes = 0;
- this.transactions = 0;
- this.averageTransactionTime = 5;
- if (dbOptions.syncBatchThreshold)
- console.warn('syncBatchThreshold is no longer supported');
- if (dbOptions.immediateBatchThreshold)
- console.warn('immediateBatchThreshold is no longer supported');
- this.commitDelay = DEFAULT_COMMIT_DELAY;
- Object.assign(this, { // these are the options that are inherited
- path: options.path,
- encoding: options.encoding,
- strictAsyncOrder: options.strictAsyncOrder,
- }, dbOptions);
- let Encoder;
- if (this.encoder && this.encoder.Encoder) {
- Encoder = this.encoder.Encoder;
- this.encoder = null; // don't copy everything from the module
- }
- if (!Encoder && !(this.encoder && this.encoder.encode) && (!this.encoding || this.encoding == 'msgpack' || this.encoding == 'cbor')) {
- Encoder = (this.encoding == 'cbor' ? moduleRequire('cbor-x').Encoder : MsgpackrEncoder);
- }
- if (Encoder) {
- this.encoder = new Encoder(Object.assign(
- assignConstrainedProperties(['copyBuffers', 'getStructures', 'saveStructures', 'useFloat32', 'useRecords', 'structuredClone', 'variableMapSize', 'useTimestamp32', 'largeBigIntToFloat', 'encodeUndefinedAsNil', 'int64AsNumber', 'onInvalidDate', 'mapsAsObjects', 'useTag259ForMaps', 'pack', 'maxSharedStructures', 'shouldShareStructure', 'randomAccessStructure', 'freezeData'],
- this.sharedStructuresKey !== undefined ? this.setupSharedStructures() : {
- copyBuffers: true, // need to copy any embedded buffers that are found since we use unsafe buffers
- }, options, dbOptions), this.encoder));
- }
- if (this.encoding == 'json') {
- this.encoder = {
- encode: JSON.stringify,
- };
- } else if (this.encoder) {
- this.decoder = this.encoder;
- this.decoderCopies = !this.encoder.needsStableBuffer
- }
- this.maxKeySize = maxKeySize;
- applyKeyHandling(this);
- allDbs.set(dbName ? name + '-' + dbName : name, this);
- }
- openDB(dbName, dbOptions) {
- if (this.dupSort && this.name == null)
- throw new Error('Can not open named databases if the main database is dupSort')
- if (typeof dbName == 'object' && !dbOptions) {
- dbOptions = dbName;
- dbName = dbOptions.name;
- } else
- dbOptions = dbOptions || {};
- try {
- return dbOptions.cache ?
- new (CachingStore(LMDBStore, env))(dbName, dbOptions) :
- new LMDBStore(dbName, dbOptions);
- } catch(error) {
- if (error.message == 'Database not found')
- return; // return undefined to indicate db not found
- if (error.message.indexOf('MDB_DBS_FULL') > -1) {
- error.message += ' (increase your maxDbs option)';
- }
- throw error;
- }
- }
- open(dbOptions, callback) {
- let db = this.openDB(dbOptions);
- if (callback)
- callback(null, db);
- return db;
- }
- backup(path, compact) {
- if (noFSAccess)
- return;
- fs.mkdirSync(pathModule.dirname(path), { recursive: true });
- return new Promise((resolve, reject) => env.copy(path, compact, (error) => {
- if (error) {
- reject(error);
- } else {
- resolve();
- }
- }));
- }
- isOperational() {
- return this.status == 'open';
- }
- sync(callback) {
- return env.sync(callback || function(error) {
- if (error) {
- console.error(error);
- }
- });
- }
- deleteDB() {
- console.warn('deleteDB() is deprecated, use drop or dropSync instead');
- return this.dropSync();
- }
- dropSync() {
- this.transactionSync(() =>
- this.db.drop({
- justFreePages: false
- }), options.overlappingSync ? 0x10002 : 2);
- }
- clear(callback) {
- if (typeof callback == 'function')
- return this.clearAsync(callback);
- console.warn('clear() is deprecated, use clearAsync or clearSync instead');
- this.clearSync();
- }
- clearSync() {
- if (this.encoder) {
- if (this.encoder.clearSharedData)
- this.encoder.clearSharedData()
- else if (this.encoder.structures)
- this.encoder.structures = []
- }
- this.transactionSync(() =>
- this.db.drop({
- justFreePages: true
- }), options.overlappingSync ? 0x10002 : 2);
- }
- readerCheck() {
- return env.readerCheck();
- }
- readerList() {
- return env.readerList().join('');
- }
- 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();
- let buffer = this.getBinary(this.sharedStructuresKey);
- if (this.useVersions)
- setLastVersion(lastVersion);
- return buffer && this.decoder.decode(buffer);
- };
- return {
- saveStructures: (structures, isCompatible) => {
- return this.transactionSync(() => {
- let existingStructuresBuffer = this.getBinary(this.sharedStructuresKey);
- let existingStructures = existingStructuresBuffer && this.decoder.decode(existingStructuresBuffer);
- if (typeof isCompatible == 'function' ?
- !isCompatible(existingStructures) :
- (existingStructures && existingStructures.length != isCompatible))
- return false; // it changed, we need to indicate that we couldn't update
- this.put(this.sharedStructuresKey, structures);
- }, options.overlappingSync ? 0x10000 : 0);
- },
- getStructures,
- copyBuffers: true, // need to copy any embedded buffers that are found since we use unsafe buffers
- };
- }
- }
- // if caching class overrides putSync, don't want to double call the caching code
- const putSync = LMDBStore.prototype.putSync;
- const removeSync = LMDBStore.prototype.removeSync;
- addReadMethods(LMDBStore, { env, maxKeySize, keyBytes, keyBytesView, getLastVersion });
- if (!options.readOnly)
- addWriteMethods(LMDBStore, { env, maxKeySize, fixedBuffer: keyBytes,
- resetReadTxn: LMDBStore.prototype.resetReadTxn, ...options });
- LMDBStore.prototype.supports = {
- permanence: true,
- bufferKeys: true,
- promises: true,
- snapshots: true,
- clear: true,
- status: true,
- deferredOpen: true,
- openCallback: true,
- };
- let Class = options.cache ? CachingStore(LMDBStore, env) : LMDBStore;
- return options.asClass ? Class : new Class(options.name || null, options);
- }
- export function openAsClass(path, options) {
- if (typeof path == 'object' && !options) {
- options = path;
- path = options.path;
- }
- options = options || {};
- options.asClass = true;
- return open(path, options);
- }
- export function getLastVersion() {
- return keyBytesView.getFloat64(16, true);
- }
- export function setLastVersion(version) {
- return keyBytesView.setFloat64(16, version, true);
- }
- export function getLastTxnId() {
- return keyBytesView.getUint32(32, true);
- }
- const KEY_BUFFER_SIZE = 4096;
- function allocateFixedBuffer() {
- keyBytes = typeof Buffer != 'undefined' ? Buffer.allocUnsafeSlow(KEY_BUFFER_SIZE) : new Uint8Array(KEY_BUFFER_SIZE);
- const keyBuffer = keyBytes.buffer;
- keyBytesView = keyBytes.dataView || (keyBytes.dataView = new DataView(keyBytes.buffer, 0, KEY_BUFFER_SIZE)); // max key size is actually 4026
- keyBytes.uint32 = new Uint32Array(keyBuffer, 0, KEY_BUFFER_SIZE >> 2);
- keyBytes.float64 = new Float64Array(keyBuffer, 0, KEY_BUFFER_SIZE >> 3);
- keyBytes.uint32.address = keyBytes.address = keyBuffer.address = getAddress(keyBuffer);
- }
- function exists(path) {
- if (fs.existsSync)
- return fs.existsSync(path);
- try {
- return fs.statSync(path);
- } catch (error) {
- return false
- }
- }
- function assignConstrainedProperties(allowedProperties, target) {
- for (let i = 2; i < arguments.length; i++) {
- let source = arguments[i];
- for (let key in source) {
- if (allowedProperties.includes(key))
- target[key] = source[key];
- }
- }
- return target;
- }
|