Chapter 49: IndexedDB Client-Side Database

Iterating with Cursors

Stream-processing massive datasets, bidirectional cursor iteration, in-place updates, deletions, and memory-efficient pagination with `IDBCursor`.

LEARNING OBJECTIVES
  • Understand why IDBCursor prevents browser memory exhaustion compared to getAll().
  • Control cursor navigation using openCursor(), continue(), advance(), and traversal directions.
  • Perform in-place batch mutations and deletions using cursor.update() and cursor.delete().
  • Implement robust cursor-based offset and keyset pagination for high-volume datasets.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine you are auditing 50,000 physical tax forms stored in a filing warehouse.

If you use getAll(), you instruct a forklift to dump all 50,000 heavy file boxes directly onto your small desktop at the exact same moment. Your desk collapses under the weight, paper flies everywhere, and your office runs out of oxygen (a browser tab crash / JavaScript Out-Of-Memory error).

If you use a Cursor (openCursor), you hire a diligent courier who brings you one single folder at a time:

  1. You inspect Folder #1.
  2. You stamp it, update it (cursor.update()), or shred it (cursor.delete()).
  3. You tap the courier on the shoulder and say "Next please" (cursor.continue()).
  4. The courier swaps out the folder. Your desk only ever holds one single folder in memory at any given millisecond.
  5. When the cabinet is empty, the courier signals that the job is finished (cursor === null).

Technical Deep Dive & Specifications

openCursor() Signature & Directions

You can open a cursor on an IDBObjectStore or an IDBIndex:

const request = source.openCursor([queryRange], [direction]);
+----------------------------------------------------------------------------------------------------+
|                                    IDBCURSOR TRAVERSAL DIRECTIONS                                   |
+-------------------+--------------------------------+-----------------------------------------------+
| Direction String  | Traversal Order                | Duplicate Key Handling                        |
+-------------------+--------------------------------+-----------------------------------------------+
| `'next'` (default)| Ascending (Lowest ➔ Highest)  | Yields all records matching index keys        |
+-------------------+--------------------------------+-----------------------------------------------+
| `'nextunique'`    | Ascending (Lowest ➔ Highest)  | Yields ONLY the first record for duplicate keys|
+-------------------+--------------------------------+-----------------------------------------------+
| `'prev'`          | Descending (Highest ➔ Lowest) | Yields all records in reverse order           |
+-------------------+--------------------------------+-----------------------------------------------+
| `'prevunique'`    | Descending (Highest ➔ Lowest) | Yields ONLY the first record per unique key   |
+-------------------+--------------------------------+-----------------------------------------------+

Cursor Anatomy & Methods

When request.onsuccess fires, request.result is an instance of IDBCursorWithValue (or null when finished):

+-----------------------------------------------------------------------------+
|                            IDBCursor Properties                             |
+-------------------+---------------------------------------------------------+
| `cursor.key`        | The index key or primary key currently under the cursor |
| `cursor.primaryKey` | The primary key of the current record                   |
| `cursor.value`      | The deserialized JavaScript object payload              |
+-------------------+---------------------------------------------------------+
|                             IDBCursor Methods                               |
+-------------------+---------------------------------------------------------+
| `cursor.continue([key])`  | Advances cursor to the next record (or to `key`)  |
| `cursor.advance(count)`   | Skips forward by `count` records (Offset paging)  |
| `cursor.update(newValue)` | Modifies the current record in-place              |
| `cursor.delete()`         | Deletes the current record from the ObjectStore   |
+-----------------------------------------------------------------------------+

The Cursor Recursive Event Loop Pattern

A cursor does not use a synchronous while loop. Instead, each call to cursor.continue() or cursor.advance() triggers another onsuccess event on the same IDBRequest:

                       request = store.openCursor()
                                     |
                                     v
                       +---------------------------+
                       |     request.onsuccess     | <----------------+
                       +---------------------------+                  |
                                     |                                |
                        const cursor = req.result;                    |
                                     |                                |
                               [cursor === null?]                     |
                                /          \                          |
                         YES   /            \   NO                    |
                              v              v                        |
                        [DONE / EXIT]   Process cursor.value          |
                                             |                        |
                                        cursor.continue() ------------+

💻 Interactive Code Playground

Starter Code

Save this file as cursors.html and open it in your browser.

Line-by-Line Code Breakdown

  • Line 77 (store.openCursor(null, 'next')): Opens a cursor across all records in ascending order. Passing null as the first argument indicates no range filter.
  • Lines 80–87 (if (cursor) ... cursor.continue()): The standard cursor loop pattern. When records exist, cursor is populated and cursor.continue() is invoked to trigger the next step. When iteration reaches the end, cursor is null.
  • Line 97 (store.openCursor(null, 'prev')): Reverses traversal, streaming from highest primary key (105) down to lowest (101).
  • Line 124 (const updateReq = cursor.update(user)): Mutates the record currently under the cursor in-place without needing a separate store.put() call.
  • Line 146 (cursor.delete()): Deletes the exact record currently referenced by the cursor.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
[03:00:00] 🛠️ Seeded 5 member records.
[03:00:00] ✅ CursorDemoDB ready.
[03:00:05] ▶️ Starting Forward Cursor Stream:
  [ID: 101] Alice - Points: 150
  [ID: 102] Bob - Points: 40
  [ID: 103] Charlie - Points: 320
  [ID: 104] Diana - Points: 10
  [ID: 105] Evan - Points: 500
🏁 Reached end of forward stream.

🏋️ Hands-On Exercise

🎯 The Challenge: Paginated Data Feed with cursor.advance()

Instructions:

  1. Open database NewsApp with store articles (keyPath: "id", autoIncrement: true).
  2. Seed 15 news articles with titles "Article 1" through "Article 15".
  3. Implement a function getPage(pageNumber, pageSize):
    • Calculate how many items to skip: skip = (pageNumber - 1) * pageSize.
    • Open a cursor.
    • If skip > 0, use cursor.advance(skip) on the first step to jump directly to the target offset.
    • Collect exactly pageSize items and log them.
  4. Test by fetching Page 2 with a pageSize of 4 (should return Articles 5, 6, 7, and 8).

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Calling Both cursor.continue() and cursor.advance(): Attempting to call both within the same onsuccess invocation throws InvalidStateError: The cursor is being continued.
  2. Forgetting to Call cursor.continue(): If you process a record in onsuccess and forget to call cursor.continue(), iteration halts permanently after the first record.
  3. Using Offset Pagination for Millions of Rows: cursor.advance(1000000) must still traverse 1,000,000 B-Tree leaf nodes. For massive tables, use keyset pagination (IDBKeyRange.lowerBound(lastSeenKey, true)) instead.

💡 Pro Tips

  1. Use openKeyCursor() When Payloads Are Not Needed: If you are aggregating keys, checking unique constraints, or counting custom ranges, store.openKeyCursor() returns only key and primaryKey, skipping all Structured Clone value deserialization.
  2. Combine Cursors with Web Workers: Streaming and filtering 500,000 records using a cursor inside a dedicated Web Worker ensures the main UI thread never drops a single frame.

📌 Key Takeaways

  • IDBCursor iterates through records one at a time, keeping memory consumption constant regardless of dataset size.
  • Traversal directions include 'next', 'prev', 'nextunique', and 'prevunique'.
  • In-place mutations and removals are executed directly via cursor.update() and cursor.delete().
  • cursor.advance(count) skips forward by a given count, enabling offset pagination.
  • openKeyCursor() streams only key metadata, bypassing object payload deserialization for maximum performance.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

What is the principal architectural advantage of using store.openCursor() instead of store.getAll() for large datasets?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What happens if you invoke cursor.advance(5)?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

Which cursor direction should you pass to openCursor() to iterate from the highest key value down to the lowest?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP