FIMViz.js

Storage — usage reference

Storage (barrel-exported) is a generic key-value store over IndexedDB. It knows nothing about Datasets, files, or FIMViz — the host names the database, the tables, and the keys. Values are stored verbatim (structured-clone, not JSON) — an ArrayBuffer/TypedArray/ Blob/Map/Set/Date round-trips exactly; JSON.stringify(arrayBuffer) would silently produce "{}". Keys are out-of-line (no keyPath) — the store never touches your value to find its key.

Contents

Discovery · Getting an instance · Rows · Tables · Version-management primitives · Connection lifecycle · Notes

Discovery — does it exist, at what version?

IndexedDB cannot downgrade: opening at a version lower than the one on disk throws a hard VersionError. And the on-disk version is not something a host can track for itself — createTable/dropTable bump it as a side effect, so the number you passed to the constructor goes stale on its own. Ask before you open:

await Storage.databases();        // → [{ name, version }, …]  every DB in this origin
await Storage.exists('my-store'); // → boolean
await db.version();               // → the CURRENT on-disk version, or null if it doesn't exist yet
db.declaredVersion;               // → what THIS instance was constructed with (a plain noun)

version() is safe to call before opening: it never creates the database and never triggers an upgrade. declaredVersion and await version() diverge as soon as a createTable/dropTable bump lands — that divergence is the thing worth checking.

The safe open is to omit version entirely, which attaches to whatever already exists:

const db = new Storage({ name: 'my-store', tables: ['userFiles'] });   // no version → no downgrade risk

A downgrade error names the ways out: check the version, open at that version or higher, omit version, or destroy() and start over. Storage.databases() throws on engines without indexedDB.databases() rather than reporting an empty list — "no databases" and "cannot tell" must not look alike when the next step might be destroying data.

Getting an instance

new Storage({ name, version?, tables?, resetTablesOnUpgrade?, versionMigrate? })

// or, via a mounted FimMap (shares one Storage across every map on that app):
const fim = await mount('#el', { storage: { name: 'my-store', version: 1, tables: ['userFiles'] } });
fim.storage   // throws until `storage` was passed to mount()/create() — the engine names no default
Param Default Notes
name — (required) Throws without one — the host owns the schema.
version null (open at whatever's on disk) Creates tables on upgrade when set.
tables [] Table (object store) names to create when version triggers an upgrade.
resetTablesOnUpgrade false On an upgrade, drop and recreate every table in tables instead of only creating missing ones — the "a version bump clears local data" migration story. A no-op on a brand-new database.
versionMigrate null (ctx: {db, transaction, oldVersion, newVersion}) => void — a STRUCTURAL hook run inside the versionchange transaction, after the default create/reset. Use for create/delete-a-store or copy-rows-between-stores. Runs only on the configured-version open, not on createTable/dropTable's incremental bumps.

Rows

Row ops are scoped to one table via db.table(name) — a handle so a call reads as a verb on the table itself, rather than repeating table as an argument on every call to the db-level instance:

const userFiles = db.table('userFiles');

await userFiles.put(key, value)     // key required; value stored verbatim; returns the key
await userFiles.get(key)             // → value, or undefined if absent (a BROKEN read REJECTS,
                                      //   distinguishable from "absent")
await userFiles.has(key)             // → boolean (distinguishes a stored `undefined` from missing)
await userFiles.delete(key)          // → true
await userFiles.clear()              // delete every row in ONE table, keep the table itself
await userFiles.list({ keys?, range? })
// keys:true  → IDBValidKey[]              (cheap — no values deserialized; good for a filename picker)
// keys:false → { key, value }[]  (default, in key order)
// range: an optional IDBKeyRange

userFiles.name   // 'userFiles'

table(name) is pure sugar — each method delegates to the equivalent db-level call (db.put(table, key, value), db.get(table, key), …) with table bound; no separate transaction logic, and nothing to dispose (cheap to create, discard freely). Both forms are fully interchangeable — a row written via db.table('userFiles').put(k, v) reads the same way via db.get('userFiles', k), and vice versa; nothing about a table's data or schema differs between them.

Tables (structural — bumps the DB version)

await db.tables()          // → string[] — every table currently in the database
await db.createTable(name)  // → false if it already existed, else creates it (version bump)
await db.dropTable(name)    // → false if it didn't exist, else drops it (version bump)

Version-management primitives (mechanisms, not a framework)

Two kinds of change, two different tools — the host owns the policy either way:

await db.table('userFiles').map((key, value) => {
  // DATA migration — NO schema/version bump needed, since keys are out-of-line and values are
  // verbatim (a record's shape isn't part of the schema). Runs in ONE readwrite transaction,
  // iterating key→value in key order.
  return newValue;          // update the row (skipped if === the current value — a no-op costs nothing)
  return Storage.DELETE;    // delete the row (a Symbol — can't collide with a real stored value)
  return undefined;         // leave the row untouched (the safe default — a callback that forgets to
                             // return does nothing, rather than wiping data)
});
// → Promise<number> — how many rows were updated or deleted

await db.clearAll();   // wipe every row in every table, in ONE transaction — keeps schema + version.
                        // The KV "start over": clear then re-add, no schema teardown. Returns the
                        // table names that were cleared. Contrast clear(table) — one table only.

versionMigrate (constructor option, above) is the STRUCTURAL counterpart — create/drop a store or copy rows between stores, which IndexedDB only allows inside an onupgradeneeded transaction. Use map() for in-place record rewrites; reach for versionMigrate only when the shape of the database itself (which stores exist) needs to change.

Connection lifecycle

await db.close()     // drop the in-memory connection; data persists, next call reopens
await db.destroy()   // delete the ENTIRE database (closes first)
db.name              // the database name (getter)

Concurrent calls share one open connection (#conn() caches the open promise) — you don't need to manage connection reuse yourself.

Notes