@vijayee/wavedb
v0.10.2
Published
Node.js bindings for WaveDB - Hierarchical B+Trie Database
Maintainers
Readme
WaveDB Node.js Bindings
Node.js bindings for WaveDB — a hierarchical key-value database with MVCC, WAL durability, and schema layer access.
Installation
npm install wavedbQuick Start
const { WaveDB } = require('wavedb');
const db = new WaveDB('/path/to/db', {
delimiter: '/',
lruMemoryMb: 50,
wal: { syncMode: 'debounced' }
});
// Async (Promise)
await db.put('users/alice/name', 'Alice');
const name = await db.get('users/alice/name');
// Sync (blocking)
db.putSync('users/bob/name', 'Bob');
const name2 = db.getSync('users/bob/name');
// Callback style
db.putCb('users/charlie/name', 'Charlie', (err) => { /* ... */ });
db.getCb('users/charlie/name', (err, val) => { /* ... */ });
// Object operations (nested data → flattened paths)
await db.putObject('users/alice', { name: 'Alice', age: 30 });
const user = await db.getObject('users/alice'); // { name: 'Alice', age: 30 }
// Batch
await db.batch([
{ type: 'put', key: 'users/alice/name', value: 'Alice' },
{ type: 'del', key: 'users/bob/name' }
]);
// Iterator
const iter = new Iterator(db, { start: 'users/alice', end: 'users/alice~' });
let entry;
while ((entry = iter.read())) {
console.log(entry.key, entry.value);
}
iter.end();
// Stream
db.createReadStream({ start: 'users/', end: 'users/~' })
.on('data', ({ key, value }) => console.log(key, '=', value))
.on('end', () => console.log('Done'));
db.close();Configuration
const db = new WaveDB(path, options);| Option | Default | Description |
|--------|---------|-------------|
| delimiter | '/' | Key path delimiter |
| chunkSize | 4 | HBTrie chunk size (immutable, set at creation) |
| btreeNodeSize | 4096 | B+tree node size (immutable) |
| enablePersist | true | Persist data to disk (immutable) |
| lruMemoryMb | 50 | LRU cache size in MB |
| lruShards | 64 | LRU shard count (0 = auto-scale) |
| workerThreads | 4 | Worker pool size |
| wal.syncMode | 'debounced' | 'immediate', 'debounced', or 'async' |
| wal.debounceMs | 250 | fsync debounce window (ms) |
| wal.maxFileSize | 131072 | Max WAL file size before sealing |
| encryption | — | Encryption configuration (see below) |
WAL Sync Modes
| Mode | Behavior | Durability | Throughput |
|------|----------|------------|------------|
| 'immediate' | fsync after every write | Highest | ~1K ops/sec |
| 'debounced' | batched fsync (default 250ms) | High | ~300K ops/sec |
| 'async' | buffered write, idle drain every 250ms | Process crash only | ~400K ops/sec |
Encryption
WaveDB supports AES-256-GCM encryption at rest for WAL files and persisted pages. Encryption is set at database creation and cannot be changed on an existing database. Key material is never persisted — only a salt and verification check are stored.
| Mode | Key | Use case |
|------|-----|----------|
| 'symmetric' | 32-byte Buffer | Simple setups, embedded devices |
| 'asymmetric' | DER-encoded RSA key pair | Key rotation, write-only nodes |
Symmetric Encryption
const { WaveDB, EncryptionError } = require('wavedb');
const key = Buffer.alloc(32); // your 32-byte AES-256 key
// ... fill key with crypto.randomBytes(32) or a derived key ...
const db = new WaveDB('/path/to/db', {
enablePersist: true,
encryption: {
type: 'symmetric',
key: key // required, exactly 32 bytes
}
});
// All operations work transparently — encryption is internal
await db.put('users/alice/name', 'Alice');
const name = await db.get('users/alice/name');
// Reopen with the same key
const db2 = new WaveDB('/path/to/db', {
enablePersist: true,
encryption: { type: 'symmetric', key }
});Asymmetric Encryption
const { WaveDB } = require('wavedb');
const fs = require('fs');
const publicKey = fs.readFileSync('public_key.der');
const privateKey = fs.readFileSync('private_key.der');
const db = new WaveDB('/path/to/db', {
enablePersist: true,
encryption: {
type: 'asymmetric',
publicKey: publicKey, // required, DER-encoded
privateKey: privateKey // optional — omit for write-only mode
}
});In asymmetric mode, a random data encryption key (DEK) is generated per session and wrapped with the RSA public key. The private key is required for decryption; a write-only node can omit privateKey.
Encryption Errors
| Condition | Error | Message |
|-----------|-------|---------|
| Opening encrypted DB without a key | EncryptionError | "Encryption required: this database was created with encryption" |
| Wrong key on re-open | EncryptionError | "Invalid encryption key" |
| Adding encryption to unencrypted DB | EncryptionError | "Encryption unsupported" |
| Symmetric key not 32 bytes | RangeError | "encryption.key must be exactly 32 bytes for AES-256" |
| Missing required key material | TypeError | "encryption.key is required for symmetric encryption..." |
try {
const db = new WaveDB('/path/to/db', { encryption: { type: 'symmetric', key } });
} catch (err) {
if (err instanceof EncryptionError) {
console.error('Encryption failed:', err.message);
}
}API Reference
Async Operations
All async methods return Promises and dispatch to the C worker pool (non-blocking).
await db.put(key, value) // Store a value
const val = await db.get(key) // Retrieve (null if not found)
await db.del(key) // Delete
await db.batch(operations) // Atomic batch of put/del
await db.putObject(key, obj) // Store nested object as flattened paths
const obj = await db.getObject(key) // Reconstruct nested objectCallback Operations
Same as async, but with Node.js-style error-first callbacks. Also returns a Promise.
db.putCb(key, value, (err) => { /* ... */ })
db.getCb(key, (err, value) => { /* ... */ })
db.delCb(key, (err) => { /* ... */ })
db.batchCb(operations, (err) => { /* ... */ })Sync Operations
Block the event loop. Use for initialization/migration scripts, not hot paths.
db.putSync(key, value)
const val = db.getSync(key) // null if not found
db.delSync(key)
db.batchSync(operations)
db.putObjectSync(key, obj)
const obj = db.getObjectSync(key)Keys and Values
// String keys with delimiter
await db.put('users/alice/name', 'Alice');
// Array keys
await db.put(['users', 'bob', 'name'], 'Bob');
// Custom delimiter
const db = new WaveDB('/path/to/db', { delimiter: ':' });
await db.put('users:charlie:name', 'Charlie');
// String or Buffer values
await db.put('key', 'text value');
await db.put('binary/key', Buffer.from([0x01, 0x02]));Streams
db.createReadStream({ start, end, reverse, keys, values, keyAsArray })
.on('data', ({ key, value }) => { /* ... */ })
.on('end', () => { /* ... */ });| Option | Default | Description |
|--------|---------|-------------|
| start | — | Start path (inclusive) |
| end | — | End path (exclusive) |
| reverse | false | Reverse order |
| keys | true | Include keys |
| values | true | Include values |
| keyAsArray | false | Return keys as arrays |
GraphQL Schema Layer
const { GraphQLLayer } = require('wavedb');
const layer = new GraphQLLayer('/path/to/db', {
enablePersist: true,
chunkSize: 4,
workerThreads: 4
});
// Define schema
layer.parseSchema(`
type User {
name: String
age: Int
}
`);
// Query
const result = layer.querySync('{ User { name } }');
// { success: true, data: { User: { name: null } }, errors: [] }
// Mutation
const created = layer.mutateSync('mutation { createUser(name: "Alice") { id name } }');
// { success: true, data: { createUser: { id: "abc123", name: "Alice" } } }
// Async variants
const result = await layer.query('{ User { name } }');
const created = await layer.mutate('mutation { createUser(name: "Bob") { id } }');
layer.close();Introspection
const schema = await layer.query('{ __schema { types { name kind } } }');
const userType = await layer.query('{ __type(name: "User") { name kind fields { name } } }');Required Fields
Fields marked ! in the schema are required for create mutations:
layer.parseSchema('type User { name: String! age: Int }');
// Missing required field — fails
layer.mutateSync('mutation { createUser(age: "30") { id } }');
// { success: false, errors: ['Missing required fields: name'] }
// Providing required field — succeeds
layer.mutateSync('mutation { createUser(name: "Alice") { id name } }');
// { success: true, data: { createUser: { id: '1', name: 'Alice' } } }Error Handling
try {
await db.put('key', 'value');
} catch (err) {
console.error('Database error:', err.message);
}Error codes: NOT_FOUND, INVALID_PATH, IO_ERROR, DATABASE_CLOSED, INVALID_ARGUMENT, ENCRYPTION
Performance
Benchmarks on Linux x86_64, Node.js v24, 50MB LRU cache, 32 worker threads, async WAL.
Node.js Sync (ASYNC WAL, 32 worker threads)
| Operation | Throughput |
|-----------|------------|
| putSync | 1.3K ops/sec |
| getSync | 304K ops/sec |
Node.js Concurrent Throughput (ASYNC WAL, 32 worker threads)
| Concurrency | put | get |
|-------------|-------|-------|
| 1 | 1.2K ops/sec | 45K ops/sec |
| 2 | 1.9K ops/sec | 47K ops/sec |
| 4 | 2.6K ops/sec | 106K ops/sec |
| 8 | 2.6K ops/sec | 102K ops/sec |
| 16 | 2.4K ops/sec | 80K ops/sec |
| 32 | 2.2K ops/sec | 114K ops/sec |
Tips: Use async operations in production. Scale concurrency to 4-8 for best throughput. Sync puts are WAL-bound; sync gets are memory-speed.
Building from Source
# Build C library
cd WaveDB && mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
# Build Node.js addon
cd ../bindings/nodejs
npm install
npm run build
# Run tests
npm testLicense
MIT
