A small wrapper that makes IndexedDB usable with promises and enhanced TypeScript support
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Enhanced cursor interfaces with promise-based navigation and async iteration support for efficient data traversal.
Access cursor state and metadata during navigation.
/**
* The key of the current index or object store item
*/
readonly key: IndexName extends IndexNames<DBTypes, StoreName>
? IndexKey<DBTypes, StoreName, IndexName>
: StoreKey<DBTypes, StoreName>;
/**
* The key of the current object store item (primary key)
*/
readonly primaryKey: StoreKey<DBTypes, StoreName>;
/**
* Returns the IDBObjectStore or IDBIndex the cursor was opened from
*/
readonly source: IndexName extends IndexNames<DBTypes, StoreName>
? IDBPIndex<DBTypes, TxStores, StoreName, IndexName, Mode>
: IDBPObjectStore<DBTypes, TxStores, StoreName, Mode>;Usage Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
const nameIndex = userStore.index("name");
// Object store cursor
let storeCursor = await userStore.openCursor();
if (storeCursor) {
console.log("Current key:", storeCursor.key); // Primary key
console.log("Primary key:", storeCursor.primaryKey); // Same as key for store cursors
console.log("Source:", storeCursor.source === userStore); // true
}
// Index cursor
let indexCursor = await nameIndex.openCursor();
if (indexCursor) {
console.log("Current key:", indexCursor.key); // Index key (name value)
console.log("Primary key:", indexCursor.primaryKey); // Primary key of the record
console.log("Source:", indexCursor.source === nameIndex); // true
}Navigate to different positions within the cursor's iteration space.
/**
* Advances the cursor a given number of records
* Resolves to null if no matching records remain
* @param count - Number of records to advance
* @returns Promise resolving to cursor at new position or null
*/
advance<T>(this: T, count: number): Promise<T | null>;/**
* Advance the cursor by one record (unless 'key' is provided)
* Resolves to null if no matching records remain
* @param key - Advance to the index or object store with a key equal to or greater than this value
* @returns Promise resolving to cursor at new position or null
*/
continue<T>(
this: T,
key?: IndexName extends IndexNames<DBTypes, StoreName>
? IndexKey<DBTypes, StoreName, IndexName>
: StoreKey<DBTypes, StoreName>
): Promise<T | null>;/**
* Advance the cursor by given keys
* The operation is 'and' – both keys must be satisfied
* Resolves to null if no matching records remain
* @param key - Advance to the index or object store with a key equal to or greater than this value
* @param primaryKey - and where the object store has a key equal to or greater than this value
* @returns Promise resolving to cursor at new position or null
*/
continuePrimaryKey<T>(
this: T,
key: IndexName extends IndexNames<DBTypes, StoreName>
? IndexKey<DBTypes, StoreName, IndexName>
: StoreKey<DBTypes, StoreName>,
primaryKey: StoreKey<DBTypes, StoreName>
): Promise<T | null>;Navigation Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
// Basic cursor navigation
let cursor = await userStore.openCursor();
while (cursor) {
console.log("User:", cursor.key, cursor.value);
cursor = await cursor.continue(); // Move to next record
}
// Skip records with advance
cursor = await userStore.openCursor();
if (cursor) {
console.log("First user:", cursor.value);
cursor = await cursor.advance(5); // Skip next 5 records
if (cursor) {
console.log("Sixth user:", cursor.value);
}
}
// Continue to specific key
cursor = await userStore.openCursor();
if (cursor) {
console.log("Starting from:", cursor.key);
cursor = await cursor.continue("user100"); // Jump to user100 or next key
if (cursor) {
console.log("Jumped to:", cursor.key);
}
}
// Index cursor with primary key continuation
const nameIndex = userStore.index("name");
let indexCursor = await nameIndex.openCursor();
if (indexCursor) {
console.log("First Alice:", indexCursor.key, indexCursor.primaryKey);
// Continue to next "Alice" with primary key >= 100
indexCursor = await indexCursor.continuePrimaryKey("Alice", 100);
if (indexCursor) {
console.log("Next Alice >= 100:", indexCursor.primaryKey);
}
}Modify or delete records at the current cursor position.
/**
* Update the current record
* Only available in readwrite and versionchange transactions
* @param value - New value for the current record
* @returns Promise resolving to the key of the updated record
*/
update: Mode extends 'readonly'
? undefined
: (
value: StoreValue<DBTypes, StoreName>
) => Promise<StoreKey<DBTypes, StoreName>>;/**
* Delete the current record
* Only available in readwrite and versionchange transactions
* @returns Promise that resolves when deletion is complete
*/
delete: Mode extends 'readonly' ? undefined : () => Promise<void>;Modification Examples:
// Update records using cursor
const updateTx = db.transaction("users", "readwrite");
const updateStore = updateTx.store;
let updateCursor = await updateStore.openCursor();
while (updateCursor) {
const user = updateCursor.value;
// Update inactive users
if (user.lastLogin < oneMonthAgo) {
await updateCursor.update({
...user,
status: "inactive"
});
}
updateCursor = await updateCursor.continue();
}
await updateTx.done;
// Delete records using cursor
const deleteTx = db.transaction("users", "readwrite");
const deleteStore = deleteTx.store;
let deleteCursor = await deleteStore.openCursor();
while (deleteCursor) {
const user = deleteCursor.value;
// Delete old inactive users
if (user.status === "inactive" && user.lastLogin < sixMonthsAgo) {
await deleteCursor.delete();
}
deleteCursor = await deleteCursor.continue();
}
await deleteTx.done;Enhanced cursor that includes the record value.
interface IDBPCursorWithValue<...> extends IDBPCursor<...> {
/**
* The value of the current item
*/
readonly value: StoreValue<DBTypes, StoreName>;
}Usage Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
// Cursor with value (most common)
let cursorWithValue = await userStore.openCursor();
while (cursorWithValue) {
console.log("Key:", cursorWithValue.key);
console.log("Value:", cursorWithValue.value); // Full record available
console.log("Name:", cursorWithValue.value.name);
cursorWithValue = await cursorWithValue.continue();
}
// Key-only cursor (more efficient when you don't need values)
let keyCursor = await userStore.openKeyCursor();
while (keyCursor) {
console.log("Key:", keyCursor.key);
// cursorKey.value; // Not available - undefined
keyCursor = await keyCursor.continue();
}Modern async iteration support for cursors.
/**
* Iterate over the cursor using async iteration
* @returns Async iterable iterator over cursor values
*/
[Symbol.asyncIterator](): AsyncIterableIterator<
IDBPCursorIteratorValue<DBTypes, TxStores, StoreName, IndexName, Mode>
>;Async Iteration Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
// Direct async iteration over store
for await (const cursor of userStore) {
console.log("User:", cursor.key, cursor.value);
// Control iteration flow
if (cursor.value.department === "Engineering") {
cursor.continue(); // Skip to next
}
// Early exit
if (cursor.value.id > 1000) {
break;
}
}
// Async iteration over index
const nameIndex = userStore.index("name");
for await (const cursor of nameIndex.iterate("Alice")) {
console.log("Alice variant:", cursor.value.name, cursor.primaryKey);
// Modify during iteration (in readwrite transaction)
// cursor.update({ ...cursor.value, processed: true });
}
// Manual cursor async iteration
let cursor = await userStore.openCursor();
if (cursor) {
for await (const iteratorCursor of cursor) {
console.log("Iterator cursor:", iteratorCursor.value);
// iteratorCursor.continue(); // Called automatically by iterator
}
}Control the order of cursor iteration.
type IDBCursorDirection = "next" | "nextunique" | "prev" | "prevunique";Direction Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
const ageIndex = userStore.index("age");
// Forward iteration (default)
let forwardCursor = await userStore.openCursor(null, "next");
while (forwardCursor) {
console.log("Forward:", forwardCursor.key);
forwardCursor = await forwardCursor.continue();
}
// Reverse iteration
let reverseCursor = await userStore.openCursor(null, "prev");
while (reverseCursor) {
console.log("Reverse:", reverseCursor.key);
reverseCursor = await reverseCursor.continue();
}
// Unique values only (useful for indexes with duplicates)
let uniqueCursor = await ageIndex.openCursor(null, "nextunique");
while (uniqueCursor) {
console.log("Unique age:", uniqueCursor.key);
uniqueCursor = await uniqueCursor.continue();
}
// Reverse unique
let reverseUniqueCursor = await ageIndex.openCursor(null, "prevunique");
while (reverseUniqueCursor) {
console.log("Reverse unique age:", reverseUniqueCursor.key);
reverseUniqueCursor = await reverseUniqueCursor.continue();
}Efficient patterns for cursor-based operations.
Batch Processing:
async function batchUpdateUsers(batchSize = 100) {
let processed = 0;
const tx = db.transaction("users", "readwrite");
const userStore = tx.store;
let cursor = await userStore.openCursor();
while (cursor) {
// Process record
await cursor.update({
...cursor.value,
lastProcessed: new Date()
});
processed++;
// Commit batch and start new transaction
if (processed % batchSize === 0) {
await tx.done;
// Start new transaction for next batch
const newTx = db.transaction("users", "readwrite");
cursor = await newTx.objectStore("users").openCursor(
IDBKeyRange.lowerBound(cursor.key, true) // Continue from current position
);
} else {
cursor = await cursor.continue();
}
}
await tx.done; // Commit final batch
}Memory-Efficient Filtering:
async function findActiveUsersEfficiently(condition: (user: User) => boolean) {
const results = [];
const tx = db.transaction("users", "readonly");
// Use cursor to avoid loading all records into memory
for await (const cursor of tx.store) {
if (condition(cursor.value)) {
results.push(cursor.value);
}
// Optional: limit memory usage
if (results.length >= 1000) {
break;
}
}
await tx.done;
return results;
}Skip Pattern with Advance:
async function paginateWithCursor(pageSize: number, pageNumber: number) {
const tx = db.transaction("users", "readonly");
const skipCount = pageSize * pageNumber;
let cursor = await tx.store.openCursor();
// Skip to page start
if (cursor && skipCount > 0) {
cursor = await cursor.advance(skipCount);
}
// Collect page results
const results = [];
let collected = 0;
while (cursor && collected < pageSize) {
results.push(cursor.value);
collected++;
cursor = await cursor.continue();
}
await tx.done;
return {
data: results,
hasMore: cursor !== null
};
}Complex Navigation:
async function findUsersBetweenIds(startId: number, endId: number, excludeId: number) {
const tx = db.transaction("users", "readonly");
const results = [];
let cursor = await tx.store.openCursor(IDBKeyRange.bound(startId, endId));
while (cursor) {
if (cursor.key !== excludeId) {
results.push(cursor.value);
}
// Skip the excluded ID efficiently
if (cursor.key < excludeId) {
cursor = await cursor.continue(excludeId + 1);
} else {
cursor = await cursor.continue();
}
}
await tx.done;
return results;
}