Search code examples
javascriptcross-browserindexeddb

indexedDB onupgradeneeded event never finishes


In my app a user should be able to dynamically create and delete objectStores. Thanks to indexedDB this operations (why in the world) are just allowed in an onupgradeneeded.

After an onupgradedneeded is triggered indexedDB never finish this request and gc never collects this connection.

There are only problems at creating and deleting stores in an existing db. The onupdateneeded at the beginning (create db and create first store) never makes any problems.

I try to close every db connection before I do an upgrade because there is a bug report for Chromium from 2013 where this problem was mentioned where this was suggested (marked as fixed: https://bugs.chromium.org/p/chromium/issues/detail?id=242115).

The fact that I haven't found any actual information about this problem leads me to the assumption that I was doing sth wrong here in my code :) But then why do some browsers just do their work ?

This is my code for reproducing this error:

db.js

// Init IndexedDB
var dbversion;
var oStore;
var dbname = "UserName1"; // == Username
initDB();


function initDB() {
    window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
    window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
    window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange

    if (!indexedDB) {
       window.alert("Datenbanksupport im Browser fehlt !")
    }else{
        console.log("indexedDB: Supported !");
        // >> first visit ?
        var vCheck = indexedDB.open(dbname);
        vCheck.onerror = errorHandler;
        vCheck.onversionchange = function(ev) {
            vCheck.close();
        };
        vCheck.onsuccess = function(event){
            var db = vCheck.result;
            dbversion = parseInt(localStorage.getItem("dbversion"));
            if (!dbversion){
                // -- maybe
                dbversion = parseInt(db.version);
                if (dbversion <= 1){
                    // -- definitely
                    var needUpgrade = false;
                    db.close();
                    var request = indexedDB.open(dbname, dbversion+1);
                    request.onerror = errorHandler;
                    request.onupgradeneeded = function(event){
                        console.log("indexedDB: start upgrade");
                        var nextDB = request.result;
                        if (!nextDB.objectStoreNames.contains('account')) {
                            nextDB.createObjectStore('account', {keyPath: "id", autoIncrement: true});
                        }
                        dbversion += 1;
                        localStorage.setItem("dbversion", dbversion);
                        needUpgrade = true;
                        console.log("indexedDB: upgrade finished");
                    }
                    request.onsuccess = function(event){
                        if (needUpgrade) {
                            var objectStore = event.target.result.transaction(['account'], "readwrite").objectStore("account");
                            row = {
                                'id' : 0,
                                'username' : dbname,
                            };
                            objectStore.add(row);
                        }
                        console.log("indexedDB: init");
                    };
                    request.oncomplete = function(event){request.result.close();}
                }else{
                    // -- no
                    localStorage.setItem("dbversion", dbversion);
                    vCheck.oncomplete = console.log("indexedDB: dbversion unknown (not in localStorage)");
                }
            }else{
                // -- no
                console.log("indexedDB: version", dbversion);
                console.log("indexedDB: init");
            }
        }
    }
}

function listStores() {
    var request = indexedDB.open(dbname, dbversion);
    request.onerror = errorHandler;
    request.onsuccess = function(event){
        var db = request.result;
        var allStores = db.objectStoreNames;
        console.log(allStores);
    }
    request.oncomplete = function(event){request.result.close();}
}

function addStore(storeName) {
    dbversion = localStorage.getItem("dbversion");
    var request = indexedDB.open(dbname, dbversion);
    request.onerror = errorHandler;
    request.onsuccess = function(event){
        var db = request.result;
        if (!db.objectStoreNames.contains(storeName)) {
            dbversion = parseInt(dbversion) + 1
            localStorage.setItem("dbversion", dbversion);
            db.close()
            var nextRequest = indexedDB.open(dbname, dbversion);
            nextRequest.onversionchange = function(ev) {
                nextRequest.close();
            };
            nextRequest.onupgradeneeded = function(event) {
                console.log("indexedDB: creating");
                var nextDB = nextRequest.result;
                nextDB.createObjectStore(storeName, {keyPath: "id", autoIncrement: true});
                oStore = storeName;
                console.log("indexedDB:", storeName, "created");
            }
            nextRequest.onerror = errorHandler;
            nextRequest.oncomplete = function(event){nextRequest.result.close();}
        }else{
            oStore = storeName;
            console.log("indexedDB:", storeName, "already exists. Do nothing...");
        }
    }
}

function selectStore(storeName){
    oStore = storeName;
    console.log("indexedDB: globalVar oStore ==", oStore);
}

function addMember(data) {
    var request = indexedDB.open(dbname, dbversion);
    request.onerror = errorHandler;
    request.onsuccess = function(event){
        var db = request.result;
        var objectStore = db.transaction([oStore], "readwrite").objectStore(oStore);
        row = {
            'id' : Date.now(),
            'name' : {
                'vname' : data['vname'],
                'nname' : data['nname'],
            },
        }
        objectStore.add(row);
        console.log("indexedDB:", row["id"], "inserted");
    }
    request.oncomplete = function(event){request.result.close();}
    return;
}

function deleteStore(storeName) {
    dbversion = localStorage.getItem("dbversion");
    dbversion = parseInt(dbversion) + 1
    localStorage.setItem("dbversion", dbversion);
    var request = indexedDB.open(dbname, dbversion);
    request.onerror = errorHandler;
    request.onupgradeneeded = function(event){
        var db = request.result;
        db.deleteObjectStore(storeName);
        console.log("indexedDB:", storeName, "deleted");
    }
    request.oncomplete = function(event){request.result.close();}
}

function errorHandler(event) {
    console.log("indexedDB: operation went wrong:");
    console.log("indexedDB:", event);
    return;
}

call this order of functions

listStores()
addStore('TestStore')
selectStore('TestStore')
addMember({'vname':'John', 'nname':'Doe'})
deleteStore('TestStore')
listStores() // <<<<<<<<<<<< hangs up !

EDIT:

As Joshua turns out the place of closing the connection was wrong. In addition there is one very important fact:

Before you can delete an objectStore quickly you have to clear it first !

here is the right deleteStore()

function deleteStore(storeName) {
    // First: Clear the store
    var request1 = indexedDB.open(dbname, dbversion);
    request1.onsuccess = function(event){
        var connection = event.target.result;

        var objectStore = connection.transaction([storeName], "readwrite").objectStore(storeName);
        var result_clear = objectStore.clear();
        result_clear.onerror = errorHandler;
        result_clear.onsuccess = function(event){
            console.log("indexedDB: clearing Store...");
            // Second: Delete the store
            dbversion += 1;
            localStorage.setItem("dbversion", dbversion);
            var request2 = indexedDB.open(dbname, dbversion);
            request2.onsuccess = function(event){
                var connection2 = event.target.result;

                // ---> Garbage Collection
                connection2.onversionchange = function(event) {
                    connection2.close();
                };

            }
            request2.onerror = errorHandler;
            request2.onupgradeneeded = function(event){
                var connection2 = event.target.result;
                connection2.deleteObjectStore(storeName);
                console.log("indexedDB:", storeName, "deleted");
            }
        }

        // ---> Garbage Collection
        connection.onversionchange = function(event) {
            connection.close();
        };

    }
    request1.onerror = errorHandler;
    request1.oncomplete = function(e){
    }
}

Solution

  • Ensure that you're watching for a versionchange event fired against the connection, not the open request.

    var openRequest = indexedDB.open(name, version);
    openRequest.onblocked = function(e) {
      // Another connection is open, preventing the upgrade,
      // and it didn't close immediately.
    };
    openRequest.onerror = function(e) {
      // Open failed - possibly the version is higher than requested.
    };
    openRequest.onupgradeneeded = function(e) {
      // Check the current version, and upgrade as needed.
      var connection = openRequest.result;
      if (e.oldVersion < 1) {
        // db is new - create v1 schema
      }
      if (e.oldVersion < 2) {
        // upgrade v1 to v2 schema
      }
      if (e.oldVersion < 3) {
        // upgrade v2 to v3, etc
      }
    };
    openRequest.onsuccess = function(e) {
      var connection = openRequest.result;
      connection.onversionchange = function(e) {
        // Close immediately to allow the upgrade requested by another
        // instance to proceed.
        connection.close();
      };
    
      // The connection is open - use it for stuff.
    };
    

    The above also demonstrates the usual pattern for versioning - your code requests a specific version on open, and upgrades older schemas if needed. When your application needs a new store you increase the version number and introduce an additional step into the upgradeneeded handler.