Granth
The database: schema and versions, opening and closing, transactions, plugins.
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)
| Option | Type | Description |
|---|---|---|
worker | () => Worker | Shorthand for the default worker runtime. Called only in the tab elected leader. |
runtime | RuntimePlugin | Explicit runtime. Overrides worker. See Runtimes. |
timeoutMs | number | How long to wait for a leader before failing. Default 5000. |
You need either worker or runtime. worker is the shorthand almost everyone wants:
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):
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
| Property | Type | Description |
|---|---|---|
name | string | Database name |
verno | number | Current version number |
tables | Table[] | 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.
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 itChanging 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:
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.friends ≡ db.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.
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.
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.
plugins → string[]
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.
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.
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.