Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TransactionInactiveError: Failed to execute 'get' on 'IDBObjectStore': The transaction is inactive or finished

This seems to be a Safari only bug. It does not occur in Chrome as far as I can tell. I have a very standard IndexedDB setup. I call initDb, save the result, and that provides me a nice way to make calls to the DB.

var initDb = function() {
    // Setup DB. whenDB is a promise we use before executing any DB requests so we know the DB is fully set up.
    parentDb = null;
    var whenDb = new Promise(function(resolve, reject) {
        var DBOpenRequest = window.indexedDB.open('groceries');
        DBOpenRequest.onsuccess = function(event) {
            parentDb = DBOpenRequest.result;
            resolve();
        };
        DBOpenRequest.onupgradeneeded = function(event) {
            var localDb = event.target.result;
            localDb.createObjectStore('unique', {
                keyPath: 'id'
            });
        };
    });

    // makeRequest needs to return an IndexedDB Request object.
    // This function just wraps that in a promise.
    var request = function(makeRequest, key) {
        return new Promise(function(resolve, reject) {
            var request = makeRequest();
            request.onerror = function() {
                reject('Request error');
            };
            request.onsuccess = function() {
                if (request.result == undefined) {
                    reject(key + ' not found');
                } else {
                    resolve(request.result);
                }
            };
        });
    };

    // Open a very typical transaction
    var transact = function(type, storeName) {
        // Make sure DB is set up, then open transaction
        return whenDb.then(function() {
            var transaction = parentDb.transaction([storeName], type);
            transaction.oncomplete = function(event) {
                console.log('transcomplete')
            };
            transaction.onerror = function(event) {
                console.log('Transaction not opened due to error: ' + transaction.error);
            };
            return transaction.objectStore(storeName);
        });
    };

    // Shortcut function to open transaction and return standard Javascript promise that waits for DB query to finish
    var read = function(storeName, key) {
        return transact('readonly', storeName).then(function(transactionStore) {
            return request(function() {
                return transactionStore.get(key);
            }, key);
        });
    };

    // A test function that combines the previous transaction, request and read functions into one.
    var test = function() {
        return whenDb.then(function() {
            var transaction = parentDb.transaction(['unique'], 'readonly');
            transaction.oncomplete = function(event) {
                console.log('transcomplete')
            };
            transaction.onerror = function(event) {
                console.log('Transaction not opened due to error: ' + transaction.error);
            };
            var store = transaction.objectStore('unique');
            return new Promise(function(resolve, reject) {
                var request = store.get('groceryList');
                request.onerror = function() {
                    console.log(request.error);
                    reject('Request error');
                };
                request.onsuccess = function() {
                    if (request.result == undefined) {
                        reject(key + ' not found');
                    } else {
                        resolve(request.result);
                    }
                };
            });
        });
    };

    // Return an object for db interactions
    return {
        read: read,
        test: test
    };
};
var db = initDb();

When I call db.read('unique', 'test') in Safari I get the error:

TransactionInactiveError: Failed to execute 'get' on 'IDBObjectStore': The transaction is inactive or finished

The same call in Chrome gives no error, just the expected promise returns. Oddly enough, calling the db.test function in Safari works as expected as well. It literally seems to be that the separation of work into two functions in Safari is somehow causing this error.

In all cases transcomplete is logged AFTER either the error is thrown (in the case of the Safari bug) or the proper value is returned (as should happen). So the transaction has NOT closed before the error saying the transaction is inactive or finished is thrown.

Having a hard time tracking down the issue here.

like image 403
Henry Avatar asked Mar 07 '23 00:03

Henry


1 Answers

Hmm, not confident in my answer, but my first guess is the pause that occurs between creating the transaction and starting a request allows the transaction to timeout and become inactive because it finds no requests active, such that a later request that does try to start is started on an inactive transaction. This can easily be solved by starting requests in the same epoch of the javascript event loop (the same tick) instead of deferring the start of a request.

The error is most likely in these lines:

var store = transaction.objectStore('unique');
return new Promise(function(resolve, reject) {
   var request = store.get('groceryList');

You need to create the request immediately to avoid this error:

var store = transaction.objectStore('unique');
var request = store.get('groceryList');

One way to solve this might be simply to approach the code differently. Promises are intended to be composable. Code that uses promises generally wants to return control to the caller, so that the caller can control the flow. Some of your functions as they are currently written violate this design pattern. It is possible that by simply using a more appropriate design pattern, you will not run into this error, or at least you will be able to identify the problem more readily.

An additional point would be your mixed use of global variables. Variables like parentDb and db are just going to potentially cause problems on certain platforms unless you really are an expert at async code.

For example, start with a simple connect or open function that resolves to an open IDBDatabase variable.

function connect(name) {
  return new Promise(function(resolve, reject) {
    var openRequest = indexedDB.open(name);
    openRequest.onsuccess = function() {
      var db = openRequest.result;
      resolve(db);
    };
  });
}

This will let you easily compose an open promise together with code that should run after it, like this:

connect('groceries').then(function(db) {
  // do stuff with db here
});

Next, use a promise to encapsulate an operation. This is not a promise per request. Pass along the db variable instead of using a global one.

function getGroceryList(db, listId) {
   return new Promise(function(resolve, reject) {
     var txn = db.transaction('unique');
     var store = txn.objectStore('unique');
     var request = store.get(listId);
     request.onsuccess = function() {
        var list = request.result;
        resolve(list);
     };
     request.onerror = function() {
        reject(request.error);
     };
   });
}

Then compose it all together

connect().then(function(db) {
  return getGroceryList(db, 'asdf');
}).catch(error);
like image 88
Josh Avatar answered Apr 06 '23 08:04

Josh