2

UPDATE I discovered the issue is that it's blocked. Despite the database always being created and upgraded by the same extension, it does not get closed. So now I'm getting the "onblocked" function called.

How do I "unblock" currently blocked databases? And how do I prevent this in the future? This is an app, so no tabs are using it. And since I can't open those databases to even delete them (this also gets blocked), how do I close them?

(For anyone wondering, to avoid this issue from the start, you HAVE to do the folllowing:)

mydb.onversionchange = function(event) {
    mydb.close();
};

Original Post

IndexedDB dies and becomes unopenable if I (accidentally) try to open and upgrade with the wrong version. As far as I can tell, there's no way to ask indexedDB for the latest version of a DB. So if I try to run the following code twice, it destroys the database and it becomes unopenable:

And it never throws an error or calls onerror. It just sits silently

var db = null;

//Note, no version passed in, so the second time I do this, it seems to cause an error
var req = indexedDB.open( "test" );
req.onsuccess = function(event) { console.log( "suc: " + event.target.result.version ); db = event.target.result; };
req.onerror = function(event) { console.log( "err: " + event ); };
req.onupgradeneeded = function(event) { console.log( "upg: " + event.target.result.version ); };

//We're doing in interval since waiting for callback
var intv = setInterval(
    function()
    {
        if ( db === null ) return;

        clearInterval( intv );

        var req2 = indexedDB.open( "test", db.version + 1 );
        req2.onsuccess = function(event) { console.log( "suc: " + event.target.result.version ); };
        req2.onerror = function(event) { console.log( "err: " + event ); };
        req2.onupgradeneeded = function(event) { console.log( "upg: " + event.target.result.version ); };
    },
    50
);

All of that code is in my chrome.runtime.onInstalled.addListener. So when I update my app, it calls it again. If I call indexedDB.open( "test" ) without passing in the new version and then again run the setInterval function, it causes everything to become unusable and I'm never able to open "test" again. This would be solved if I could query indexedDB for the version of a database prior to attempting to open it. Does that exist?

Don Rhummy
  • 24,730
  • 42
  • 175
  • 330

2 Answers2

1

Maybe this helps?

function getVersion(callback) {
  var r = indexedDB.open('asdf');
  r.onblocked = r.onerror = console.error;
  r.onsuccess = function(event) {
    event.target.result.close();
    callback(event.target.result.version);      
  };
}

getVersion(function(version) {
  console.log('The version is: %s', version);
});

Ok, based on the convo, this little util function might set you on the path:

var DATABASE_NAME_CONSTANT = 'whatever';

// Basic indexedDB connection helper
// @param callback the action to perform with the open connection
// @param version the version of the database to open or upgrade to
// @param upgradeNeeded the callback if the db should be upgraded
function connect(callback, version, upgradeNeeded) {
   var r = indexedDB.open(DATABASE_NAME_CONSTANT, version);
   if(upgradeNeeded) r.onupgradeneeded = updateNeeded;
   r.onblocked = r.onerror = console.error;
   r.onsuccess = function(event) {
     console.log('Connected to %s version %s', 
       DATABASE_NAME_CONSTANT, version);
     callback(event.target.result);
   };
}

// Now let us say you needed to connect
// and need to have the version be upgraded
// and need to send in custom upgrades based on some ajax call

function fetch() {
  var xhr = new XMLHttpRequest();
  // ... setup the request and what not
  xhr.onload = function(event) {
    // if response is 200 etc
    // store the json in some variable
    var responseJSON = ...;

    console.log('Fetched the json file successfully');
    // Let's suppose you send in version and updgradeNeeded
    // as properties of your fetched JSON object
    var targetVersion = responseJSON.idb.targetVersion;
    var upgradeNeeded = responseJSON.idb.upgradeNeeded;

    // Now connect and do whatever
    connect(function(db) {
      // Do stuff with the locally scoped db variable
      // For example, grab a prop from the fetched object
      db.objectStore('asdf').put(responseJSON.recordToInsert);

      // If you feel the need, but should not, close the db
      db.close();
      console.log('Finished doing idb stuff');
    }, targetVersion, upgradeNeeded);
  }
}
Josh
  • 17,834
  • 7
  • 50
  • 68
  • It never calls `onsuccess`. It only calls `onblocked`, and in there this results in an error: `DOM IDBDatabase Exception 11 ` – Don Rhummy Jun 11 '13 at 02:51
  • Trying to clarify then, the above is printing the 'blocked' error to the console even when you do not pass in the version parameter to indexedDB.open? – Josh Jun 11 '13 at 03:03
  • Also, review http://stackoverflow.com/questions/11898375/how-to-get-objectstore-from-indexeddb/16379163#16379163 . It may help you. Avoid using a global db variable. Only access from the callback. This will likely resolve your blocking issue. The designers of indexedDB use the callback pattern to allow for async function calls, so trying to use a global db variable is just going to cause you headaches. – Josh Jun 11 '13 at 03:07
  • to your first question: yes, even without the version param, it throws the error. – Don Rhummy Jun 11 '13 at 04:59
  • To your second idea, that's a thought. Everything I've read (all the tutorials and intros) on IndexedDB recommends saving a reference to the DB. The link you posted shows the error is because of the global DB being null, but that wouldn't happen in my case since I'm checking it in an interval before calling the update function. I only update if I have a valid `db`. The reason why saving `db` to a global variable can be good is sometimes the `indexedDB.open(..)` call can take 1+ seconds to call the `onsuccess` callback. – Don Rhummy Jun 11 '13 at 05:00
  • I could be wrong, but so far it appears that as long as I check that my global `db` is not null and I call `mydb.onversionchange = function(event) { mydb.close(); };` it is OK. I'll continue to monitor, but that appears to fix the issue. – Don Rhummy Jun 11 '13 at 05:41
  • You should try it without the global. It will solve the blocking problem, and you do not need to check for null/closed/etc or use setTimeout/setInterval tricks. Also, onversionchange is deprecated. – Josh Jun 11 '13 at 15:33
  • does that mean that everytime I want to use the db, I'll need to again call the asynchronous `open()`? – Don Rhummy Jun 11 '13 at 17:12
  • Generally, yes. You are generally not penalized for it. You just need to use a callback pattern. If you hate the callback pattern so much, I would recommend learning about promises. For example, look at Google Closure's source and its use of the dojo promises object in connection with indexedDB. Or something like https://github.com/axemclion/jquery-indexeddb – Josh Jun 12 '13 at 13:06
0

I think it is best to provide the version number always. If you don't how are you going to manage upgrades on the db structure? If you don't its a good chance you will get in a situation where same db versions on a client will have an other database structure, and I don't think that is the thing you want. So I would suggest to keep the version number in a variable.

Also when working with indexeddb you will have to provide an upgrade plan from al previous versions to the current. Meaning version 4 has a certain structure, but you will have to be able to get that same structure from scratch as from version 1,2 and 3

Kristof Degrave
  • 4,142
  • 22
  • 32
  • how do you suggest saving that version number across chrome restarts? – Don Rhummy Jun 11 '13 at 05:42
  • Yes, but chrome won't save that across restarts. Imagine that my app can connect to a server to check for new data and changes to the data schema (via ajax). If it finds that, it will change the schema and update the version. Saving that info into a variable in javascript won't (*I think*) exist across chrome restarts. Will it? – Don Rhummy Jun 11 '13 at 05:49
  • If that is the case, I would advice you either to keep track of all the open connections, so you can close them. Or close the connection every time an operation is done. That is the way I build my lib linq2indexeddb – Kristof Degrave Jun 11 '13 at 05:52
  • if you call `db.close()` does it require calling asynchronous `db.open()` again to run any transfusions? (I assume it does but wanted to be sure) – Don Rhummy Jun 11 '13 at 06:39
  • No it doesn't, you can call it in the complete event of your transaction. txn.oncomplete = function (e){ e.target.close();} – Kristof Degrave Jun 11 '13 at 09:04
  • no, I meant, for the next transaction, do I need to call asynchronous open again? – Don Rhummy Jun 11 '13 at 14:30
  • Yes, you will have to. Another option is to keep track of all your existing connections, close them when you want to op the database with a higher version. But in all cases after you upgraded, you will have to make a new connection, or reuse the connection you made to the higher version – Kristof Degrave Jun 12 '13 at 05:29
  • Thank you. Is there a way to find all the open connections, or do I need to keep track of them manually? Also, will a simple call to `db.close()` close all open connections (My app is a singleton) or does it need to be done for each transaction? – Don Rhummy Jun 12 '13 at 05:47
  • You will have to do it your self, also I would let connections open if you don't keep track of them. That will result in memory leaks because the connections stay open and won't get used anymore – Kristof Degrave Jun 12 '13 at 06:03
  • How do you keep track of open connections? Isn't it just the `db` object that I've cached? Won't closing that be all I need to close? Or do I need to keep track of every onsuccess return from a transaction or open call (i.e. each is a different open connection)? – Don Rhummy Jun 12 '13 at 06:16
  • You can do this by saving the result of opening a dbconnection to a variable. for example var db; indexeddb.open("").onsuccess = function (e) { var db = e.target.result; }. If you have multiple you can add it to an array. – Kristof Degrave Jun 12 '13 at 07:12
  • Why would you keep opening it over and over instead of reusing the first saved `db`? Wouldn't that be more efficient and use less memory? – Don Rhummy Jun 12 '13 at 16:02
  • Yes it would, but as I said in that case you need to keep track on it and need to close all calls before you can upgrade. Believe me the cost for opening a now connection isn't very high. – Kristof Degrave Jun 13 '13 at 05:49