Storage

Snapshots

Copy one committed version out as a portable file, and load it back into any store.

A snapshot is one committed version of a database, copied out as a single byte array and loadable into any block store. It is how you back a database up, seed a test fixture from real data, ship a prepared database as a static asset, or hand a colleague exactly what you were looking at.

From the database

The database copies itself out as the finished file, and loads one back:

const bytes = await db.exportSnapshot();

const restored = new MinnowDatabase(new MemoryBlockStore());
await restored.importSnapshot(bytes);

Both take an onProgress callback, and both work the same way through the worker client — there the file crosses the channel in slices, so the main thread copies a few megabytes at a time instead of stalling on one clone of the whole database.

That is also what the devtools' Download database button does; see the devtools.

Everything below is the layer underneath, for when you want the records rather than the file, or a store that has no database in front of it.

import { encodeSnapshot, decodeSnapshot } from "@minnowdb/core/storage";

const bytes = await encodeSnapshot(await store.exportSnapshot());

Loading

Into a fresh in-memory store:

import { MemoryBlockStore } from "@minnowdb/core/storage";

const db = new MinnowDatabase(MemoryBlockStore.fromSnapshot(await decodeSnapshot(bytes)));

Or into IndexedDB, where it is durable and available on the next visit:

const store = await IndexedDbBlockStore.open({ name: "seeded" });
await store.importSnapshot(await decodeSnapshot(bytes), {
  onProgress: ({ writtenBytes, totalBytes }) => {
    setProgress(writtenBytes / totalBytes);
  },
});

The target store must be empty. Loading into a database that already holds one throws rather than merging two histories. MemoryBlockStore.fromSnapshot builds a store around a snapshot instead, which is the same thing for a store that was never used.

What it carries

Everything needed to read the data and to keep writing correctly afterwards:

  • The block bytes the current manifest points at — verbatim, already compressed.
  • One checkpoint manifest, the table catalog, the live segments, and the committed transactions that segment visibility resolves through.
  • The row-id and auto-increment counters, so later writes continue past the high-water mark instead of colliding with rows that are there but hidden.
  • Unique-key membership, so an insert that duplicates an existing key still conflicts.
  • Full-text bases, when they already cover the exported version.

And what it deliberately drops: leases, query spill pages, garbage-collection and compaction job records, in-flight transactions, and version history. A database with ten thousand commits behind it loads as one clean version. Superseded blocks a compaction left behind are not copied, so a snapshot is usually smaller than the database it came from.

A full-text index that does not already cover the exported version is marked for rebuild rather than shipped stale. The index is a pruning accelerator that the scan re-verifies, so that costs a rebuild, never a wrong answer.

The container

A magic number, a format version, a gzipped JSON header, and the block payloads laid end to end. Blocks are already self-describing and doubly CRC-checked, so loading authenticates every block header without decompressing any payload — a corrupt file fails at load rather than mid-query.

Read the header alone when you only need to know what a file is:

import { readSnapshotSummary } from "@minnowdb/core/storage";

const summary = await readSnapshotSummary(bytes);
// { formatVersion, version, createdAt, tableCount, blockCount, payloadBytes, byteLength }

Cheap enough to run against a large file before deciding whether to load it.

Exporting safely

MemoryBlockStore#exportSnapshot runs on the store's commit queue, so it always sees one consistent version.

IndexedDbBlockStore#exportSnapshot reads across several transactions, so a concurrent commit could move the version underneath it. Hold a backup lease across the call when another writer is possible; a build script that is the only writer in its process does not need one.

MinnowDatabase#exportSnapshot delegates to whichever of those the database was built on, so it inherits the same rule. A store that implements neither method says so rather than failing as a missing property.

The snapshot format is versioned and validated, but like the block format it carries no compatibility promise while the library is version zero. Treat a snapshot as a copy of a database you can rebuild, not as an archival format.

On this page