Skip to content

The DB Runtime

The runtime ties tables to a storage driver and executes commands. There are three runtime classes; you almost always use the first two together.

DB is the core runtime. You construct it with a driver and optional settings, then load your tables.

import { DB, hyperDBTraceStore } from "@will-be-done/hyperdb";
import { BptreeInmemDriver } from "@will-be-done/hyperdb/drivers/inmemory";
const baseDb = new DB(new BptreeInmemDriver(), {
runtimeValidation: true,
freezeArgs: true,
freezeRows: false,
tracer: hyperDBTraceStore,
});
baseDb.loadTables([tasksTable]);
OptionDefaultDescription
runtimeValidationfalseValidate full records against their table validators on writes and on reads from the driver
freezeArgsfalseDeep-freeze selector args used by cached selectors/runs
freezeRowsfalseDeep-freeze rows after write normalization
traits[]Initial metadata traits attached to the DB
tracerglobal defaultPer-DB tracer — an instance, "default", or "disabled" (see Devtools & Tracing)
dbNameA name used by tracing/devtools to label this database

runtimeValidation is invaluable in development: it catches schema mismatches at the boundary instead of letting bad data into storage. freezeArgs / freezeRows help surface accidental mutation of cached data.

SubscribableDB wraps a DB and adds everything reactivity needs: revisions, subscriptions, and lifecycle hooks. Wrap your base DB in it for any app that renders from the data.

import { DB, SubscribableDB } from "@will-be-done/hyperdb";
const db = new SubscribableDB(new DB(new BptreeInmemDriver()));
db.loadTables([tasksTable]);

Every committed transaction increments a revision and notifies subscribers with the operations it performed:

db.getRevision(); // current revision number
const unsub = db.subscribe((ops, traits, revision) => {
// ops describe the committed insert/upsert/delete operations
});

This is the mechanism the selector cache uses.

You can run extra commands inside the same transaction whenever rows change. Each hook is a generator and may itself read and write. This is how the sync engine records change-tracking rows alongside every mutation.

const off = db.afterChange(function* (db, table, traits, ops) {
// runs for every insert/upsert/delete, within the committing transaction
});
db.afterInsert(function* (db, table, traits, ops) {
/* InsertOp[] */
});
db.afterUpsert(function* (db, table, traits, ops) {
/* UpsertOp[] */
});
db.afterDelete(function* (db, table, traits, ops) {
/* DeleteOp[] */
});

InsertOp / UpsertOp / DeleteOp carry the affected rows (upserts and deletes include the previous value), so a hook has everything it needs to derive a diff.

Because hooks run within the transaction, anything they write commits atomically with the change that triggered them — and a throw rolls the whole thing back.

Selectors and actions are generators. The dispatch and select helpers run them for you, but you can also drive a generator directly:

HelperUse
syncDispatch(db, action(args))Run an action in a transaction (sync drivers)
asyncDispatch(db, action(args))Run an action in a transaction (async drivers)
select(db, gen)Run a selector once (sync drivers)
runSelectorAsync(db, () => gen)Run a selector once (async drivers)
execSync(generator)Drive a raw DB-command generator (sync)
execAsync(generator)Drive a raw DB-command generator (async)

execSync throws if the generator yields an async command, which is how the sync/async split is enforced. Async drivers must go through execAsync / asyncDispatch / runSelectorAsync.

Traits are arbitrary metadata you attach to a DB or a transaction; they flow through to hooks and the tracer. Derive a DB view that carries extra traits with withTraits, and read the current set inside an action with getCurrentTraits.

import { getCurrentTraits } from "@will-be-done/hyperdb";
const scopedDb = db.withTraits({ type: "myFeature.source", value: "import" });
const whoami = action({
name: "whoami",
args: {},
handler: function* () {
const traits = yield* getCurrentTraits();
return traits;
},
});

A typical use is tagging writes with their origin (e.g. “local edit” vs. “applied from sync”) so an afterChange hook can decide whether to record them.