Friday, October 12, 2012

How not to design a IndexedDB HTML5 API

Today I'm playing a little with the IndexedDB API and I was a bit confused about how it has been designed.
The antecedents: Since the IndexedDB API is asyncronous, when you call to a method, for example open(), it won't be blocked waiting for the results of the function. Instead, you will get a request object, where you can associate callbacks to receive errors, results, or other kind of events. You can see here how it is done:
  1. var request = indexedDB.open("TestIndexed", 1);  
  2. request.onsuccess = function(evt) {
  3.     console.log("db sucessfully opened");
  4.     db = request.result;                                                            
  5. };
As you can see, we call to the open() method, immediately it returns a request object where we can install an onsuccess callback that will be called if the open operation was successfully completed. What confused me was that if the open() method send an event before you has been installed the callback you will lose the event. We can argue that this shouldn't ocurr because if you install the event just after the method call, you will stay almost sure that the database operation will spend more time that assign the callbacks.
I agree with that, but anyway I don't like that my API interface works only because I suppose some methods will take more time than others.

The problem is that if you put some code between the database open() and the callbacks, you will get in trouble. Every browser will take more or less time on the open() operation. For example, on my firefox browser took arround 70 miliseconds and in chrome took arround 120 milisecs. So, if you have something like this:
  1. var request = indexedDB.open("TestIndexed", 1);  
  2. foo();
  3. request.onsuccess = function (evt) {
  4.     db = request.result;                                                            
  5. };
Depending the time take the foo() method, this code might or might not work on a specific browser.
I know it hasn't sense do something like that. But for that reason, the design of the API should reinforce you to prepare all your data and events before the method call. It is really ugly prepare your events after calling the method. A better approach to this scenario could be:

  1. var request = indexedDB.open("TestIndexed", 1)
  2. request.onsuccess = function(evt) {
  3.     console.log("db sucessfully opened");
  4.     db = request.result;                                                            
  5. };
  6. request.onerror = function(evt) {
  7.     console.log("error");                                                      
  8. };
  9. request.perform() // This invoke the operation
Or even better:

  1. var callbacks = {};
  2. callbacks.onsuccess = function (evt) {
  3.     db = request.result;                                                            
  4. };
  5. indexedDB.open("TestIndexed", 1, callbacks);  
In this case you are being forced to define your callbacks before calling the method, so you will never lose any event, and it is more clear, elegant and follow the same convention that others functions in  Javascript.

In order to solve that issue, I'm using this class to call to IndexedDB API methods:
  1. var AsyncCall = {
  2.     call: function(request, callbacks) {
  3.         for(var c in callbacks) {
  4.             request[c] = callbacks[c];
  5.         }
  6.     }
  7. };
Now, we can use the IndexedDB API as follow:
  1. AsyncCall.call(indexedDB.open("TestIndexed", 1), {
  2.     onsuccess: function(evt) {
  3.         console.log("ok");
  4.     },
  5.     onerror: function(evt) {
  6.         console.log("error");
  7.     }
  8. });