@kyneta/leveldb-store
v5.0.0
Published
LevelDB storage backend for @kyneta/exchange — server-side persistent storage
Readme
@kyneta/leveldb-store
Server-side persistent storage for @kyneta/exchange, backed by LevelDB via classic-level.
Implements the Store interface — pass directly to Exchange({ store: ... }) for automatic document persistence and hydration.
Install
pnpm add @kyneta/leveldb-storeQuick Start
import { Exchange } from "@kyneta/exchange"
import { createLevelDBStore } from "@kyneta/leveldb-store"
const exchange = new Exchange({
principal: "my-server",
type: "service",
store: await createLevelDBStore("./data/exchange-db"),
transports: [networkTransport],
})
// Documents are automatically persisted on mutation and hydrated on restart.
const doc = exchange.get("my-doc", TodoDoc)That's it. The Exchange handles hydration (loading from storage on get() / replicate()) and persistence (saving incremental deltas via onStateAdvanced) — no manual save/load needed.
API
createLevelDBStore(dbPath)
Async factory that opens the database, checks its format (see below), takes its seat, and resolves to a Store. The dbPath is the directory where LevelDB stores its files. await it before passing to the Exchange. A second open of the directory while one holds it throws.
import { createLevelDBStore } from "@kyneta/leveldb-store"
const store = await createLevelDBStore("./data/exchange-db")LevelDBStore
The class implementing the Store interface. Open it with createLevelDBStore or LevelDBStore.open(dbPath): a store is born holding its seat, so its constructor is private.
import { LevelDBStore } from "@kyneta/leveldb-store"
const store = await LevelDBStore.open("./data/exchange-db")
// ... use with Exchange ...
await store.close() // release the directory, and with it the seatThe peer's identity
The store issues the exchange's peerId: one seat, kept in the database and reused on every open, so a restarted process is the same peer. classic-level locks the directory, so one open store owns it and nothing else can write under the seat. For the same reason a json document has one writer here by construction, and the store records none.
Store-format check
On open, the store stamps a { major, minor } format version into a store-meta\x00 key namespace (separate from the per-doc doc-meta\x00 keys), and on subsequent opens refuses — with StoreFormatVersionError — a store whose stamped major is incompatible with the running build, or an unversioned store that already holds documents. No automatic migration is performed. The format is 1.1: store-meta\x00seats holds the seat. A 1.0 store opens as one with no seat yet, and is given one.
Binary Codec
The module also exports encodeStoreRecord and decodeStoreRecord for the compact binary envelope format. These are pure functions — useful for debugging, migration scripts, or building custom tooling over the LevelDB data files.
import { encodeStoreRecord, decodeStoreRecord } from "@kyneta/leveldb-store"
const bytes = encodeStoreRecord(record) // StoreRecord → Uint8Array
const record = decodeStoreRecord(bytes) // Uint8Array → StoreRecordDesign
Key-Space
Keys follow the FoundationDB convention — null-byte (\x00) separated prefixes:
| Key pattern | Value |
|---|---|
| meta\x00{docId} | JSON-encoded DocMetadata |
| entry\x00{docId}\x00{seqNo} | Binary-encoded StoreRecord |
The \x00 separator cannot appear in valid UTF-8 strings, so no docId validation is needed — the key-space imposes zero constraints on callers. Documents with overlapping name prefixes (e.g. doc vs doc-extra) are fully isolated.
Sequence Numbers
Each document maintains a monotonic sequence counter for entry ordering. SeqNos are zero-padded to 10 digits, supporting up to 10 billion entries per document.
On reboot, the max seqNo for a document is lazily discovered via a single reverse-iterator seek on first append — no full scan needed.
Binary Envelope
Entries are stored in a compact binary format (not JSON) for minimal overhead:
[1 byte flags] [4 bytes version length BE] [N bytes version UTF-8] [remaining: payload data]Flags byte layout:
- bit 0: kind (0 = entirety, 1 = since)
- bit 1: encoding (0 = json, 1 = binary)
- bit 2: data type (0 = string, 1 = Uint8Array)
Atomicity
Every write commits through a single LevelDB batch. append() of a metadata record commits the materialized index update and the record together, so a crash can never leave the doc-meta index advanced past its backing record (an entry-record append is a single record write). compact() atomically deletes the entries at or before a mark and writes the new ones after the rest. A concurrent reader never observes a partial intermediate state.
Testing
The package passes the full Store conformance suite from @kyneta/exchange/testing, plus LevelDB-specific tests for close/reopen persistence and binary codec edge cases.
pnpm testTo use the conformance suite for your own Store implementation:
import { describeStore } from "@kyneta/exchange/testing"
describeStore(
"MyStore",
() => new MyStore(),
async (store) => { /* cleanup */ },
)Peer Dependencies
{
"peerDependencies": {
"@kyneta/exchange": "^1.1.0",
"@kyneta/schema": "^1.1.0"
}
}License
MIT
