Bug 871445 - patch 3 - DataStore: getChanges + revisionID, r=ehsan, sr=mounir, r=bent
☠☠ backed out by 5f6343a6b656 ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 02 Oct 2013 11:30:26 -0400
changeset 149706 cab0ddcff382acfafe5d734e74f678e8ba018043
parent 149705 100f3ba430f991285fc1d489fcec0d8c416f166b
child 149707 0e76a4134db27848b7631df48f4aa531348c461b
push id25401
push userphilringnalda@gmail.com
push dateThu, 03 Oct 2013 14:59:30 +0000
treeherdermozilla-central@51b36c5fd45f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, mounir, bent
bugs871445
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 871445 - patch 3 - DataStore: getChanges + revisionID, r=ehsan, sr=mounir, r=bent
dom/base/IndexedDBHelper.jsm
dom/datastore/DataStore.jsm
dom/datastore/DataStoreDB.jsm
dom/datastore/DataStoreService.js
dom/datastore/tests/Makefile.in
dom/datastore/tests/test_revision.html
--- a/dom/base/IndexedDBHelper.jsm
+++ b/dom/base/IndexedDBHelper.jsm
@@ -107,19 +107,27 @@ IndexedDBHelper.prototype = {
    *        Success callback to call on a successful transaction commit.
    *        The result is stored in txn.result.
    * @param failureCb
    *        Error callback to call when an error is encountered.
    */
   newTxn: function newTxn(txn_type, store_name, callback, successCb, failureCb) {
     this.ensureDB(function () {
       if (DEBUG) debug("Starting new transaction" + txn_type);
-      let txn = this._db.transaction(this.dbStoreNames, txn_type);
+      let txn = this._db.transaction(Array.isArray(store_name) ? store_name : this.dbStoreNames, txn_type);
       if (DEBUG) debug("Retrieving object store", this.dbName);
-      let store = txn.objectStore(store_name);
+      let stores;
+      if (Array.isArray(store_name)) {
+        stores = [];
+        for (let i = 0; i < store_name.length; ++i) {
+          stores.push(txn.objectStore(store_name[i]));
+        }
+      } else {
+        stores = txn.objectStore(store_name);
+      }
 
       txn.oncomplete = function (event) {
         if (DEBUG) debug("Transaction complete. Returning to callback.");
         if (successCb) {
           successCb(txn.result);
         }
       };
 
@@ -132,17 +140,17 @@ IndexedDBHelper.prototype = {
         if (failureCb) {
           if (event.target.error) {
             failureCb(event.target.error.name);
           } else {
             failureCb("UnknownError");
           }
         }
       };
-      callback(txn, store);
+      callback(txn, stores);
     }.bind(this), failureCb);
   },
 
   /**
    * Initialize the DB. Does not call open.
    *
    * @param aDBName
    *        DB name for the open call.
--- a/dom/datastore/DataStore.jsm
+++ b/dom/datastore/DataStore.jsm
@@ -9,20 +9,30 @@
 var EXPORTED_SYMBOLS = ["DataStore"];
 
 function debug(s) {
   // dump('DEBUG DataStore: ' + s + '\n');
 }
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
+const REVISION_ADDED = "added";
+const REVISION_UPDATED = "updated";
+const REVISION_REMOVED = "removed";
+const REVISION_VOID = "void";
+
 Cu.import("resource://gre/modules/DataStoreDB.jsm");
 Cu.import("resource://gre/modules/ObjectWrapper.jsm");
 Cu.import('resource://gre/modules/Services.jsm');
 
+/* Helper function */
+function createDOMError(aWindow, aEvent) {
+  return new aWindow.DOMError(aEvent.target.error.name);
+}
+
 /* DataStore object */
 
 function DataStore(aAppId, aName, aOwner, aReadOnly, aGlobalScope) {
   this.appId = aAppId;
   this.name = aName;
   this.owner = aOwner;
   this.readOnly = aReadOnly;
 
@@ -30,106 +40,163 @@ function DataStore(aAppId, aName, aOwner
   this.db.init(aOwner, aName, aGlobalScope);
 }
 
 DataStore.prototype = {
   appId: null,
   name: null,
   owner: null,
   readOnly: null,
+  revisionId: null,
 
   newDBPromise: function(aWindow, aTxnType, aFunction) {
     let db = this.db;
     return new aWindow.Promise(function(aResolve, aReject) {
       debug("DBPromise started");
       db.txn(
         aTxnType,
-        function(aTxn, aStore) {
+        function(aTxn, aStore, aRevisionStore) {
           debug("DBPromise success");
-          aFunction(aResolve, aReject, aTxn, aStore);
+          aFunction(aResolve, aReject, aTxn, aStore, aRevisionStore);
         },
-        function() {
+        function(aEvent) {
           debug("DBPromise error");
-          aReject(new aWindow.DOMError("InvalidStateError"));
+          aReject(createDOMError(aWindow, aEvent));
         }
       );
     });
   },
 
-  getInternal: function(aWindow, aResolve, aReject, aStore, aId) {
+  getInternal: function(aWindow, aResolve, aStore, aId) {
     debug("GetInternal " + aId);
 
     let request = aStore.get(aId);
     request.onsuccess = function(aEvent) {
       debug("GetInternal success. Record: " + aEvent.target.result);
       aResolve(ObjectWrapper.wrap(aEvent.target.result, aWindow));
     };
-
-    request.onerror = function(aEvent) {
-      debug("GetInternal error");
-      aReject(new aWindow.DOMError(aEvent.target.error.name));
-    };
   },
 
-  updateInternal: function(aWindow, aResolve, aReject, aStore, aId, aObj) {
+  updateInternal: function(aResolve, aStore, aRevisionStore, aId, aObj) {
     debug("UpdateInternal " + aId);
 
+    let self = this;
     let request = aStore.put(aObj, aId);
     request.onsuccess = function(aEvent) {
       debug("UpdateInternal success");
-      // No wrap here because the result is always a int.
-      aResolve(aEvent.target.result);
-    };
-    request.onerror = function(aEvent) {
-      debug("UpdateInternal error");
-      aReject(new aWindow.DOMError(aEvent.target.error.name));
+
+      self.addRevision(aRevisionStore, aId, REVISION_UPDATED,
+        function() {
+          debug("UpdateInternal - revisionId increased");
+          // No wrap here because the result is always a int.
+          aResolve(aEvent.target.result);
+        }
+      );
     };
   },
 
-  addInternal: function(aWindow, aResolve, aReject, aStore, aObj) {
+  addInternal: function(aResolve, aStore, aRevisionStore, aObj) {
     debug("AddInternal");
 
+    let self = this;
     let request = aStore.put(aObj);
     request.onsuccess = function(aEvent) {
       debug("Request successful. Id: " + aEvent.target.result);
-      // No wrap here because the result is always a int.
-      aResolve(aEvent.target.result);
+      self.addRevision(aRevisionStore, aEvent.target.result, REVISION_ADDED,
+        function() {
+          debug("AddInternal - revisionId increased");
+          // No wrap here because the result is always a int.
+          aResolve(aEvent.target.result);
+        }
+      );
     };
-    request.onerror = function(aEvent) {
-      debug("AddInternal error");
-      aReject(new aWindow.DOMError(aEvent.target.error.name));
+  },
+
+  removeInternal: function(aResolve, aStore, aRevisionStore, aId) {
+    debug("RemoveInternal");
+
+    let self = this;
+    let request = aStore.get(aId);
+    request.onsuccess = function(aEvent) {
+      debug("RemoveInternal success. Record: " + aEvent.target.result);
+      if (aEvent.target.result === undefined) {
+        aResolve(false);
+        return;
+      }
+
+      let deleteRequest = aStore.delete(aId);
+      deleteRequest.onsuccess = function() {
+        debug("RemoveInternal success");
+        self.addRevision(aRevisionStore, aId, REVISION_REMOVED,
+          function() {
+            aResolve(true);
+          }
+        );
+      };
     };
   },
 
-  removeInternal: function(aResolve, aReject, aStore, aId) {
-    debug("RemoveInternal");
+  clearInternal: function(aResolve, aStore, aRevisionStore) {
+    debug("ClearInternal");
 
-    let request = aStore.delete(aId);
+    let self = this;
+    let request = aStore.clear();
     request.onsuccess = function() {
-      debug("RemoveInternal success");
-      aResolve();
-    };
-    request.onerror = function(aEvent) {
-      debug("RemoveInternal error");
-      aReject(new aWindow.DOMError(aEvent.target.error.name));
+      debug("ClearInternal success");
+      self.db.clearRevisions(aRevisionStore,
+        function() {
+          debug("Revisions cleared");
+
+          self.addRevision(aRevisionStore, 0, REVISION_VOID,
+            function() {
+              debug("ClearInternal - revisionId increased");
+              aResolve();
+            }
+          );
+        }
+      );
     };
   },
 
-  clearInternal: function(aResolve, aReject, aStore) {
-    debug("ClearInternal");
+  addRevision: function(aRevisionStore, aId, aType, aSuccessCb) {
+    let self = this;
+    this.db.addRevision(aRevisionStore, aId, aType,
+      function(aRevisionId) {
+        self.revisionId = aRevisionId;
+        aSuccessCb();
+      }
+    );
+  },
+
+  retrieveRevisionId: function(aSuccessCb) {
+    if (this.revisionId != null) {
+      aSuccessCb();
+      return;
+    }
 
-    let request = aStore.clear();
-    request.onsuccess = function() {
-      debug("ClearInternal success");
-      aResolve();
-    };
-    request.onerror = function(aEvent) {
-      debug("ClearInternal error");
-      aReject(new aWindow.DOMError(aEvent.target.error.name));
-    };
+    let self = this;
+    this.db.revisionTxn(
+      'readwrite',
+      function(aTxn, aRevisionStore) {
+        debug("RetrieveRevisionId transaction success");
+
+        let request = aRevisionStore.openCursor(null, 'prev');
+        request.onsuccess = function(aEvent) {
+          let cursor = aEvent.target.result;
+          if (!cursor) {
+            // If the revision doesn't exist, let's create the first one.
+            self.addRevision(aRevisionStore, 0, REVISION_VOID, aSuccessCb);
+            return;
+          }
+
+          self.revisionId = cursor.value.revisionId;
+          aSuccessCb();
+        };
+      }
+    );
   },
 
   throwInvalidArg: function(aWindow) {
     return aWindow.Promise.reject(
       new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id"));
   },
 
   throwReadOnly: function(aWindow) {
@@ -158,100 +225,200 @@ DataStore.prototype = {
       get: function DS_get(aId) {
         aId = parseInt(aId);
         if (isNaN(aId) || aId <= 0) {
           return self.throwInvalidArg(aWindow);
         }
 
         // Promise<Object>
         return self.newDBPromise(aWindow, "readonly",
-          function(aResolve, aReject, aTxn, aStore) {
-            self.getInternal(aWindow, aResolve, aReject, aStore, aId);
+          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+            self.getInternal(aWindow, aResolve, aStore, aId);
           }
         );
       },
 
       update: function DS_update(aId, aObj) {
         aId = parseInt(aId);
         if (isNaN(aId) || aId <= 0) {
           return self.throwInvalidArg(aWindow);
         }
 
         if (self.readOnly) {
           return self.throwReadOnly(aWindow);
         }
 
         // Promise<void>
         return self.newDBPromise(aWindow, "readwrite",
-          function(aResolve, aReject, aTxn, aStore) {
-            self.updateInternal(aWindow, aResolve, aReject, aStore, aId, aObj);
+          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+            self.updateInternal(aResolve, aStore, aRevisionStore, aId, aObj);
           }
         );
       },
 
       add: function DS_add(aObj) {
         if (self.readOnly) {
           return self.throwReadOnly(aWindow);
         }
 
         // Promise<int>
         return self.newDBPromise(aWindow, "readwrite",
-          function(aResolve, aReject, aTxn, aStore) {
-            self.addInternal(aWindow, aResolve, aReject, aStore, aObj);
+          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+            self.addInternal(aResolve, aStore, aRevisionStore, aObj);
           }
         );
       },
 
       remove: function DS_remove(aId) {
         aId = parseInt(aId);
         if (isNaN(aId) || aId <= 0) {
           return self.throwInvalidArg(aWindow);
         }
 
         if (self.readOnly) {
           return self.throwReadOnly(aWindow);
         }
 
         // Promise<void>
         return self.newDBPromise(aWindow, "readwrite",
-          function(aResolve, aReject, aTxn, aStore) {
-            self.removeInternal(aResolve, aReject, aStore, aId);
+          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+            self.removeInternal(aResolve, aStore, aRevisionStore, aId);
           }
         );
       },
 
       clear: function DS_clear() {
         if (self.readOnly) {
           return self.throwReadOnly(aWindow);
         }
 
         // Promise<void>
         return self.newDBPromise(aWindow, "readwrite",
-          function(aResolve, aReject, aTxn, aStore) {
-            self.clearInternal(aResolve, aReject, aStore);
+          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+            self.clearInternal(aResolve, aStore, aRevisionStore);
           }
         );
       },
 
+      get revisionId() {
+        return self.revisionId;
+      },
+
+      getChanges: function(aRevisionId) {
+        debug("GetChanges: " + aRevisionId);
+
+        if (aRevisionId === null || aRevisionId === undefined) {
+          return aWindow.Promise.reject(
+            new aWindow.DOMError("SyntaxError", "Invalid revisionId"));
+        }
+
+        // Promise<DataStoreChanges>
+        return new aWindow.Promise(function(aResolve, aReject) {
+          debug("GetChanges promise started");
+          self.db.revisionTxn(
+            'readonly',
+            function(aTxn, aStore) {
+              debug("GetChanges transaction success");
+
+              let request = self.db.getInternalRevisionId(
+                aRevisionId,
+                aStore,
+                function(aInternalRevisionId) {
+                  if (aInternalRevisionId == undefined) {
+                    aResolve(undefined);
+                    return;
+                  }
+
+                  // This object is the return value of this promise.
+                  // Initially we use maps, and then we convert them in array.
+                  let changes = {
+                    revisionId: '',
+                    addedIds: {},
+                    updatedIds: {},
+                    removedIds: {}
+                  };
+
+                  let request = aStore.mozGetAll(aWindow.IDBKeyRange.lowerBound(aInternalRevisionId, true));
+                  request.onsuccess = function(aEvent) {
+                    for (let i = 0; i < aEvent.target.result.length; ++i) {
+                      let data = aEvent.target.result[i];
+
+                      switch (data.operation) {
+                        case REVISION_ADDED:
+                          changes.addedIds[data.objectId] = true;
+                          break;
+
+                        case REVISION_UPDATED:
+                          // We don't consider an update if this object has been added
+                          // or if it has been already modified by a previous
+                          // operation.
+                          if (!(data.objectId in changes.addedIds) &&
+                              !(data.objectId in changes.updatedIds)) {
+                            changes.updatedIds[data.objectId] = true;
+                          }
+                          break;
+
+                        case REVISION_REMOVED:
+                          let id = data.objectId;
+
+                          // If the object has been added in this range of revisions
+                          // we can ignore it and remove it from the list.
+                          if (id in changes.addedIds) {
+                            delete changes.addedIds[id];
+                          } else {
+                            changes.removedIds[id] = true;
+                          }
+
+                          if (id in changes.updatedIds) {
+                            delete changes.updatedIds[id];
+                          }
+                          break;
+                      }
+                    }
+
+                    // The last revisionId.
+                    if (aEvent.target.result.length) {
+                      changes.revisionId = aEvent.target.result[aEvent.target.result.length - 1].revisionId;
+                    }
+
+                    // From maps to arrays.
+                    changes.addedIds = Object.keys(changes.addedIds).map(function(aKey) { return parseInt(aKey, 10); });
+                    changes.updatedIds = Object.keys(changes.updatedIds).map(function(aKey) { return parseInt(aKey, 10); });
+                    changes.removedIds = Object.keys(changes.removedIds).map(function(aKey) { return parseInt(aKey, 10); });
+
+                    let wrappedObject = ObjectWrapper.wrap(changes, aWindow);
+                    aResolve(wrappedObject);
+                  };
+                }
+              );
+            },
+            function(aEvent) {
+              debug("GetChanges transaction failed");
+              aReject(createDOMError(aWindow, aEvent));
+            }
+          );
+        });
+      },
+
       /* TODO:
-         readonly attribute DOMString revisionId
          attribute EventHandler onchange;
-         Promise<DataStoreChanges> getChanges(DOMString revisionId)
          getAll(), getLength()
        */
 
       __exposedProps__: {
         name: 'r',
         owner: 'r',
         readOnly: 'r',
         get: 'r',
         update: 'r',
         add: 'r',
         remove: 'r',
-        clear: 'r'
+        clear: 'r',
+        revisionId: 'r',
+        getChanges: 'r'
       }
     };
 
     return object;
   },
 
   delete: function() {
     this.db.delete();
--- a/dom/datastore/DataStoreDB.jsm
+++ b/dom/datastore/DataStoreDB.jsm
@@ -9,46 +9,96 @@ const {classes: Cc, interfaces: Ci, util
 this.EXPORTED_SYMBOLS = ['DataStoreDB'];
 
 function debug(s) {
   // dump('DEBUG DataStoreDB: ' + s + '\n');
 }
 
 const DATASTOREDB_VERSION = 1;
 const DATASTOREDB_OBJECTSTORE_NAME = 'DataStoreDB';
+const DATASTOREDB_REVISION = 'revision';
+const DATASTOREDB_REVISION_INDEX = 'revisionIndex';
 
 Cu.import('resource://gre/modules/IndexedDBHelper.jsm');
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
 
 this.DataStoreDB = function DataStoreDB() {}
 
 DataStoreDB.prototype = {
 
   __proto__: IndexedDBHelper.prototype,
 
   upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
     debug('updateSchema');
     aDb.createObjectStore(DATASTOREDB_OBJECTSTORE_NAME, { autoIncrement: true });
+    let store = aDb.createObjectStore(DATASTOREDB_REVISION,
+                                      { autoIncrement: true,
+                                        keyPath: 'internalRevisionId' });
+    store.createIndex(DATASTOREDB_REVISION_INDEX, 'revisionId', { unique: true });
   },
 
   init: function(aOrigin, aName) {
     let dbName = aOrigin + '_' + aName;
     this.initDBHelper(dbName, DATASTOREDB_VERSION,
-                      [DATASTOREDB_OBJECTSTORE_NAME]);
+                      [DATASTOREDB_OBJECTSTORE_NAME, DATASTOREDB_REVISION]);
   },
 
   txn: function(aType, aCallback, aErrorCb) {
     debug('Transaction request');
     this.newTxn(
       aType,
-      DATASTOREDB_OBJECTSTORE_NAME,
+      aType == 'readonly'
+        ? [ DATASTOREDB_OBJECTSTORE_NAME ] : [ DATASTOREDB_OBJECTSTORE_NAME, DATASTOREDB_REVISION ],
+      function(aTxn, aStores) {
+        aType == 'readonly' ? aCallback(aTxn, aStores[0], null) : aCallback(aTxn, aStores[0], aStores[1]);
+      },
+      function() {},
+      aErrorCb
+    );
+  },
+
+  revisionTxn: function(aType, aCallback, aErrorCb) {
+    debug("Transaction request");
+    this.newTxn(
+      aType,
+      DATASTOREDB_REVISION,
       aCallback,
       function() {},
       aErrorCb
     );
   },
 
+  addRevision: function(aStore, aId, aType, aSuccessCb) {
+    debug("AddRevision: " + aId + " - " + aType);
+    let revisionId =  uuidgen.generateUUID().toString();
+    let request = aStore.put({ revisionId: revisionId, objectId: aId, operation: aType });
+    request.onsuccess = function() {
+      aSuccessCb(revisionId);
+    }
+  },
+
+  getInternalRevisionId: function(aRevisionId, aStore, aSuccessCb) {
+    debug('GetInternalRevisionId');
+    let request = aStore.index(DATASTOREDB_REVISION_INDEX).getKey(aRevisionId);
+    request.onsuccess = function(aEvent) {
+      aSuccessCb(aEvent.target.result);
+    }
+  },
+
+  clearRevisions: function(aStore, aSuccessCb) {
+    debug("ClearRevisions");
+    let request = aStore.clear();
+    request.onsuccess = function() {
+      aSuccessCb();
+    }
+  },
+
   delete: function() {
     debug('delete');
     this.close();
     indexedDB.deleteDatabase(this.dbName);
     debug('database deleted');
   }
 }
--- a/dom/datastore/DataStoreService.js
+++ b/dom/datastore/DataStoreService.js
@@ -56,26 +56,48 @@ DataStoreService.prototype = {
 
     this.stores[aName][aAppId] = store;
   },
 
   getDataStores: function(aWindow, aName) {
     debug('getDataStores - aName: ' + aName);
     let self = this;
     return new aWindow.Promise(function(resolve, reject) {
-      let results = [];
+      let matchingStores = [];
 
       if (aName in self.stores) {
         for (let appId in self.stores[aName]) {
-          let obj = self.stores[aName][appId].exposeObject(aWindow);
-          results.push(obj);
+          matchingStores.push(self.stores[aName][appId]);
         }
       }
 
-      resolve(results);
+      let callbackPending = matchingStores.length;
+      let results = [];
+
+      if (!callbackPending) {
+        resolve(results);
+        return;
+      }
+
+      for (let i = 0; i < matchingStores.length; ++i) {
+        let obj = matchingStores[i].exposeObject(aWindow);
+        results.push(obj);
+
+        matchingStores[i].retrieveRevisionId(
+          function() {
+            --callbackPending;
+            if (!callbackPending) {
+              resolve(results);
+            }
+          },
+          function() {
+            reject();
+          }
+        );
+      }
     });
   },
 
   observe: function observe(aSubject, aTopic, aData) {
     debug('getDataStores - aTopic: ' + aTopic);
     if (aTopic != 'webapps-clear-data') {
       return;
     }
--- a/dom/datastore/tests/Makefile.in
+++ b/dom/datastore/tests/Makefile.in
@@ -10,13 +10,14 @@ VPATH            = @srcdir@
 relativesrcdir   = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_FILES = \
   test_app_install.html \
   test_readonly.html \
   test_basic.html \
+  test_revision.html \
   file_app.sjs \
   file_app.template.webapp \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/test_revision.html
@@ -0,0 +1,235 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - basic operation on a readonly db</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+  <script type="application/javascript;version=1.7">
+
+  var gBaseURL = 'http://test/tests/dom/datastore/tests/';
+  var gHostedManifestURL = gBaseURL + 'file_app.sjs';
+  var gApp;
+  var gStore;
+  var gPreviousRevisionId = '';
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function installApp() {
+    var request = navigator.mozApps.install(gHostedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      gApp = request.result;
+      runTest();
+    }
+  }
+
+  function testGetDataStores() {
+    navigator.getDataStores('foo').then(function(stores) {
+      is(stores.length, 1, "getDataStores('foo') returns 1 element");
+      is(stores[0].name, 'foo', 'The dataStore.name is foo');
+      is(stores[0].readOnly, false, 'The dataStore foo is not in readonly');
+
+      gStore = stores[0];
+
+      runTest();
+    }, cbError);
+  }
+
+  function testStoreAdd(value, expectedId) {
+    return gStore.add(value).then(function(id) {
+      is(id, expectedId, "store.add() is called");
+      runTest();
+    }, cbError);
+  }
+
+  function testStoreUpdate(id, value) {
+    return gStore.update(id, value).then(function(retId) {
+      is(id, retId, "store.update() is called with the right id");
+      runTest();
+    }, cbError);
+  }
+
+  function testStoreRemove(id, expectedSuccess) {
+    return gStore.remove(id).then(function(success) {
+      is(success, expectedSuccess, "store.remove() returns the right value");
+      runTest();
+    }, cbError);
+  }
+
+  function testStoreRevisionId() {
+    is(/[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}/.test(gStore.revisionId), true, "store.revisionId returns something");
+    runTest();
+  }
+
+  function testStoreWrongRevisions(id) {
+    return gStore.getChanges(id).then(
+      function(what) {
+        is(what, undefined, "Wrong revisionId == undefined object");
+        runTest();
+      }, cbError);
+  }
+
+  function testStoreRevisions(id, changes) {
+    return gStore.getChanges(id).then(function(what) {
+      is(JSON.stringify(changes.addedIds),
+         JSON.stringify(what.addedIds), "store.revisions - addedIds: " +
+         JSON.stringify(what.addedIds) + " | " + JSON.stringify(changes.addedIds));
+      is(JSON.stringify(changes.updatedIds),
+         JSON.stringify(what.updatedIds), "store.revisions - updatedIds: " +
+         JSON.stringify(what.updatedIds) + " | " + JSON.stringify(changes.updatedIds));
+      is(JSON.stringify(changes.removedIds),
+         JSON.stringify(what.removedIds), "store.revisions - removedIds: " +
+         JSON.stringify(what.removedIds) + " | " + JSON.stringify(changes.removedIds));
+      runTest();
+    }, cbError);
+  }
+
+  function uninstallApp() {
+    // Uninstall the app.
+    request = navigator.mozApps.mgmt.uninstall(gApp);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      // All done.
+      ok(true, "All done");
+      runTest();
+    }
+  }
+
+  function testStoreRevisionIdChanged() {
+    isnot(gStore.revisionId, gPreviousRevisionId, "Revision changed");
+    gPreviousRevisionId = gStore.revisionId;
+    runTest();
+  }
+
+  function testStoreRevisionIdNotChanged() {
+    is(gStore.revisionId, gPreviousRevisionId, "Revision changed");
+    runTest();
+  }
+
+  var revisions = [];
+
+  var tests = [
+    // Permissions
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "browser", "allow": 1, "context": document },
+         { "type": "embed-apps", "allow": 1, "context": document },
+         { "type": "webapps-manage", "allow": 1, "context": document }], runTest);
+    },
+
+    // Preferences
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest);
+    },
+
+    // No confirmation needed when an app is installed
+    function() {
+      SpecialPowers.autoConfirmAppInstall(runTest);
+    },
+
+    // Installing the app
+    installApp,
+
+    // Test for GetDataStore
+    testGetDataStores,
+
+    // The first revision is not empty
+    testStoreRevisionIdChanged,
+
+    // wrong revision ID
+    function() { testStoreWrongRevisions('foobar'); },
+
+    // Add
+    function() { testStoreAdd({ number: 42 }, 1); },
+    function() { revisions.push(gStore.revisionId); testStoreRevisionId(); },
+    testStoreRevisionIdChanged,
+    function() { testStoreRevisions(revisions[0], { addedIds: [], updatedIds: [], removedIds: [] }); },
+
+    // Add
+    function() { testStoreAdd({ number: 42 }, 2); },
+    function() { revisions.push(gStore.revisionId); runTest(); },
+    testStoreRevisionIdChanged,
+    function() { testStoreRevisions(revisions[0], { addedIds: [2], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[1], { addedIds: [], updatedIds: [], removedIds: [] }); },
+
+    // Add
+    function() { testStoreAdd({ number: 42 }, 3); },
+    function() { revisions.push(gStore.revisionId); runTest(); },
+    testStoreRevisionIdChanged,
+    function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [], removedIds: [] }); },
+
+    // Update
+    function() { testStoreUpdate(3, { number: 43 }); },
+    function() { revisions.push(gStore.revisionId); runTest(); },
+    testStoreRevisionIdChanged,
+    function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [3], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [], removedIds: [] }); },
+
+    // Update
+    function() { testStoreUpdate(3, { number: 42 }); },
+    function() { revisions.push(gStore.revisionId); runTest(); },
+    testStoreRevisionIdChanged,
+    function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [3], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [3], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[4], { addedIds: [], updatedIds: [], removedIds: [] }); },
+
+    // Remove
+    function() { testStoreRemove(3, true); },
+    function() { revisions.push(gStore.revisionId); runTest(); },
+    testStoreRevisionIdChanged,
+    function() { testStoreRevisions(revisions[0], { addedIds: [2], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[1], { addedIds: [], updatedIds: [], removedIds: [] }); },
+    function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [], removedIds: [3] }); },
+    function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [], removedIds: [3] }); },
+    function() { testStoreRevisions(revisions[4], { addedIds: [], updatedIds: [], removedIds: [3] }); },
+    function() { testStoreRevisions(revisions[5], { addedIds: [], updatedIds: [], removedIds: [] }); },
+
+    function() { testStoreRemove(3, false); },
+    testStoreRevisionIdNotChanged,
+
+    // Remove
+    function() { testStoreRemove(42, false); },
+    testStoreRevisionIdNotChanged,
+
+    // Uninstall the app
+    uninstallApp
+  ];
+
+  function runTest() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTest();
+  </script>
+</pre>
+</body>
+</html>
+