You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何可靠检测IndexedDB事务是否处于活跃状态?

问题描述

我正在实现一个简易IndexedDB管理器,目标是在必要时自动创建新事务,简化操作接口。目前我把事务的oncomplete回调作为事务不可再用的标识,在get、add、put等方法开始时创建新事务。多数情况下这个方法可行,但偶尔会抛出事务不活跃的异常。

我做了一个复现场景:给输入框绑定input事件监听器,将输入框当前值保存到IndexedDB。快速输入时,控制台有时会抛出异常,且最后输入的值无法保存。比如同时按下1和2后,控制台输出如下:

dbManager.js:56 put 1
dbManager.js:47 Ensuring the store is valid
dbManager.js:18 Generating new store
dbManager.js:56 put 12
dbManager.js:47 Ensuring the store is valid
dbManager.js:67 put - catch 12
dbManager.js:60 Uncaught (in promise) DOMException: Failed to execute 'put' on 'IDBObjectStore': The transaction is not active.
                  at (...)/lib/dbManager.js:60:44
                  at new Promise (<anonymous>)
                  at MyStore.put ((...)/lib/dbManager.js:58:16)
                  at HTMLInputElement.putData ((...)/lib/script.js:5:11)
dbManager.js:62 put - onsuccess 1
dbManager.js:27 Transaction completed

可以看到,transaction is not active异常在事务的oncomplete回调触发前就抛出了,这个问题在Chrome、Opera、Edge和Firefox中都会出现。

我该如何解决这个问题?有没有其他方法检测事务是否活跃?我想到一个临时方案是把请求推入FIFO队列,按顺序执行并等待每个请求完成,但这可能导致每个请求都要创建单独的事务。


问题代码

lib/dbManager.js

class MyIndexedDb {
    async connect(name, upgrade, version) {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(name, version);
            request.onupgradeneeded = (e) => upgrade(request.result, e);
            request.onsuccess = () => {
                this.database = request.result;
                this.database.onerror = (e) => console.error(e);
                resolve();
            };
            request.onerror = () => reject(request.error);
        });
    }


    getStore = (storeName, mode) => {
        const idbStore = new MyStore(() => {
            console.log("Generating new store");
            if (!this.database) {
                throw ("Database is not defined.");
            }

            const transaction = this.database.transaction(storeName, mode);
            const store = transaction.objectStore(storeName);

            transaction.oncomplete = () => {
                console.log("Transaction completed");
                idbStore.invalidateTransaction();
            }
            return store;
        });
        return idbStore;
    }
}

class MyStore {
    constructor(storeGenerator) {
        this.storeGenerator = storeGenerator;
        this.shouldRegenerateStore = true;
    }

    invalidateTransaction = () => {
        this.shouldRegenerateStore = true;
    }

    ensureValidStore = () => {
        console.log("Ensuring the store is valid");
        if (!this.shouldRegenerateStore) {
            return;
        }
        this.shouldRegenerateStore = false;
        this.store = this.storeGenerator();
    }

    put = async (key, value) => {
        console.log("put", value);
        this.ensureValidStore();
        return new Promise((resolve, reject) => {
            try {
                const request = this.store.put(value, key);
                request.onsuccess = () => {
                    console.log("put - onsuccess", value);
                    return resolve(request.result);
                }
            }
            catch (e) {
                console.log("put - catch", value);
                reject(e);
                return;
            }
        });
    }
}

lib/script.js

let store, input;

function putData() {
  try {
    store.put("key", input.value);
  } catch (e) {
    console.error(e);
  }
}

function upgradeDb(db) {
    db.createObjectStore("store");
}

async function load() {
  const idb = new MyIndexedDb();
  try {
      await idb.connect("db", upgradeDb);
  } catch (e) {
      console.error(e);
      return;
  }

  store = idb.getStore("store", "readwrite");

  input = document.getElementById("input");
  input.addEventListener("input", putData);
}

load();

index.html

<!doctype html>
<html>
  <head>
    <script defer src="lib/dbManager.js"></script>
    <script defer src="lib/script.js"></script>
  </head>
  <body>
    <input id="input"/>
  </body>
</html>

解决方案

核心问题分析

问题出在事务的生命周期判断时机:IndexedDB的事务可能在oncomplete回调触发前就进入非活跃状态。当你快速发起第二个put请求时,第一个事务已经因为没有新请求而被浏览器自动关闭,但oncomplete回调还没来得及执行,此时shouldRegenerateStore仍然是false,导致复用了已经失效的事务。

解决方法

1. 捕获异常并自动重试

既然无法提前精准判断事务是否活跃,最简单的方案是在执行操作时捕获TransactionInactiveError,捕获到异常后重新生成事务并重试请求:

修改MyStore的put方法:

put = async (key, value) => {
    console.log("put", value);
    let attempt = 0;
    const maxAttempts = 2; // 最多重试一次
    while (attempt < maxAttempts) {
        try {
            this.ensureValidStore();
            return await new Promise((resolve, reject) => {
                const request = this.store.put(value, key);
                request.onsuccess = () => {
                    console.log("put - onsuccess", value);
                    resolve(request.result);
                };
                request.onerror = () => reject(request.error);
            });
        } catch (e) {
            if (e.name === "TransactionInactiveError" && attempt < maxAttempts - 1) {
                console.log("Transaction inactive, retrying put", value);
                this.shouldRegenerateStore = true; // 标记需要重新生成事务
                attempt++;
            } else {
                console.log("put - catch", value);
                throw e;
            }
        }
    }
};

2. 监听所有事务结束事件

除了oncomplete,还要监听事务的onerror和onabort事件,确保任何导致事务结束的情况都能标记shouldRegenerateStore为true:

修改getStore中的事务事件绑定:

const transaction = this.database.transaction(storeName, mode);
const store = transaction.objectStore(storeName);

transaction.oncomplete = () => {
    console.log("Transaction completed");
    idbStore.invalidateTransaction();
};
transaction.onerror = () => {
    console.log("Transaction errored");
    idbStore.invalidateTransaction();
};
transaction.onabort = () => {
    console.log("Transaction aborted");
    idbStore.invalidateTransaction();
};

3. 使用请求队列合并操作(推荐)

你的临时方案思路可优化:不是每个请求单独创建事务,而是将短时间内的多个请求合并到同一个事务中,既保证顺序执行,又减少事务创建开销。

修改MyStore类,添加请求队列:

class MyStore {
    constructor(storeGenerator) {
        this.storeGenerator = storeGenerator;
        this.shouldRegenerateStore = true;
        this.requestQueue = [];
        this.isProcessingQueue = false;
    }

    invalidateTransaction = () => {
        this.shouldRegenerateStore = true;
    }

    ensureValidStore = () => {
        if (!this.shouldRegenerateStore) {
            return;
        }
        this.shouldRegenerateStore = false;
        this.store = this.storeGenerator();
    }

    // 处理队列的核心方法
    processQueue = async () => {
        if (this.isProcessingQueue) return;
        this.isProcessingQueue = true;

        while (this.requestQueue.length > 0) {
            const { operation, resolve, reject } = this.requestQueue.shift();
            try {
                this.ensureValidStore();
                const result = await new Promise((res, rej) => {
                    const request = operation(this.store);
                    request.onsuccess = () => res(request.result);
                    request.onerror = () => rej(request.error);
                });
                resolve(result);
            } catch (e) {
                if (e.name === "TransactionInactiveError") {
                    this.shouldRegenerateStore = true;
                    // 重新加入队列头部重试
                    this.requestQueue.unshift({ operation, resolve, reject });
                } else {
                    reject(e);
                }
            }
        }

        this.isProcessingQueue = false;
    }

    put = async (key, value) => {
        console.log("put", value);
        return new Promise((resolve, reject) => {
            this.requestQueue.push({
                operation: (store) => store.put(value, key),
                resolve,
                reject
            });
            this.processQueue();
        });
    }
}

这个方案的优势:

  • 自动合并短时间内的多个请求到同一个事务,提升性能
  • 确保请求按顺序执行,避免并发冲突
  • 遇到事务失效时自动重试,无需手动处理

最终效果

使用队列方案后,快速输入时所有请求都会被正确处理,不会再出现事务不活跃的异常,同时也能复用事务减少开销。


内容的提问来源于stack exchange,提问作者Jakub Janowski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 16:16:06