Skip to content

Granth

The database: schema and versions, opening and closing, transactions, plugins.

js
import Granth from 'granthdb';

const db = new Granth('myapp', {
  worker: () => new Worker(new URL('./db.worker.js', import.meta.url), { type: 'module' }),
});

Constructor

new Granth(name, options)

OptionTypeDescription
worker() => WorkerShorthand for the default worker runtime. Called only in the tab elected leader.
runtimeRuntimePluginExplicit runtime. Overrides worker. See Runtimes.
timeoutMsnumberHow long to wait for a leader before failing. Default 5000.

You need either worker or runtime. worker is the shorthand almost everyone wants:

js
const db = new Granth('myapp', {
  worker: () => new Worker(new URL('./db.worker.js', import.meta.url), { type: 'module' }),
});

To run without a Worker at all (strict CSP, SSR, Node, tests):

js
import { inlineRuntime } from 'granth-runtime-inline';
const db = new Granth('myapp', { runtime: inlineRuntime({ createHandlers }) });

The constructor is side-effect free — it touches no browser API. new Granth(...) at module scope is safe under SSR (Next.js, Nuxt, Angular Universal); nothing happens until you use it.

Properties

PropertyTypeDescription
namestringDatabase name
vernonumberCurrent version number
tablesTable[]All declared tables

Methods

version(n).stores({...})

Declares the schema for version n. Cumulative, exactly like Dexie: later versions declare only what changed, and unchanged stores carry over. null deletes a store.

js
db.version(1).stores({ friends: '++id, name, age, *tags' });
db.version(2).stores({ friends: '++id, name, age, city, *tags' }); // adds `city`
db.version(3).stores({ oldTable: null });                          // drops it

Changing a schema without bumping the version throws a clear error rather than being silently ignored. Data transforms go in your worker file (see Storage), because a function cannot cross into a worker:

js
startGranthWorker({ sqlite3InitModule, upgrades: { 2: (engine) => { /* backfill */ } } });

open()Promise<OpenResult>

Runs migrations. Idempotent and safe to call from every tab. You rarely need it — any query auto-opens first.

Resolves to { version, from, migrated, schema }.

table(name)Table

Also available as a property: db.friendsdb.table('friends').

transaction(...)

Two forms — see Transaction.

liveQuery(querier, opts)Observable

See liveQuery.

close(), delete() / deleteDatabase()

close() flushes pending writes first. delete() destroys the database file; not recoverable.

export(opts?)Promise<Dump> · import(dump, opts?)Promise<Record<string, number>>

A complete, JSON-safe snapshot, and its counterpart.

js
const dump = await db.export();               // { format:'granth/1', version, tables }
localStorage.setItem('backup', JSON.stringify(dump));

await db.import(dump, { clear: true });        // idempotent (INSERT OR REPLACE)

Rows are exported in their stored form, so a JSON.stringify round trip preserves Date, NaN, Infinity and BigInt — exporting decoded documents would push them back through JSON and lose exactly what the codec exists to protect. Indexes are rebuilt on import.

This is what makes "always keep a rebuild path" (Storage) actionable rather than advice.

clearAll()Promise<string[]>

Empties every table without dropping the schema. Returns the table names cleared.

size()Promise<number>

Bytes the database occupies on disk.

storageKind()Promise<'opfs' | 'indexeddb' | 'memory'>

Which storage backend actually opened. See Storage.

runtimeKind()'worker' | 'inline'

Which runtime connected. See Runtimes.

use(plugin)PluginHandle

Register an addon. Returns a handle so it can be removed again at runtime.

js
const handle = db.use({
  name: 'audit',
  setup(ctx) {
    ctx.before(({ op, table, args }) => log(op, table));
    ctx.after(({ op }, result) => { /* return a value to replace the result */ });
    ctx.onDispose(() => log('audit removed'));
  },
});

db.plugins;          // ['audit']
await handle.dispose();
db.plugins;          // []

A before hook that returns a value short-circuits the call entirely — that is how a cache or an encryption addon intercepts. See Plugins.

pluginsstring[]

Names of the registered addons.

flush()Promise<void>

Forces a checkpoint. No-op on OPFS; on the IndexedDB fallback it persists immediately.

isOpen(), hasBeenClosed(), hasFailed()

on(event, fn) / once(event, fn)

Events: ready, versionchange, blocked, close.

Granth.isSupported()boolean (static)

false during SSR and in browsers without Web Locks or a secure context. Safe to call anywhere.

js
if (!Granth.isSupported()) return <ServerFallback />;

db.Version (Dexie compatibility)

Present so that code doing instanceof db.Version or feature-detecting it keeps working after a migration. Schema versions are declared with version(n).stores({...}); there is nothing useful to call on this class, and new code should ignore it.

Utilities

getByKeyPath(obj, keyPath)unknown

Reads a possibly-dotted keyPath out of a plain object, the same way an index does when it evaluates 'addr.city'. Exported because sorting or grouping results client-side otherwise means re-implementing it — and re-implementing it slightly differently is how a list ends up ordered differently from the query that produced it.

js
import { getByKeyPath } from 'granthdb';

const rows = await db.friends.toArray();
rows.sort((a, b) => getByKeyPath(a, 'addr.city') < getByKeyPath(b, 'addr.city') ? -1 : 1);

Returns undefined for a missing path rather than throwing, so it is safe on documents that predate the field.