open.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. import { Compression, getAddress, arch, fs, path as pathModule, lmdbError, EventEmitter, MsgpackrEncoder, Env,
  2. Dbi, tmpdir, os, nativeAddon, version } from './native.js';
  3. import { CachingStore, setGetLastVersion } from './caching.js';
  4. import { addReadMethods, makeReusableBuffer } from './read.js';
  5. import { addWriteMethods } from './write.js';
  6. import { applyKeyHandling } from './keys.js';
  7. let moduleRequire = typeof require == 'function' && require;
  8. export function setRequire(require) {
  9. moduleRequire = require;
  10. }
  11. setGetLastVersion(getLastVersion, getLastTxnId);
  12. let keyBytes, keyBytesView;
  13. const buffers = [];
  14. const { onExit, getEnvsPointer, setEnvsPointer, getEnvFlags, setJSFlags } = nativeAddon;
  15. if (globalThis.__lmdb_envs__)
  16. setEnvsPointer(globalThis.__lmdb_envs__);
  17. else
  18. globalThis.__lmdb_envs__ = getEnvsPointer();
  19. // this is hard coded as an upper limit because it is important assumption of the fixed buffers in writing instructions
  20. // this corresponds to the max key size for 8KB pages
  21. const MAX_KEY_SIZE = 4026;
  22. // this is used as the key size by default because default page size is OS page size, which is usually
  23. // 4KB (but is 16KB on M-series MacOS), and this keeps a consistent max key size when no page size specified.
  24. const DEFAULT_MAX_KEY_SIZE = 1978;
  25. const DEFAULT_COMMIT_DELAY = 0;
  26. export const allDbs = new Map();
  27. let defaultCompression;
  28. let lastSize;
  29. let hasRegisteredOnExit;
  30. export function open(path, options) {
  31. if (nativeAddon.open) {
  32. if (nativeAddon.open !== open) {
  33. // this is the case when lmdb-js has been opened in both ESM and CJS mode, which means that there are two
  34. // separate JS modules, but they are both using the same native module.
  35. getLastVersion = nativeAddon.getLastVersion;
  36. getLastTxnId = nativeAddon.getLastTxnId;
  37. setGetLastVersion(getLastVersion, getLastTxnId);
  38. return nativeAddon.open(path, options);
  39. }
  40. } else {
  41. nativeAddon.open = open;
  42. nativeAddon.getLastVersion = getLastVersion;
  43. nativeAddon.getLastTxnId = getLastTxnId;
  44. }
  45. if (!keyBytes) // TODO: Consolidate get buffer and key buffer (don't think we need both)
  46. allocateFixedBuffer();
  47. if (typeof path == 'object' && !options) {
  48. options = path;
  49. path = options.path;
  50. }
  51. options = options || {};
  52. let noFSAccess = options.noFSAccess; // this can only be configured on open, can't let users change it
  53. let userOptions = options;
  54. if (path == null) {
  55. options = Object.assign({
  56. deleteOnClose: true,
  57. noSync: true,
  58. }, options);
  59. path = tmpdir() + '/' + Math.floor(Math.random() * 2821109907455).toString(36) + '.mdb'
  60. } else if (!options)
  61. options = {};
  62. let extension = pathModule.extname(path);
  63. let name = pathModule.basename(path, extension);
  64. let is32Bit = arch().endsWith('32');
  65. let isLegacyLMDB = version.patch < 90;
  66. let remapChunks = (options.remapChunks || options.encryptionKey || (options.mapSize ?
  67. (is32Bit && options.mapSize > 0x100000000) : // larger than fits in address space, must use dynamic maps
  68. is32Bit)) && !isLegacyLMDB; // without a known map size, we default to being able to handle large data correctly/well*/
  69. let userMapSize = options.mapSize;
  70. options = Object.assign({
  71. noSubdir: Boolean(extension),
  72. isRoot: true,
  73. maxDbs: 12,
  74. remapChunks,
  75. keyBytes,
  76. overlappingSync: (options.noSync || options.readOnly) ? false : (os != 'win32'),
  77. // default map size limit of 4 exabytes when using remapChunks, since it is not preallocated and we can
  78. // make it super huge.
  79. mapSize: remapChunks ? 0x10000000000000 :
  80. isLegacyLMDB ? is32Bit ? 0x1000000 : 0x100000000 : 0x20000, // Otherwise we start small with 128KB
  81. safeRestore: process.env.LMDB_RESTORE == 'safe',
  82. }, options);
  83. options.path = path;
  84. if (options.asyncTransactionOrder == 'strict') {
  85. options.strictAsyncOrder = true;
  86. }
  87. if (nativeAddon.version.major + nativeAddon.version.minor / 100 + nativeAddon.version.patch / 10000 < 0.0980) {
  88. options.overlappingSync = false; // not support on older versions
  89. options.trackMetrics = false;
  90. options.usePreviousSnapshot = false;
  91. options.safeRestore = false;
  92. options.remapChunks = false;
  93. if (!userMapSize) options.mapSize = 0x40000000; // 1 GB
  94. }
  95. if (!exists(options.noSubdir ? pathModule.dirname(path) : path))
  96. fs.mkdirSync(options.noSubdir ? pathModule.dirname(path) : path, { recursive: true }
  97. );
  98. function makeCompression(compressionOptions) {
  99. if (compressionOptions instanceof Compression)
  100. return compressionOptions;
  101. let useDefault = typeof compressionOptions != 'object';
  102. if (useDefault && defaultCompression)
  103. return defaultCompression;
  104. compressionOptions = Object.assign({
  105. threshold: 1000,
  106. dictionary: fs.readFileSync(new URL('./dict/dict.txt', import.meta.url.replace(/dist[\\\/]index.cjs$/, ''))),
  107. getValueBytes: makeReusableBuffer(0),
  108. }, compressionOptions);
  109. let compression = Object.assign(new Compression(compressionOptions), compressionOptions);
  110. if (useDefault)
  111. defaultCompression = compression;
  112. return compression;
  113. }
  114. if (isLegacyLMDB) {
  115. // legacy LMDB, turn off these options
  116. Object.assign(options, { overlappingSync: false, remapChunks: false, safeRestore: false });
  117. }
  118. if (options.compression)
  119. options.compression = makeCompression(options.compression);
  120. let flags =
  121. (options.overlappingSync ? 0x1000 : 0) |
  122. (options.noSubdir ? 0x4000 : 0) |
  123. (options.noSync ? 0x10000 : 0) |
  124. (options.readOnly ? 0x20000 : 0) |
  125. (options.noMetaSync ? 0x40000 : 0) |
  126. (options.useWritemap ? 0x80000 : 0) |
  127. (options.mapAsync ? 0x100000 : 0) |
  128. (options.noReadAhead ? 0x800000 : 0) |
  129. (options.noMemInit ? 0x1000000 : 0) |
  130. (options.usePreviousSnapshot ? 0x2000000 : 0) |
  131. (options.remapChunks ? 0x4000000 : 0) |
  132. (options.safeRestore ? 0x800 : 0) |
  133. (options.trackMetrics ? 0x400 : 0);
  134. let env = new Env();
  135. let jsFlags = (options.overlappingSync ? 0x1000 : 0) |
  136. (options.separateFlushed ? 1 : 0) |
  137. (options.deleteOnClose ? 2 : 0);
  138. let rc = env.open(options, flags, jsFlags);
  139. env.path = path;
  140. if (rc)
  141. lmdbError(rc);
  142. delete options.keyBytes // no longer needed, don't copy to stores
  143. let maxKeySize = env.getMaxKeySize();
  144. maxKeySize = Math.min(maxKeySize, options.pageSize ? MAX_KEY_SIZE : DEFAULT_MAX_KEY_SIZE);
  145. flags = getEnvFlags(env.address); // re-retrieve them, they are not necessarily the same if we are connecting to an existing env
  146. if (flags & 0x1000) {
  147. if (userOptions.noSync) {
  148. env.close();
  149. throw new Error('Can not set noSync on a database that was opened with overlappingSync');
  150. }
  151. } else if (options.overlappingSync) {
  152. if (userOptions.overlappingSync) {
  153. env.close();
  154. throw new Error('Can not enable overlappingSync on a database that was opened without this flag');
  155. }
  156. options.overlappingSync = false;
  157. jsFlags = jsFlags & 0xff; // clear overlapping sync
  158. setJSFlags(env.address, jsFlags);
  159. }
  160. env.readerCheck(); // clear out any stale entries
  161. if ((options.overlappingSync || options.deleteOnClose) && !hasRegisteredOnExit && process.on) {
  162. hasRegisteredOnExit = true;
  163. process.on('exit', onExit);
  164. }
  165. class LMDBStore extends EventEmitter {
  166. constructor(dbName, dbOptions) {
  167. super();
  168. if (dbName === undefined)
  169. throw new Error('Database name must be supplied in name property (may be null for root database)');
  170. if (options.compression && dbOptions.compression !== false && typeof dbOptions.compression != 'object')
  171. dbOptions.compression = options.compression; // use the parent compression if available
  172. else if (dbOptions.compression)
  173. dbOptions.compression = makeCompression(dbOptions.compression);
  174. if (dbOptions.dupSort && (dbOptions.useVersions || dbOptions.cache)) {
  175. throw new Error('The dupSort flag can not be combined with versions or caching');
  176. }
  177. let keyIsBuffer = dbOptions.keyIsBuffer
  178. if (dbOptions.keyEncoding == 'uint32') {
  179. dbOptions.keyIsUint32 = true;
  180. } else if (dbOptions.keyEncoder) {
  181. if (dbOptions.keyEncoder.enableNullTermination) {
  182. dbOptions.keyEncoder.enableNullTermination()
  183. } else
  184. keyIsBuffer = true;
  185. } else if (dbOptions.keyEncoding == 'binary') {
  186. keyIsBuffer = true;
  187. }
  188. let flags = (dbOptions.reverseKey ? 0x02 : 0) |
  189. (dbOptions.dupSort ? 0x04 : 0) |
  190. (dbOptions.dupFixed ? 0x10 : 0) |
  191. (dbOptions.integerDup ? 0x20 : 0) |
  192. (dbOptions.reverseDup ? 0x40 : 0) |
  193. (!options.readOnly && dbOptions.create !== false ? 0x40000 : 0) |
  194. (dbOptions.useVersions ? 0x100 : 0);
  195. let keyType = (dbOptions.keyIsUint32 || dbOptions.keyEncoding == 'uint32') ? 2 : keyIsBuffer ? 3 : 0;
  196. if (keyType == 2)
  197. flags |= 0x08; // integer key
  198. if (options.readOnly) {
  199. // in read-only mode we use a read-only txn to open the database
  200. // TODO: LMDB is actually not entirely thread-safe when it comes to opening databases with
  201. // read-only transactions since there is a race condition on setting the update dbis that
  202. // occurs outside the lock
  203. // make sure we are using a fresh read txn, so we don't want to share with a cursor txn
  204. this.resetReadTxn();
  205. this.ensureReadTxn();
  206. this.db = new Dbi(env, flags, dbName, keyType, dbOptions.compression);
  207. } else {
  208. this.transactionSync(() => {
  209. this.db = new Dbi(env, flags, dbName, keyType, dbOptions.compression);
  210. }, options.overlappingSync ? 0x10002 : 2); // no flush-sync, but synchronously commit
  211. }
  212. this._commitReadTxn(); // current read transaction becomes invalid after opening another db
  213. if (!this.db || this.db.dbi == 0xffffffff) {// not found
  214. throw new Error('Database not found')
  215. }
  216. this.dbAddress = this.db.address
  217. this.db.name = dbName || null;
  218. this.name = dbName;
  219. this.status = 'open';
  220. this.env = env;
  221. this.reads = 0;
  222. this.writes = 0;
  223. this.transactions = 0;
  224. this.averageTransactionTime = 5;
  225. if (dbOptions.syncBatchThreshold)
  226. console.warn('syncBatchThreshold is no longer supported');
  227. if (dbOptions.immediateBatchThreshold)
  228. console.warn('immediateBatchThreshold is no longer supported');
  229. this.commitDelay = DEFAULT_COMMIT_DELAY;
  230. Object.assign(this, { // these are the options that are inherited
  231. path: options.path,
  232. encoding: options.encoding,
  233. strictAsyncOrder: options.strictAsyncOrder,
  234. }, dbOptions);
  235. let Encoder;
  236. if (this.encoder && this.encoder.Encoder) {
  237. Encoder = this.encoder.Encoder;
  238. this.encoder = null; // don't copy everything from the module
  239. }
  240. if (!Encoder && !(this.encoder && this.encoder.encode) && (!this.encoding || this.encoding == 'msgpack' || this.encoding == 'cbor')) {
  241. Encoder = (this.encoding == 'cbor' ? moduleRequire('cbor-x').Encoder : MsgpackrEncoder);
  242. }
  243. if (Encoder) {
  244. this.encoder = new Encoder(Object.assign(
  245. assignConstrainedProperties(['copyBuffers', 'getStructures', 'saveStructures', 'useFloat32', 'useRecords', 'structuredClone', 'variableMapSize', 'useTimestamp32', 'largeBigIntToFloat', 'encodeUndefinedAsNil', 'int64AsNumber', 'onInvalidDate', 'mapsAsObjects', 'useTag259ForMaps', 'pack', 'maxSharedStructures', 'shouldShareStructure', 'randomAccessStructure', 'freezeData'],
  246. this.sharedStructuresKey !== undefined ? this.setupSharedStructures() : {
  247. copyBuffers: true, // need to copy any embedded buffers that are found since we use unsafe buffers
  248. }, options, dbOptions), this.encoder));
  249. }
  250. if (this.encoding == 'json') {
  251. this.encoder = {
  252. encode: JSON.stringify,
  253. };
  254. } else if (this.encoder) {
  255. this.decoder = this.encoder;
  256. this.decoderCopies = !this.encoder.needsStableBuffer
  257. }
  258. this.maxKeySize = maxKeySize;
  259. applyKeyHandling(this);
  260. allDbs.set(dbName ? name + '-' + dbName : name, this);
  261. }
  262. openDB(dbName, dbOptions) {
  263. if (this.dupSort && this.name == null)
  264. throw new Error('Can not open named databases if the main database is dupSort')
  265. if (typeof dbName == 'object' && !dbOptions) {
  266. dbOptions = dbName;
  267. dbName = dbOptions.name;
  268. } else
  269. dbOptions = dbOptions || {};
  270. try {
  271. return dbOptions.cache ?
  272. new (CachingStore(LMDBStore, env))(dbName, dbOptions) :
  273. new LMDBStore(dbName, dbOptions);
  274. } catch(error) {
  275. if (error.message == 'Database not found')
  276. return; // return undefined to indicate db not found
  277. if (error.message.indexOf('MDB_DBS_FULL') > -1) {
  278. error.message += ' (increase your maxDbs option)';
  279. }
  280. throw error;
  281. }
  282. }
  283. open(dbOptions, callback) {
  284. let db = this.openDB(dbOptions);
  285. if (callback)
  286. callback(null, db);
  287. return db;
  288. }
  289. backup(path, compact) {
  290. if (noFSAccess)
  291. return;
  292. fs.mkdirSync(pathModule.dirname(path), { recursive: true });
  293. return new Promise((resolve, reject) => env.copy(path, compact, (error) => {
  294. if (error) {
  295. reject(error);
  296. } else {
  297. resolve();
  298. }
  299. }));
  300. }
  301. isOperational() {
  302. return this.status == 'open';
  303. }
  304. sync(callback) {
  305. return env.sync(callback || function(error) {
  306. if (error) {
  307. console.error(error);
  308. }
  309. });
  310. }
  311. deleteDB() {
  312. console.warn('deleteDB() is deprecated, use drop or dropSync instead');
  313. return this.dropSync();
  314. }
  315. dropSync() {
  316. this.transactionSync(() =>
  317. this.db.drop({
  318. justFreePages: false
  319. }), options.overlappingSync ? 0x10002 : 2);
  320. }
  321. clear(callback) {
  322. if (typeof callback == 'function')
  323. return this.clearAsync(callback);
  324. console.warn('clear() is deprecated, use clearAsync or clearSync instead');
  325. this.clearSync();
  326. }
  327. clearSync() {
  328. if (this.encoder) {
  329. if (this.encoder.clearSharedData)
  330. this.encoder.clearSharedData()
  331. else if (this.encoder.structures)
  332. this.encoder.structures = []
  333. }
  334. this.transactionSync(() =>
  335. this.db.drop({
  336. justFreePages: true
  337. }), options.overlappingSync ? 0x10002 : 2);
  338. }
  339. readerCheck() {
  340. return env.readerCheck();
  341. }
  342. readerList() {
  343. return env.readerList().join('');
  344. }
  345. setupSharedStructures() {
  346. const getStructures = () => {
  347. let lastVersion; // because we are doing a read here, we may need to save and restore the lastVersion from the last read
  348. if (this.useVersions)
  349. lastVersion = getLastVersion();
  350. let buffer = this.getBinary(this.sharedStructuresKey);
  351. if (this.useVersions)
  352. setLastVersion(lastVersion);
  353. return buffer && this.decoder.decode(buffer);
  354. };
  355. return {
  356. saveStructures: (structures, isCompatible) => {
  357. return this.transactionSync(() => {
  358. let existingStructuresBuffer = this.getBinary(this.sharedStructuresKey);
  359. let existingStructures = existingStructuresBuffer && this.decoder.decode(existingStructuresBuffer);
  360. if (typeof isCompatible == 'function' ?
  361. !isCompatible(existingStructures) :
  362. (existingStructures && existingStructures.length != isCompatible))
  363. return false; // it changed, we need to indicate that we couldn't update
  364. this.put(this.sharedStructuresKey, structures);
  365. }, options.overlappingSync ? 0x10000 : 0);
  366. },
  367. getStructures,
  368. copyBuffers: true, // need to copy any embedded buffers that are found since we use unsafe buffers
  369. };
  370. }
  371. }
  372. // if caching class overrides putSync, don't want to double call the caching code
  373. const putSync = LMDBStore.prototype.putSync;
  374. const removeSync = LMDBStore.prototype.removeSync;
  375. addReadMethods(LMDBStore, { env, maxKeySize, keyBytes, keyBytesView, getLastVersion });
  376. if (!options.readOnly)
  377. addWriteMethods(LMDBStore, { env, maxKeySize, fixedBuffer: keyBytes,
  378. resetReadTxn: LMDBStore.prototype.resetReadTxn, ...options });
  379. LMDBStore.prototype.supports = {
  380. permanence: true,
  381. bufferKeys: true,
  382. promises: true,
  383. snapshots: true,
  384. clear: true,
  385. status: true,
  386. deferredOpen: true,
  387. openCallback: true,
  388. };
  389. let Class = options.cache ? CachingStore(LMDBStore, env) : LMDBStore;
  390. return options.asClass ? Class : new Class(options.name || null, options);
  391. }
  392. export function openAsClass(path, options) {
  393. if (typeof path == 'object' && !options) {
  394. options = path;
  395. path = options.path;
  396. }
  397. options = options || {};
  398. options.asClass = true;
  399. return open(path, options);
  400. }
  401. export function getLastVersion() {
  402. return keyBytesView.getFloat64(16, true);
  403. }
  404. export function setLastVersion(version) {
  405. return keyBytesView.setFloat64(16, version, true);
  406. }
  407. export function getLastTxnId() {
  408. return keyBytesView.getUint32(32, true);
  409. }
  410. const KEY_BUFFER_SIZE = 4096;
  411. function allocateFixedBuffer() {
  412. keyBytes = typeof Buffer != 'undefined' ? Buffer.allocUnsafeSlow(KEY_BUFFER_SIZE) : new Uint8Array(KEY_BUFFER_SIZE);
  413. const keyBuffer = keyBytes.buffer;
  414. keyBytesView = keyBytes.dataView || (keyBytes.dataView = new DataView(keyBytes.buffer, 0, KEY_BUFFER_SIZE)); // max key size is actually 4026
  415. keyBytes.uint32 = new Uint32Array(keyBuffer, 0, KEY_BUFFER_SIZE >> 2);
  416. keyBytes.float64 = new Float64Array(keyBuffer, 0, KEY_BUFFER_SIZE >> 3);
  417. keyBytes.uint32.address = keyBytes.address = keyBuffer.address = getAddress(keyBuffer);
  418. }
  419. function exists(path) {
  420. if (fs.existsSync)
  421. return fs.existsSync(path);
  422. try {
  423. return fs.statSync(path);
  424. } catch (error) {
  425. return false
  426. }
  427. }
  428. function assignConstrainedProperties(allowedProperties, target) {
  429. for (let i = 2; i < arguments.length; i++) {
  430. let source = arguments[i];
  431. for (let key in source) {
  432. if (allowedProperties.includes(key))
  433. target[key] = source[key];
  434. }
  435. }
  436. return target;
  437. }