xitdb Immutable Database
xitdb (current version 0.13.0) is an embedded, immutable database library for TypeScript and JavaScript, designed for storing and managing structured data in a versioned manner. It excels by efficiently creating a new "copy" of the database with each transaction, allowing past states to be read from or reverted to. Unlike traditional databases, xitdb operates without a query engine, instead providing direct APIs to manipulate core data structures like ArrayList and HashMap, which can be nested arbitrarily. It supports both single-file and in-memory storage, incrementally reading and writing to handle datasets larger than available memory. A key differentiator is its entirely synchronous API, eliminating the need for `async/await`, and its pure TypeScript implementation with no external dependencies beyond the JavaScript standard library. Reads never block writes, and multiple threads/processes can access the database concurrently without locks. This makes it a powerful alternative to SQL databases like SQLite for applications requiring simplicity, immutability, and tight integration with native TypeScript data structures, especially in scenarios akin to version control systems. The project is under active development, with an irregular, feature-driven release cadence, reflecting its 0.x version status.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'put')
cause Attempting to call a write method (like `put` or `append`) on a `ReadHashMap` or `ReadArrayList` instance, or on a cursor obtained outside of a transaction context.fixEnsure you are within a `history.appendContext` callback and are using `WriteHashMap` or `WriteArrayList` instances created from the provided `cursor`. -
Error: File already open
cause Trying to instantiate `CoreBufferedFile` with the same file path multiple times concurrently or without properly disposing of previous instances.fixEnsure `CoreBufferedFile` instances are properly closed or disposed of when no longer needed. If using `using` declarations, ensure they are within the correct scope to manage resources. Avoid opening the same file multiple times in parallel within a single process unless explicitly designed for it. -
TypeError: history.appendContext is not a function
cause Trying to call `appendContext` on an object that is not a `WriteArrayList` (or `WriteHashMap` in its capacity to append to a list within).fixVerify that `history` is indeed an instance of `WriteArrayList` initialized from the database's root cursor, as this is the standard entry point for transactions. -
ReferenceError: Hasher is not defined
cause The `Hasher` class was used without being imported.fixAdd `import { Hasher } from 'xitdb';` to the top of your TypeScript/JavaScript file.
Warnings
- breaking As xitdb is currently in version 0.x.y, the API is not yet stable, and breaking changes may occur between minor or even patch releases. Review changelogs carefully when upgrading.
- gotcha xitdb does not include a query engine. Data access and manipulation are performed directly through JavaScript/TypeScript data structures (ArrayList, HashMap). Developers accustomed to SQL or NoSQL query languages will need to adapt.
- gotcha The database is immutable; transactions always create new versions. Attempting to modify data in-place on a read cursor will not work. Always operate on `WriteArrayList` or `WriteHashMap` instances obtained within a transaction's context.
- gotcha The API is entirely synchronous, which can be unexpected for I/O-heavy operations in modern JavaScript/TypeScript development. This design decision simplifies usage but requires careful consideration in environments where blocking the event loop is problematic.
- gotcha The quickstart example uses 'SHA-1' for hashing. While suitable for internal database integrity, SHA-1 is cryptographically broken and should not be used for security-sensitive applications like password hashing or digital signatures.
Install
-
npm install xitdb -
yarn add xitdb -
pnpm add xitdb
Imports
- Database
const Database = require('xitdb').Databaseimport { Database } from 'xitdb' - CoreBufferedFile
import CoreBufferedFile from 'xitdb'
import { CoreBufferedFile } from 'xitdb' - WriteArrayList
import { ArrayList } from 'xitdb'import { WriteArrayList } from 'xitdb' - Hasher
const Hasher = require('xitdb')import { Hasher } from 'xitdb'
Quickstart
import { CoreBufferedFile, Database, Hasher, WriteArrayList, WriteHashMap, Bytes, Uint } from 'xitdb';
import * as path from 'path';
import * as fs from 'fs';
const dbFilePath = path.join(process.cwd(), 'main.db');
// Ensure the database file doesn't exist from a previous run
if (fs.existsSync(dbFilePath)) {
fs.unlinkSync(dbFilePath);
}
// init the db
using core = new CoreBufferedFile(dbFilePath);
const hasher = new Hasher('SHA-1');
const db = new Database(core, hasher);
// to get the benefits of immutability, the top-level data structure
// must be an ArrayList, so each transaction is stored as an item in it
const history = new WriteArrayList(db.rootCursor());
// Execute a transaction to add data
history.appendContext(history.getSlot(-1), (cursor) => {
const moment = new WriteHashMap(cursor);
moment.put('foo', new Bytes('foo'));
moment.put('bar', new Bytes('bar'));
const fruitsCursor = moment.putCursor('fruits');
const fruits = new WriteArrayList(fruitsCursor);
fruits.append(new Bytes('apple'));
fruits.append(new Bytes('pear'));
fruits.append(new Bytes('grape'));
const peopleCursor = moment.putCursor('people');
const people = new WriteArrayList(peopleCursor);
const aliceCursor = people.appendCursor();
const alice = new WriteHashMap(aliceCursor);
alice.put('name', new Bytes('Alice'));
alice.put('age', new Uint(25));
const bobCursor = people.appendCursor();
const bob = new WriteHashMap(bobCursor);
bob.put('name', new Bytes('Bob'));
bob.put('age', new Uint(42));
});
// Read the most recent state of the database
const latestMomentCursor = history.getSlot(-1);
if (latestMomentCursor) {
const latestMoment = new WriteHashMap(latestMomentCursor);
console.log('Database state after transaction:');
// You would typically iterate or access specific keys here
// For demonstration, let's just confirm an item exists
console.log('Has "foo" key:', latestMoment.get('foo')?.toString());
console.log('Has "fruits" key:', latestMoment.get('fruits') !== undefined);
} else {
console.log('No data found in the database history.');
}
// Clean up the database file (optional, for repeated runs)
// fs.unlinkSync(dbFilePath);