lmdb-js

repository·master·Indexed 20 days ago

https://github.com/kriszyp/lmdb-js

An ultra-fast, ACID-compliant key-value database interface for Node.js, Bun, and Deno. It provides high-performance serialization of JavaScript objects and arrays into LMDB's binary storage, featuring synchronous reads, asynchronous writes, and optional off-main-thread LZ4 compression.

Tokens
10.3K
Snippets
41
Records
55
Agent score
70%

What's inside lmdb-js

  1. Understand Overlapping Sync and Durability

    master

    When overlappingSync: true is enabled (default on non-Windows), LMDB allows disk flushing to occur in parallel with future transactions. This improves performance by not holding the writer lock during the slow flush process.

    Managing Durability: There is a distinction between a transaction being committed (visible to other threads) and being flushed (durable on disk).

    1. Committed: The promise returned by put or remove resolves when the transaction is committed and visible.
    2. Flushed: The db.flushed property (or the .flushed property on a promise if separateFlushed: true is set) resolves when the OS reports the data is truly durable on disk.

    Note: overlappingSync is generally not recommended on Windows due to poor performance characteristics with large databases.

    let db = open('my-db', { overlappingSync: true });
    
    let written = db.put(key, value);
    await written; // Wait for it to be committed (visible)
    
    // Access the value immediately after commit
    let v = db.get(key);
    
    await db.flushed; // Wait for the last commit to be fully flushed to disk (durable)
  2. Implement custom key encoding

    master

    To define more efficient encodings for specific key types (like UUIDs), provide a keyEncoder object. This object must implement two methods:

    1. writeKey(key, targetBuffer, startPosition): Writes the key to the targetBuffer starting at startPosition and returns the new end position in the buffer.
    2. readKey(sourceBuffer, start, end): Reads the key from the sourceBuffer between the start and end positions and returns the decoded key.
  3. Enable and use caching in lmdb-js

    master

    lmdb-js supports an optimized LRFU (Least Recently/Frequently Used) and weak-referencing caching mechanism. Enabling caching can improve performance for large objects with high deserialization costs and provides immediate synchronous access to data after a put operation.

    Key Benefits:

    • Performance: Faster get operations for frequently accessed or large objects.
    • Object Identity: As long as an object is in memory, get will return the exact same object instance (identity correlation).
    • Immediate Access: You can get a value immediately after calling put without awaiting the put promise.

    Requirements:

    • Node.js 14.10+ (or Node v13.0 with --harmony-weak-ref).
    • Caching does not apply to getRange queries.

    Multi-worker/Process Note: Since the cache is stored separately for each process, use the validated: true flag to ensure in-memory objects match the stored data across workers.

    // Enable caching with validation for multi-worker environments
    let db = open({
    	cache: {
    		validated: true,
    	},
    });
    
    // Immediate access pattern
    db.put('hi', 'there');
    db.get('hi'); // returns 'there' immediately without awaiting the put
  4. Use shared structures for efficient object storage

    master

    Shared structures optimize storage and retrieval speed for databases containing many objects with similar property sets (e.g., arrays of similar objects). When enabled, the library automatically extracts and stores structural metadata in a dedicated entry.

    Requirements & Usage:

    • This feature is only available when using the default MessagePack or CBOR encoding (via msgpackr or cbor-x).
    • To enable, specify a sharedStructuresKey in the open() options. It is recommended to use a Symbol to avoid collisions with standard JS primitive values.
    • You do not need to interact with the metadata key directly; the library manages it automatically.
    let myDB = open('my-db', {
    	sharedStructuresKey: Symbol.for('structures'),
    });
  5. Achieve atomicity with versioning and conditional writes

    master

    To handle high concurrency across multiple processes, use versioning to perform conditional updates. This ensures a data update only occurs if the entry's current version matches your expected version, preventing race conditions.

    1. Enable versioning: Set useVersions: true when calling open().
    2. Set a version: Use the version argument in put(key, value, version).
    3. Conditional update: Use the ifVersion argument in put(key, value, version, ifVersion) or use the ifVersion(key, expectedVersion, callback) method to wrap multiple operations in a conditional block.
    4. Retrieve version: Use getLastVersion() to get the current version of an entry.
    // 1. Enable versioning
    let myDB = open('my-db', { useVersions: true });
    
    // 2. Conditional write: new version 4, only if previous version was 3
    myDB.put('key1', 'value1', 4, 3);
    
    // 3. Conditional block for multiple operations
    myDB.ifVersion('key1', 4, () => {
    	myDB.put('key1', 'value2', 5); // equivalent to myDB.put('key1', 'value2', 5, 4);
    	myDB.put('anotherKey', 'value', 3);
    	myDB2.put('keyInOtherDb', 'value');
    });
  6. How lmdb-js works: Design and Performance

    master

    Core Architecture

    lmdb-js is designed for synchronous reads and asynchronous writes.

    • Synchronous Reads: Because LMDB is a memory-mapped database, reading within a transaction typically does not use I/O (except for potential page faults). This allows for instant synchronous access to values.
    • Asynchronous Writes: Committing transactions involves I/O. To maximize throughput, lmdb-js queues asynchronous off-thread write operations. Transactions return a Promise that resolves once data is written and flushed to disk.

    Key Features

    • Data Translation: Uses optimized native C++ code to translate JS values (primitives, arrays, objects) to/from binary data.
    • Crash-Proof Design: Uses default syncing configurations to ensure data integrity even during power loss.
    • Optimistic Locking: Supports conditional writes, allowing for atomic operations that depend on previously read data. This enables scaling across multiple processes or threads.
    • Compression: Offers optional off-main-thread LZ4 compression, which performs compression on the same thread used for asynchronous writes to minimize main-thread impact.
    • Automatic Growth: Automatically manages database file expansion using heuristics to minimize fragmentation.
  7. Transition from LevelUp to lmdb-js

    master

    lmdb-js supports most of the LevelUp API, including put, del, batch, status, isOperation, and getMany.

    While get is synchronous in lmdb-js for performance, you can use the levelup export to wrap a database instance and provide a LevelUp-style API (supporting both callbacks and Promises) for get calls.

    let dbLevel = levelup(db)
    dbLevel.get(id, (error, value) => {
      // callback style
    })
    
    // or
    dbLevel.get(id).then(...)
  8. Compile LZ4 with gcc/MinGW (Dynamic Linking)

    master

    To use the dynamic LZ4 library with gcc/MinGW, you need the header files from the include\ directory and the dynamic library dll\msys-lz4-1.dll.

    You must include the library in your linking options. The resulting executable will require dll\msys-lz4-1.dll to run.

    gcc $(CFLAGS) -Iinclude\ test-dll.c -o test-dll dll\msys-lz4-1.dll
  9. Build lmdb from source with specific options

    master

    You can pass flags to npm install to configure the LMDB build:

    • Disable Robust Mutexes: Use --use_robust=false. This reduces performance overhead but means if a process dies during a transaction, the OS might not clean up semaphores, potentially hanging other processes. Recommended only if not using multiple processes.
    • Use Legacy Data Format: Use --use_data_v1=true to build with the older LMDB data format version 1. This is useful for portability with older libraries, though it lacks features like encryption and remapping.
    # Disable robust mutexes
    npm install lmdb --build-from-source --use_robust=false
    
    # Use legacy data format v1
    npm install lmdb --build-from-source --use_data_v1=true
  10. Compile LZ4 with Visual C++ (Dynamic Linking)

    master

    To use the LZ4 DLL with Visual C++, you need the header files from include\ and the import library dll\liblz4.dll.a. Follow these steps in your project properties:

    1. Include Directories: Add the header files to Additional Include Directories (found under C/C++ -> General).
    2. Library Dependencies: Add the import library to Additional Dependencies (found under Linker -> Input).
      • Note: If you only provide the name liblz4.dll.a without a full path, you must also add the library directory to Linker\General\Additional Library Directories.

    Runtime Requirement: The compiled executable requires dll\msys-lz4-1.dll to be available.

  11. Install lmdb

    master

    Install the lmdb package via npm. This library is a high-performance interface to LMDB for Node.js, Bun, and Deno, designed for storing structured JS data (objects, arrays, etc.) in an ACID-compliant database.

    For maximum performance on older Node.js versions, you can install from source:

    npm install --build-from-source
    npm install lmdb
  12. Create LZ4 DLL using MinGW+MSYS on Windows

    master

    You can generate a dynamic library (liblz4.dll) and an import library (liblz4.lib) using MinGW+MSYS with the make liblz4 command.

    If cross-compiling on Linux for Windows (64-bit), you can set the DLLTOOL variable. To link a project using the generated DLL with gcc/MinGW, include the DLL in your linking options.

    # Cross-compilation example on Linux
    make BUILD_STATIC=no CC=x86_64-w64-mingw32-gcc DLLTOOL=x86_64-w64-mingw32-dlltool OS=Windows_NT
    
    # Linking a test file with the DLL
    $(CC) $(CFLAGS) -Iinclude/ test-dll.c -o test-dll dll\liblz4.dll