Low-level API
The @aibulat/indexeddb root entry. IndexedDB, but usable: the raw API predates promises, so every read is an IDBRequest you attach onsuccess and onerror to, and the type definitions know nothing about your schema. This entry wraps the whole surface in Proxy objects so requests become promises and store names become literal types. You still think in object stores, transactions and cursors — that is the point. For a schema DSL and a query builder instead, see Nexie.
It is a fork of idb by Jake Archibald, forked at v8.0.3 and maintained as an API-compatible superset: everything idb does works the same way, plus fixes and additions upstream never shipped. It is ~1.9 kB brotli'd and has no runtime dependencies.
Install
npm install @aibulat/indexeddbESM only — there is no CommonJS build and no UMD global.
Signature
function openDB<DBTypes extends DBSchema | unknown = unknown>(
name: string,
version?: number,
callbacks?: OpenDBCallbacks<DBTypes>,
): Promise<IDBPDatabase<DBTypes>>;
function deleteDB(
name: string,
callbacks?: DeleteDBCallbacks,
): Promise<void>;
function wrap(value: IDBDatabase): IDBPDatabase;
function unwrap<T>(wrapped: T): unknown;
function ignoreConstraints<T>(operation: Promise<T>): Promise<T | undefined>;Use
import { openDB } from "@aibulat/indexeddb";
const db = await openDB("my-db", 1, {
upgrade(db) {
db.createObjectStore("keyval");
},
});
await db.put("keyval", "hello", "greeting");
console.log(await db.get("keyval", "greeting")); // "hello"Typed schemas
Describe the database once and every store name, key and value is checked:
import { openDB, type DBSchema } from "@aibulat/indexeddb";
interface MyDB extends DBSchema {
articles: {
key: number;
value: { id: number; title: string; date: Date };
indexes: { date: Date };
};
}
const db = await openDB<MyDB>("articles-db", 1, {
upgrade(db) {
const store = db.createObjectStore("articles", { keyPath: "id" });
store.createIndex("date", "date");
},
});
// Typed: the store name, the value shape and the index name are all checked.
const byDate = await db.getAllFromIndex("articles", "date");Async iteration
Stores, indexes and cursors are async-iterable:
const tx = db.transaction("articles");
for await (const cursor of tx.store) {
console.log(cursor.value.title);
}Use iterateKeys() when you only need keys and would rather not read every value.
Beyond idb
// Read in reverse, or ask for keys and values together.
const newest = await db.getAll("articles", { direction: "prev", count: 10 });
const records = await db.getAllRecords("articles"); // { key, primaryKey, value }
// Skip duplicates without losing the rest of the batch. Call it in the same
// turn as the write, on a store or index request (not a db.* shortcut).
const tx = db.transaction("articles", "readwrite");
await Promise.all(articles.map((a) => ignoreConstraints(tx.store.add(a))));
await tx.done;
// Close at the end of the scope.
using db2 = await openDB("articles-db", 1);Behaviour
| Requests | Any method that would return an IDBRequest returns a promise instead. |
| Transactions | tx.done is a promise for the transaction completing, rejecting with the error that actually caused the failure. tx.store is the store, when the transaction covers exactly one. |
| Shortcuts | db.get, getKey, getAll, getAllKeys, getAllRecords, count, put, add, delete, clear and the …FromIndex variants run a whole one-store transaction for you. |
| Identity | Wrapped objects are cached, so db.transaction === db.transaction, and they still satisfy instanceof IDBDatabase. |
| Escape hatch | unwrap() returns the underlying native object; wrap() enhances one you were handed. |
Gotchas
- Do not
awaitanything else mid-transaction. IndexedDB auto-commits a transaction once it has nothing left to do after microtasks drain. Awaiting afetchin the middle closes it, and the nextstore.putfails. This is IndexedDB behaviour, not something the wrapper can hide. (Nexie solves this differently — its transactions surviveawait, at the cost of the promises being its own.) - Some methods throw rather than reject. The library cannot know in advance which members return an
IDBRequest, sostore.putand friends may throw synchronously. Inside anasyncfunction there is no observable difference. For the same reason, assigningonsuccess/onerrorto the returned promise does nothing — there is no request there. Useunwrap()if you need the real one. - Types degrade deliberately without a schema. With no
DBSchematype argument, store names widen tostringand values toany— which is the documented way to opt out during multi-version migrations.
Full reference for this entry, including every enhancement and the TypeScript opt-out, is in Part 1 of the package README.
Want a schema DSL, a query builder, transactions that survive await and reactive queries instead? That is the other API in this package — Nexie.