or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

database-management.mdevents-utilities.mdindex.mdkey-ranges.mdobject-stores.mdtransactions-cursors.md
tile.json

transactions-cursors.mddocs/

Transactions and Cursors

Transaction management and cursor-based iteration for efficient data processing and batch operations.

Capabilities

IDBTransaction

Provides atomic operations across one or more object stores with ACID properties.

interface IDBTransaction extends EventTarget {
  /** List of object stores in this transaction scope (readonly) */
  readonly objectStoreNames: DOMStringList;
  
  /** Transaction access mode (readonly) */
  readonly mode: "readonly" | "readwrite" | "versionchange";
  
  /** Durability setting for the transaction (readonly) */
  readonly durability: "default" | "strict" | "relaxed";
  
  /** Database this transaction belongs to (readonly) */
  readonly db: IDBDatabase;
  
  /** Error that caused transaction to abort (readonly) */
  readonly error: DOMException | null;
  
  /**
   * Gets an object store within this transaction
   * @param name - Object store name
   * @returns Object store instance bound to this transaction
   */
  objectStore(name: string): IDBObjectStore;
  
  /**
   * Explicitly aborts the transaction
   */
  abort(): void;
  
  /**
   * Explicitly commits the transaction (auto-commits when no pending operations)
   */
  commit(): void;
  
  // Event handlers
  onabort: ((event: Event) => void) | null;
  oncomplete: ((event: Event) => void) | null;
  onerror: ((event: Event) => void) | null;
}

Usage Examples:

import "fake-indexeddb/auto";

// Single store transaction
const tx1 = db.transaction("products", "readonly");
const productsStore = tx1.objectStore("products");

productsStore.get("LAP001").onsuccess = (event) => {
  console.log("Product:", event.target.result);
};

tx1.oncomplete = () => {
  console.log("Read transaction completed");
};

// Multi-store transaction
const tx2 = db.transaction(["products", "orders"], "readwrite");
const productsStore2 = tx2.objectStore("products");
const ordersStore = tx2.objectStore("orders");

// Atomic operation across multiple stores
const productRequest = productsStore2.get("LAP001");
productRequest.onsuccess = (event) => {
  const product = event.target.result;
  
  if (product.stock > 0) {
    // Decrease stock
    product.stock--;
    productsStore2.put(product);
    
    // Create order
    ordersStore.add({
      productSku: product.sku,
      quantity: 1,
      price: product.price,
      timestamp: new Date()
    });
  } else {
    // Abort if out of stock
    tx2.abort();
  }
};

tx2.oncomplete = () => {
  console.log("Order transaction completed successfully");
};

tx2.onabort = () => {
  console.log("Order transaction aborted - insufficient stock");
};

tx2.onerror = (event) => {
  console.error("Transaction error:", event.target.error);
};

Transaction Lifecycle Management

// Transaction with durability settings
const tx = db.transaction("criticalData", "readwrite", {
  durability: "strict" // Ensures data is written to persistent storage
});

const store = tx.objectStore("criticalData");

// Queue multiple operations
store.put({ id: 1, data: "important" });
store.put({ id: 2, data: "critical" });
store.delete("old-record");

// Manual commit for control
tx.commit();

tx.oncomplete = () => {
  console.log("Critical data saved with strict durability");
};

// Handle transaction timeout/abort
setTimeout(() => {
  if (tx.error === null) {
    console.log("Transaction taking too long, aborting");
    tx.abort();
  }
}, 5000);

IDBCursor

Enables efficient iteration over records in object stores or indexes.

interface IDBCursor {
  /** Source object store or index (readonly) */
  readonly source: IDBObjectStore | IDBIndex;
  
  /** Iteration direction (readonly) */
  readonly direction: IDBCursorDirection;
  
  /** Current key (readonly) */
  readonly key: IDBValidKey;
  
  /** Current primary key (readonly) */
  readonly primaryKey: IDBValidKey;
  
  /**
   * Advances cursor by specified number of records
   * @param count - Number of records to skip
   */
  advance(count: number): void;
  
  /**
   * Continues cursor to next record or specific key
   * @param key - Optional key to continue to
   */
  continue(key?: IDBValidKey): void;
  
  /**
   * Continues cursor to specific key and primary key combination
   * @param key - Index key to continue to
   * @param primaryKey - Primary key to continue to
   */
  continuePrimaryKey(key: IDBValidKey, primaryKey: IDBValidKey): void;
  
  /**
   * Deletes the current record
   * @returns Request completing when deletion is done
   */
  delete(): IDBRequest;
  
  /**
   * Updates the current record
   * @param value - New value for the record
   * @returns Request completing when update is done
   */
  update(value: any): IDBRequest;
}

IDBCursorWithValue

Extended cursor that also provides access to record values.

interface IDBCursorWithValue extends IDBCursor {
  /** Current record value (readonly) */
  readonly value: any;
}

Usage Examples:

// Basic cursor iteration
const tx = db.transaction("products", "readonly");
const store = tx.objectStore("products");

store.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    const product = cursor.value;
    console.log(`${product.name}: $${product.price}`);
    cursor.continue();
  } else {
    console.log("No more products");
  }
};

// Cursor with key range and direction
const priceIndex = store.index("by_price");
const range = IDBKeyRange.bound(100, 500); // $100-$500 range

priceIndex.openCursor(range, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    console.log(`Expensive: ${cursor.value.name} - $${cursor.key}`);
    cursor.continue();
  }
};

// Key-only cursor for better performance
store.openKeyCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    console.log("Product SKU:", cursor.key);
    cursor.continue();
  }
};

Advanced Cursor Patterns

Batch Processing with Cursors

// Process records in batches
function processBatch(store, batchSize = 100) {
  return new Promise((resolve, reject) => {
    const results = [];
    let processed = 0;
    
    store.openCursor().onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor) {
        const record = cursor.value;
        
        // Process record
        const processedRecord = {
          ...record,
          processedAt: new Date(),
          status: 'processed'
        };
        
        results.push(processedRecord);
        processed++;
        
        if (processed >= batchSize) {
          // Process batch and continue
          console.log(`Processed batch of ${results.length} records`);
          results.length = 0; // Clear batch
          processed = 0;
        }
        
        cursor.continue();
      } else {
        // Process final batch
        if (results.length > 0) {
          console.log(`Processed final batch of ${results.length} records`);
        }
        resolve();
      }
    };
    
    store.openCursor().onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// Usage
const tx = db.transaction("largeDataset", "readonly");
const store = tx.objectStore("largeDataset");

processBatch(store, 50).then(() => {
  console.log("All records processed");
});

Cursor Pagination

// Paginate through results using cursors
function getPage(store, pageSize = 20, lastKey = null) {
  return new Promise((resolve) => {
    const results = [];
    let count = 0;
    
    const range = lastKey ? IDBKeyRange.lowerBound(lastKey, true) : null;
    
    store.openCursor(range).onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor && count < pageSize) {
        results.push({
          key: cursor.key,
          value: cursor.value
        });
        count++;
        cursor.continue();
      } else {
        const hasMore = cursor !== null;
        const nextKey = hasMore ? cursor.key : null;
        
        resolve({
          results,
          hasMore,
          nextKey
        });
      }
    };
  });
}

// Usage - paginate through products
async function paginateProducts() {
  const tx = db.transaction("products", "readonly");
  const store = tx.objectStore("products");
  
  let page = 1;
  let lastKey = null;
  
  do {
    const result = await getPage(store, 10, lastKey);
    
    console.log(`Page ${page}:`, result.results.map(r => r.value.name));
    
    lastKey = result.nextKey;
    page++;
    
    if (!result.hasMore) break;
  } while (lastKey);
}

Conditional Updates with Cursors

// Update records based on conditions
function updatePrices(store, priceIncrease = 0.1) {
  return new Promise((resolve, reject) => {
    let updated = 0;
    
    const tx = store.transaction;
    
    store.openCursor().onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor) {
        const product = cursor.value;
        
        // Apply conditional logic
        if (product.category === "electronics" && product.price < 1000) {
          product.price = Math.round(product.price * (1 + priceIncrease) * 100) / 100;
          product.lastUpdated = new Date();
          
          cursor.update(product).onsuccess = () => {
            updated++;
          };
        }
        
        cursor.continue();
      } else {
        resolve(updated);
      }
    };
    
    store.openCursor().onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// Usage
const tx = db.transaction("products", "readwrite");
const store = tx.objectStore("products");

updatePrices(store, 0.05).then((count) => {
  console.log(`Updated prices for ${count} electronics products`);
});

tx.oncomplete = () => {
  console.log("Price update transaction completed");
};

Cursor with Index Iteration

// Iterate through index with cursor for complex queries
function findProductsByPriceRange(categoryIndex, category, minPrice, maxPrice) {
  return new Promise((resolve) => {
    const results = [];
    
    // Use compound index for efficient querying
    const range = IDBKeyRange.bound([category, minPrice], [category, maxPrice]);
    
    categoryIndex.openCursor(range).onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor) {
        results.push(cursor.value);
        cursor.continue();
      } else {
        resolve(results);
      }
    };
  });
}

// Usage
const tx = db.transaction("products", "readonly");
const store = tx.objectStore("products");
const categoryIndex = store.index("category_price");

findProductsByPriceRange(categoryIndex, "electronics", 500, 1500).then((products) => {
  console.log("Electronics in $500-$1500 range:", products);
});

Cursor Direction Options

// Different cursor directions
const store = tx.objectStore("products");
const priceIndex = store.index("by_price");

// Forward iteration (default)
priceIndex.openCursor(null, "next").onsuccess = (event) => {
  // Cheapest to most expensive
};

// Reverse iteration  
priceIndex.openCursor(null, "prev").onsuccess = (event) => {
  // Most expensive to cheapest
};

// Unique values only (forward)
priceIndex.openCursor(null, "nextunique").onsuccess = (event) => {
  // Skip duplicate prices, cheapest to expensive
};

// Unique values only (reverse)
priceIndex.openCursor(null, "prevunique").onsuccess = (event) => {
  // Skip duplicate prices, expensive to cheapest
};

Transaction Error Handling

// Comprehensive error handling
const tx = db.transaction(["products", "inventory"], "readwrite");

tx.onerror = (event) => {
  const error = event.target.error;
  console.error("Transaction failed:", error.name, error.message);
  
  // Handle specific error types
  switch (error.name) {
    case "AbortError":
      console.log("Transaction was aborted");
      break;
    case "ConstraintError":  
      console.log("Constraint violation occurred");
      break;
    case "ReadOnlyError":
      console.log("Attempted write on readonly transaction");
      break;
    default:
      console.log("Unknown transaction error");
  }
};

tx.onabort = (event) => {
  console.log("Transaction aborted");
  // Cleanup or retry logic
};

// Operation-level error handling
const store = tx.objectStore("products");
const request = store.add(product);

request.onerror = (event) => {
  console.error("Add operation failed:", event.target.error);
  // Don't prevent transaction from continuing
  event.preventDefault();
};