Skip to content

Eagerly create multi-entry/fulltext index object stores in transaction constructor#84

Open
jell0wed wants to merge 3 commits intomicrosoft:masterfrom
jell0wed:users/jepoisso/idb-transaction-indexstoremap
Open

Eagerly create multi-entry/fulltext index object stores in transaction constructor#84
jell0wed wants to merge 3 commits intomicrosoft:masterfrom
jell0wed:users/jepoisso/idb-transaction-indexstoremap

Conversation

@jell0wed
Copy link
Copy Markdown
Collaborator

@jell0wed jell0wed commented Apr 28, 2026

Problem

IndexedDbProvider.openTransaction() creates the IDB transaction inside a .then() callback. When _fakeComplicatedKeys is true, multi-entry and full-text indexes are emulated as separate physical IDB object stores (e.g. replyChains_byMessageSearchKeys, dbVersion_dbName). These extra stores must be included in the IDB transaction scope — and openTransaction() does include them in the db.transaction() call — but the constructor of IndexedDbTransaction only captures references to the primary stores. The index stores are captured lazily, inside getStore().

This creates a timing bug:

IndexedDbProvider.openTransaction(["replyChains"], false)
  → lockHelper.openTransaction(...)               // Promise<token>
  → .then(token => {                              // microtask M1
        trans = db.transaction([
            "replyChains",
            "replyChains_byMessageSearchKeys",    // added by openTransaction
        ], "readonly");                           // IDB tx created — "active" in M1
        return new IndexedDbTransaction(trans, ...);
        // constructor: this._stores = [trans.objectStore("replyChains")]  ✓
        //              _indexStoreMap NOT populated                        ✗
    })
  // M1 exits → transaction transitions "active" → "inactive"

DbProvider._getStoreTransaction().then(trans => {  // microtask M2
    trans.getStore("replyChains")
      → this._trans.objectStore("replyChains_byMessageSearchKeys")
      //  ↑ throws InvalidStateError: transaction is no longer "active"
})

An IDB transaction is only "active" for the duration of the task or microtask in which it was created (or in which one of its request events fired). By the time getStore() is called in M2, the transaction has gone inactive and objectStore() throws InvalidStateError.

Even without any actual IDB requests being issued, the transaction can auto-commit once it goes inactive and the current event task completes with no pending requests — leaving it in a "done" state that also makes any subsequent objectStore() call throw.

Solution

Eagerly capture multi-entry/full-text index object stores in the IndexedDbTransaction constructor, in the same microtask (M1) as the db.transaction() call — while the transaction is guaranteed to still be "active".

// Eagerly populate _indexStoreMap in the constructor alongside _stores,
// so objectStore() is called while the transaction is still "active".
this._indexStoreMap = new Map<string, IDBObjectStore>();
if (_fakeComplicatedKeys) {
    each(this._transToken.storeNames, (storeName) => {
        const storeSchema = find(_schema.stores, (s) => s.name === storeName);
        if (storeSchema?.indexes) {
            each(storeSchema.indexes, (indexSchema) => {
                if (indexSchema.multiEntry || indexSchema.fullText) {
                    const indexStoreName = storeSchema.name + "_" + indexSchema.name;
                    this._indexStoreMap.set(indexStoreName, this._trans.objectStore(indexStoreName));
                }
            });
        }
    });
}

getStore() is updated to look up from this map rather than calling this._trans.objectStore() lazily.

@jell0wed jell0wed self-assigned this Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant