Bug 871445 - patch 7 - DataStore: WebIDL porting
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 02 Oct 2013 13:27:15 -0400
changeset 163552 8d69d1739e3433f30ae4b76c64d82b444475b59c
parent 163551 5b2656292c18a5078e39ec13655887844189f0c0
child 163553 0e04ff490bf3b6eb318087bc72cf970c14418ca6
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)
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 7 - DataStore: WebIDL porting
dom/datastore/DataStore.jsm
dom/datastore/tests/file_basic.html
dom/datastore/tests/file_changes.html
dom/webidl/DataStore.webidl
dom/webidl/DataStoreChangeEvent.webidl
dom/webidl/moz.build
--- a/dom/datastore/DataStore.jsm
+++ b/dom/datastore/DataStore.jsm
@@ -27,21 +27,321 @@ Cu.import("resource://gre/modules/DataSt
 Cu.import("resource://gre/modules/ObjectWrapper.jsm");
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
-/* Helper function */
+/* Helper functions */
 function createDOMError(aWindow, aEvent) {
   return new aWindow.DOMError(aEvent.target.error.name);
 }
 
+function throwInvalidArg(aWindow) {
+  return aWindow.Promise.reject(
+    new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id"));
+}
+
+function throwReadOnly(aWindow) {
+  return aWindow.Promise.reject(
+    new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
+}
+
+function parseIds(aId) {
+  function parseId(aId) {
+    aId = parseInt(aId);
+    return (isNaN(aId) || aId <= 0) ? null : aId;
+  }
+
+  if (!Array.isArray(aId)) {
+    return parseId(aId);
+  }
+
+  for (let i = 0; i < aId.length; ++i) {
+    aId[i] = parseId(aId[i]);
+    if (aId[i] === null) {
+      return null;
+    }
+  }
+
+  return aId;
+}
+
+/* Exposed DataStore object */
+function ExposedDataStore(aWindow, aDataStore, aReadOnly) {
+  debug("ExposedDataStore created");
+  this.init(aWindow, aDataStore, aReadOnly);
+}
+
+ExposedDataStore.prototype = {
+  classDescription: "DataStore XPCOM Component",
+  classID: Components.ID("{db5c9602-030f-4bff-a3de-881a8de370f2}"),
+  contractID: "@mozilla.org/dom/datastore;1",
+  QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]),
+
+  callbacks: [],
+
+  init: function(aWindow, aDataStore, aReadOnly) {
+    debug("ExposedDataStore init");
+
+    this.window = aWindow;
+    this.dataStore = aDataStore;
+    this.isReadOnly = aReadOnly;
+  },
+
+  receiveMessage: function(aMessage) {
+    debug("receiveMessage");
+
+    if (aMessage.name != "DataStore:Changed:Return:OK") {
+      debug("Wrong message: " + aMessage.name);
+      return;
+    }
+
+    let self = this;
+
+    this.dataStore.retrieveRevisionId(
+      function() {
+        let event = new self.window.DataStoreChangeEvent('change', aMessage.data);
+        self.__DOM_IMPL__.dispatchEvent(event);
+      },
+      // Forcing the reading of the revisionId
+      true
+    );
+  },
+
+  // Public interface :
+
+  get name() {
+    return this.dataStore.name;
+  },
+
+  get owner() {
+    return this.dataStore.owner;
+  },
+
+  get readOnly() {
+    return this.isReadOnly;
+  },
+
+  get: function(aId) {
+    aId = parseIds(aId);
+    if (aId === null) {
+      return throwInvalidArg(this.window);
+    }
+
+    let self = this;
+
+    // Promise<Object>
+    return this.dataStore.newDBPromise(this.window, "readonly",
+      function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+               self.dataStore.getInternal(aStore,
+                                          Array.isArray(aId) ?  aId : [ aId ],
+                                          function(aResults) {
+          aResolve(Array.isArray(aId) ? aResults : aResults[0]);
+        });
+      }
+    );
+  },
+
+  update: function(aId, aObj) {
+    aId = parseInt(aId);
+    if (isNaN(aId) || aId <= 0) {
+      return throwInvalidArg(this.window);
+    }
+
+    if (this.isReadOnly) {
+      return throwReadOnly(this.window);
+    }
+
+    let self = this;
+
+    // Promise<void>
+    return this.dataStore.newDBPromise(this.window, "readwrite",
+      function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+        self.dataStore.updateInternal(aResolve, aStore, aRevisionStore, aId, aObj);
+      }
+    );
+  },
+
+  add: function(aObj) {
+    if (this.isReadOnly) {
+      return throwReadOnly(this.window);
+    }
+
+    let self = this;
+
+    // Promise<int>
+    return this.dataStore.newDBPromise(this.window, "readwrite",
+      function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+        self.dataStore.addInternal(aResolve, aStore, aRevisionStore, aObj);
+      }
+    );
+  },
+
+  remove: function(aId) {
+    aId = parseInt(aId);
+    if (isNaN(aId) || aId <= 0) {
+      return throwInvalidArg(this.window);
+    }
+
+    if (this.isReadOnly) {
+      return throwReadOnly(this.window);
+    }
+
+    let self = this;
+
+    // Promise<void>
+    return this.dataStore.newDBPromise(this.window, "readwrite",
+      function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+        self.dataStore.removeInternal(aResolve, aStore, aRevisionStore, aId);
+      }
+    );
+  },
+
+  clear: function() {
+    if (this.isReadOnly) {
+      return throwReadOnly(this.window);
+    }
+
+    let self = this;
+
+    // Promise<void>
+    return this.dataStore.newDBPromise(this.window, "readwrite",
+      function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+        self.dataStore.clearInternal(aResolve, aStore, aRevisionStore);
+      }
+    );
+  },
+
+  get revisionId() {
+    return this.dataStore.revisionId;
+  },
+
+  getChanges: function(aRevisionId) {
+    debug("GetChanges: " + aRevisionId);
+
+    if (aRevisionId === null || aRevisionId === undefined) {
+      return this.window.Promise.reject(
+        new this.window.DOMError("SyntaxError", "Invalid revisionId"));
+    }
+
+    let self = this;
+
+    // Promise<DataStoreChanges>
+    return new this.window.Promise(function(aResolve, aReject) {
+      debug("GetChanges promise started");
+      self.dataStore.db.revisionTxn(
+        'readonly',
+        function(aTxn, aStore) {
+          debug("GetChanges transaction success");
+
+          let request = self.dataStore.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(self.window.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, self.window);
+                aResolve(wrappedObject);
+              };
+            }
+          );
+        },
+        function(aEvent) {
+          debug("GetChanges transaction failed");
+          aReject(createDOMError(self.window, aEvent));
+        }
+      );
+    });
+  },
+
+  getLength: function() {
+    let self = this;
+
+    // Promise<int>
+    return this.dataStore.newDBPromise(this.window, "readonly",
+      function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
+        self.dataStore.getLengthInternal(aResolve, aStore);
+      }
+    );
+  },
+
+  set onchange(aCallback) {
+    debug("Set OnChange");
+    this.__DOM_IMPL__.setEventHandler("onchange", aCallback);
+  },
+
+  get onchange() {
+    debug("Get OnChange");
+    return this.__DOM_IMPL__.getEventHandler("onchange");
+  }
+};
+
 /* DataStore object */
 
 function DataStore(aAppId, aName, aOwner, aReadOnly, aGlobalScope) {
   this.appId = aAppId;
   this.name = aName;
   this.owner = aOwner;
   this.readOnly = aReadOnly;
 
@@ -244,346 +544,33 @@ DataStore.prototype = {
           self.revisionId = cursor.value.revisionId;
           aSuccessCb();
         };
       }
     );
   },
 
   exposeObject: function(aWindow, aReadOnly) {
-    let self = this;
-    let object = {
-      callbacks: [],
-
-      // Public interface :
-
-      get name() {
-        return self.name;
-      },
-
-      get owner() {
-        return self.owner;
-      },
-
-      get readOnly() {
-        return aReadOnly;
-      },
-
-      get: function DS_get(aId) {
-        aId = this.parseIds(aId);
-        if (aId === null) {
-          return this.throwInvalidArg(aWindow);
-        }
-
-        // Promise<Object>
-        return self.newDBPromise(aWindow, "readonly",
-          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
-            self.getInternal(aStore, Array.isArray(aId) ?  aId : [ aId ],
-                             function(aResults) {
-              aResolve(Array.isArray(aId) ? aResults : aResults[0]);
-            });
-          }
-        );
-      },
-
-      update: function DS_update(aId, aObj) {
-        aId = parseInt(aId);
-        if (isNaN(aId) || aId <= 0) {
-          return this.throwInvalidArg(aWindow);
-        }
-
-        if (aReadOnly) {
-          return this.throwReadOnly(aWindow);
-        }
-
-        // Promise<void>
-        return self.newDBPromise(aWindow, "readwrite",
-          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
-            self.updateInternal(aResolve, aStore, aRevisionStore, aId, aObj);
-          }
-        );
-      },
-
-      add: function DS_add(aObj) {
-        if (aReadOnly) {
-          return this.throwReadOnly(aWindow);
-        }
-
-        // Promise<int>
-        return self.newDBPromise(aWindow, "readwrite",
-          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 this.throwInvalidArg(aWindow);
-        }
-
-        if (aReadOnly) {
-          return this.throwReadOnly(aWindow);
-        }
-
-        // Promise<void>
-        return self.newDBPromise(aWindow, "readwrite",
-          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
-            self.removeInternal(aResolve, aStore, aRevisionStore, aId);
-          }
-        );
-      },
-
-      clear: function DS_clear() {
-        if (aReadOnly) {
-          return this.throwReadOnly(aWindow);
-        }
-
-        // Promise<void>
-        return self.newDBPromise(aWindow, "readwrite",
-          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;
+    debug("Exposing Object");
 
-                          // 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));
-            }
-          );
-        });
-      },
-
-      getLength: function DS_getLength() {
-        // Promise<int>
-        return self.newDBPromise(aWindow, "readonly",
-          function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
-            self.getLengthInternal(aResolve, aStore);
-          }
-        );
-      },
-
-      set onchange(aCallback) {
-        debug("Set OnChange");
-        this.onchangeCb = aCallback;
-      },
-
-      get onchange() {
-        debug("Get OnChange");
-        return this.onchangeCb;
-      },
-
-      addEventListener: function(aName, aCallback) {
-        debug("addEventListener:" + aName);
-        if (aName != 'change') {
-          return;
-        }
-
-        this.callbacks.push(aCallback);
-      },
-
-      removeEventListener: function(aName, aCallback) {
-        debug('removeEventListener');
-        let pos = this.callbacks.indexOf(aCallback);
-        if (pos != -1) {
-          this.callbacks.splice(pos, 1);
-        }
-      },
-
-      __exposedProps__: {
-        name: 'r',
-        owner: 'r',
-        readOnly: 'r',
-        get: 'r',
-        update: 'r',
-        add: 'r',
-        remove: 'r',
-        clear: 'r',
-        revisionId: 'r',
-        getChanges: 'r',
-        getLength: 'r',
-        onchange: 'rw',
-        addEventListener: 'r',
-        removeEventListener: 'r'
-      },
-
-      throwInvalidArg: function(aWindow) {
-        return aWindow.Promise.reject(
-          new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id"));
-      },
-
-      throwReadOnly: function(aWindow) {
-        return aWindow.Promise.reject(
-          new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
-      },
-
-      parseIds: function(aId) {
-        function parseId(aId) {
-          aId = parseInt(aId);
-          return (isNaN(aId) || aId <= 0) ? null : aId;
-        }
-
-        if (!Array.isArray(aId)) {
-          return parseId(aId);
-        }
-
-        for (let i = 0; i < aId.length; ++i) {
-          aId[i] = parseId(aId[i]);
-          if (aId[i] === null) {
-            return null;
-          }
-        }
-
-        return aId;
-      },
-
-
-      receiveMessage: function(aMessage) {
-        debug("receiveMessage");
-
-        if (aMessage.name != "DataStore:Changed:Return:OK") {
-          debug("Wrong message: " + aMessage.name);
-          return;
-        }
-
-        self.retrieveRevisionId(
-          function() {
-            if (object.onchangeCb || object.callbacks.length) {
-              let wrappedData = ObjectWrapper.wrap(aMessage.data, aWindow);
-
-              // This array is used to avoid that a callback adds/removes
-              // another eventListener.
-              var cbs = [];
-              if (object.onchangeCb) {
-                cbs.push(object.onchangeCb);
-              }
-
-              for (let i = 0; i < object.callbacks.length; ++i) {
-                cbs.push(object.callbacks[i]);
-              }
-
-              for (let i = 0; i < cbs.length; ++i) {
-                try {
-                  cbs[i](wrappedData);
-                } catch(e) {}
-              }
-            }
-          },
-          // Forcing the reading of the revisionId
-          true
-        );
-      }
-    };
+    let exposedObject = new ExposedDataStore(aWindow, this, aReadOnly);
+    let object = aWindow.DataStore._create(aWindow, exposedObject);
 
     Services.obs.addObserver(function(aSubject, aTopic, aData) {
       let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
-      if (wId == object.innerWindowID) {
-        cpmm.removeMessageListener("DataStore:Changed:Return:OK", object);
+      if (wId == exposedObject.innerWindowID) {
+        cpmm.removeMessageListener("DataStore:Changed:Return:OK", exposedObject);
       }
     }, "inner-window-destroyed", false);
 
     let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils);
-    object.innerWindowID = util.currentInnerWindowID;
+    exposedObject.innerWindowID = util.currentInnerWindowID;
 
-    cpmm.addMessageListener("DataStore:Changed:Return:OK", object);
+    cpmm.addMessageListener("DataStore:Changed:Return:OK", exposedObject);
     cpmm.sendAsyncMessage("DataStore:RegisterForMessages",
                           { store: this.name, owner: this.owner });
 
     return object;
   },
 
   delete: function() {
     this.db.delete();
@@ -605,9 +592,8 @@ DataStore.prototype = {
 function DataStoreAccess(aAppId, aName, aOrigin, aReadOnly) {
   this.appId = aAppId;
   this.name = aName;
   this.origin = aOrigin;
   this.readOnly = aReadOnly;
 }
 
 DataStoreAccess.prototype = {};
-
--- a/dom/datastore/tests/file_basic.html
+++ b/dom/datastore/tests/file_basic.html
@@ -40,46 +40,16 @@
       ok("clear" in store, "store.clear exists");
 
       gStore = stores[0];
 
       runTest();
     }, cbError);
   }
 
-  function testStoreErrorGet(id) {
-    gStore.get(id).then(function(what) {
-      ok(false, "store.get(" + id + ") retrieves data");
-    }, function(error) {
-      ok(true, "store.get() failed properly because the id is non-valid");
-      ok(error instanceof DOMError, "error is a DOMError");
-      is(error.name, "SyntaxError", "Error is a syntax error");
-    }).then(runTest, cbError);
-  }
-
-  function testStoreErrorUpdate(id) {
-    gStore.update(id, "foo").then(function(what) {
-      ok(false, "store.update(" + id + ") retrieves data");
-    }, function(error) {
-      ok(true, "store.update() failed properly because the id is non-valid");
-      ok(error instanceof DOMError, "error is a DOMError");
-      is(error.name, "SyntaxError", "Error is a syntax error");
-    }).then(runTest, cbError);
-  }
-
-  function testStoreErrorRemove(id) {
-    gStore.remove(id).then(function(what) {
-      ok(false, "store.remove(" + id + ") retrieves data");
-    }, function(error) {
-      ok(true, "store.remove() failed properly because the id is non-valid");
-      ok(error instanceof DOMError, "error is a DOMError");
-      is(error.name, "SyntaxError", "Error is a syntax error");
-    }).then(runTest, cbError);
-  }
-
   function testStoreGet(id, value) {
     gStore.get(id).then(function(what) {
       ok(true, "store.get() retrieves data");
       is(what, value, "store.get(" + id + ") returns " + value);
     }, function() {
       ok(false, "store.get(" + id + ") retrieves data");
     }).then(runTest, cbError);
   }
@@ -115,21 +85,16 @@
       ok(true, "store.clear() is called");
     }, cbError);
   }
 
   var tests = [
     // Test for GetDataStore
     testGetDataStores,
 
-    // Broken ID
-    function() { testStoreErrorGet('hello world'); },
-    function() { testStoreErrorGet(true); },
-    function() { testStoreErrorGet(null); },
-
     // Unknown ID
     function() { testStoreGet(42, undefined); },
     function() { testStoreGet(42, undefined); }, // twice
 
     // Add + Get - number
     function() { testStoreAdd(42).then(function(id) {
                    gId = id; runTest(); }, cbError); },
     function() { testStoreGet(gId, 42); },
@@ -140,34 +105,24 @@
                    gId = id; runTest(); }, cbError); },
     function() { testStoreGet(gId, true); },
 
     // Add + Get - string
     function() { testStoreAdd("hello world").then(function(id) {
                    gId = id; runTest(); }, cbError); },
     function() { testStoreGet(gId, "hello world"); },
 
-    // Broken update
-    function() { testStoreErrorUpdate('hello world'); },
-    function() { testStoreErrorUpdate(true); },
-    function() { testStoreErrorUpdate(null); },
-
     // Update + Get - string
     function() { testStoreUpdate(gId, "hello world 2").then(function() {
                    runTest(); }, cbError); },
     function() { testStoreGet(gId, "hello world 2"); },
 
     // getLength
     function() { testStoreGetLength(3).then(function() { runTest(); }, cbError); },
 
-    // Broken remove
-    function() { testStoreErrorRemove('hello world'); },
-    function() { testStoreErrorRemove(true); },
-    function() { testStoreErrorRemove(null); },
-
     // Remove
     function() { testStoreRemove(gId).then(function(what) {
                    runTest(); }, cbError); },
     function() { testStoreGet(gId).catch(function() {
                    runTest(); }); },
 
     // Remove - wrong ID
     function() { testStoreRemove(gId).then(function(what) {
--- a/dom/datastore/tests/file_changes.html
+++ b/dom/datastore/tests/file_changes.html
@@ -56,30 +56,32 @@
   }
 
   function testStoreRemove(id, expectedSuccess) {
     gStore.remove(id).then(function(success) {
       is(success, expectedSuccess, "store.remove() returns the right value");
     }, cbError);
   }
 
-  function eventListener(obj) {
-    ok(obj, "OnChangeListener is called with data");
-    is(obj.id, gChangeId, "OnChangeListener is called with the right ID: " + obj.id);
-    is(obj.operation, gChangeOperation, "OnChangeListener is called with the right operation:" + obj.operation + " " + gChangeOperation);
+  function eventListener(evt) {
+    ok(evt, "OnChangeListener is called with data");
+    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(evt.revisionId), true, "event.revisionId returns something");
+    is(evt.id, gChangeId, "OnChangeListener is called with the right ID: " + evt.id);
+    is(evt.operation, gChangeOperation, "OnChangeListener is called with the right operation:" + evt.operation + " " + gChangeOperation);
     runTest();
   }
 
   var tests = [
     // Test for GetDataStore
     testGetDataStores,
 
     // Add onchange = function
     function() {
       gStore.onchange = eventListener;
+      is(gStore.onchange, eventListener, "onChange is set");
       runTest();
     },
 
     // Add
     function() { gChangeId = 1; gChangeOperation = 'added';
                  testStoreAdd({ number: 42 }, 1); },
 
     // Update
new file mode 100644
--- /dev/null
+++ b/dom/webidl/DataStore.webidl
@@ -0,0 +1,54 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+[Pref="dom.datastore.enabled",
+ JSImplementation="@mozilla.org/dom/datastore;1"]
+interface DataStore : EventTarget {
+  // Returns the label of the DataSource.
+  readonly attribute DOMString name;
+
+  // Returns the origin of the DataSource (e.g., 'facebook.com').
+  // This value is the manifest URL of the owner app.
+  readonly attribute DOMString owner;
+
+  // is readOnly a F(current_app, datastore) function? yes
+  readonly attribute boolean readOnly;
+
+  // Promise<any>
+  Promise get(unsigned long id);
+
+  // Promise<any>
+  Promise get(sequence<unsigned long> id);
+
+  // Promise<void>
+  Promise update(unsigned long id, any obj);
+
+  // Promise<unsigned long>
+  Promise add(any obj);
+
+  // Promise<boolean>
+  Promise remove(unsigned long id);
+
+  // Promise<void>
+  Promise clear();
+
+  readonly attribute DOMString revisionId;
+
+  attribute EventHandler onchange;
+
+  // Promise<DataStoreChanges>
+  Promise getChanges(DOMString revisionId);
+
+  // Promise<unsigned long>
+  Promise getLength();
+};
+
+dictionary DataStoreChanges {
+  DOMString revisionId;
+  sequence<unsigned long> addedIds;
+  sequence<unsigned long> updatedIds;
+  sequence<unsigned long> removedIds;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/DataStoreChangeEvent.webidl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+dictionary DataStoreChangeEventInit : EventInit {
+  DOMString revisionId = "";
+  unsigned long id = 0;
+  DOMString operation = "";
+};
+
+[Pref="dom.datastore.enabled",
+ Constructor(DOMString type, optional DataStoreChangeEventInit eventInitDict)]
+interface DataStoreChangeEvent : Event {
+  readonly attribute DOMString revisionId;
+  readonly attribute unsigned long id;
+  readonly attribute DOMString operation;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -68,16 +68,17 @@ WEBIDL_FILES = [
     'DOMRect.webidl',
     'DOMRectList.webidl',
     'DOMRequest.webidl',
     'DOMSettableTokenList.webidl',
     'DOMStringMap.webidl',
     'DOMTokenList.webidl',
     'DOMTransaction.webidl',
     'DataContainerEvent.webidl',
+    'DataStore.webidl',
     'DelayNode.webidl',
     'DesktopNotification.webidl',
     'DeviceMotionEvent.webidl',
     'DeviceStorage.webidl',
     'Document.webidl',
     'DocumentFragment.webidl',
     'DocumentType.webidl',
     'DragEvent.webidl',
@@ -530,16 +531,17 @@ if CONFIG['ENABLE_TESTS']:
     PREPROCESSED_TEST_WEBIDL_FILES += [
         'TestCodeGen.webidl',
         'TestExampleGen.webidl',
         'TestJSImplGen.webidl',
     ]
 
 GENERATED_EVENTS_WEBIDL_FILES = [
     'BlobEvent.webidl',
+    'DataStoreChangeEvent.webidl',
     'DeviceLightEvent.webidl',
     'DeviceProximityEvent.webidl',
     'MediaStreamEvent.webidl',
     'MozInterAppMessageEvent.webidl',
     'RTCDataChannelEvent.webidl',
     'RTCPeerConnectionIceEvent.webidl',
     'UserProximityEvent.webidl',
 ]