CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-idb

A small wrapper that makes IndexedDB usable with promises and enhanced TypeScript support

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

cursor-navigation.mddocs/

Cursor Navigation

Enhanced cursor interfaces with promise-based navigation and async iteration support for efficient data traversal.

Capabilities

Cursor Properties

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
}

Cursor Navigation Methods

Navigate to different positions within the cursor's iteration space.

Advance Method

/**
 * 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>;

Continue Method

/**
 * 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>;

Continue Primary Key Method

/**
 * 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);
  }
}

Cursor Modification Operations

Modify or delete records at the current cursor position.

Update Method

/**
 * 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 Method

/**
 * 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;

Cursor with Value Interface

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();
}

Async Iterator Interface

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
  }
}

Cursor Direction

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();
}

Cursor Performance Patterns

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;
}

docs

cursor-navigation.md

database-operations.md

enhanced-database.md

index-operations.md

index.md

object-store-operations.md

promise-wrapping.md

transaction-management.md

tile.json