Bug 871445 - patch 3 - DataStore: getChanges + revisionID, r=ehsan, sr=mounir, r=bent
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 02 Oct 2013 13:27:11 -0400
changeset 163548 46b906bb782b2e12d82134ef205e6a11666205dc
parent 163547 6e29aa59b5a6181ea6478176c377f7b5442a51de
child 163549 ce3eceb7fa125532171e5985e5a4b1e6ae284b00
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [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>
+