Bug 871445 - patch 5 - DataStore: onchange, r=ehsan, r=bent
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 02 Oct 2013 13:27:15 -0400
changeset 163550 0b0557b547aa33c05a12660d9fff80f1585ec8f7
parent 163549 ce3eceb7fa125532171e5985e5a4b1e6ae284b00
child 163551 5b2656292c18a5078e39ec13655887844189f0c0
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, 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 5 - DataStore: onchange, r=ehsan, r=bent
b2g/chrome/content/shell.js
dom/datastore/DataStore.jsm
dom/datastore/DataStoreChangeNotifier.jsm
dom/datastore/DataStoreService.js
dom/datastore/moz.build
dom/datastore/tests/Makefile.in
dom/datastore/tests/file_app.sjs
dom/datastore/tests/file_app.template.webapp
dom/datastore/tests/file_app2.template.webapp
dom/datastore/tests/file_app2.template.webapp^headers^
dom/datastore/tests/file_changes.html
dom/datastore/tests/file_changes2.html
dom/datastore/tests/test_app_install.html
dom/datastore/tests/test_basic.html
dom/datastore/tests/test_changes.html
dom/datastore/tests/test_readonly.html
dom/datastore/tests/test_revision.html
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* 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/. */
 
 Cu.import('resource://gre/modules/ContactService.jsm');
 Cu.import('resource://gre/modules/SettingsChangeNotifier.jsm');
+Cu.import('resource://gre/modules/DataStoreChangeNotifier.jsm');
 Cu.import('resource://gre/modules/AlarmService.jsm');
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import('resource://gre/modules/PermissionPromptHelper.jsm');
 Cu.import('resource://gre/modules/ObjectWrapper.jsm');
 Cu.import('resource://gre/modules/accessibility/AccessFu.jsm');
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import('resource://gre/modules/UserAgentOverrides.jsm');
--- a/dom/datastore/DataStore.jsm
+++ b/dom/datastore/DataStore.jsm
@@ -1,32 +1,37 @@
 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 'use strict'
 
-var EXPORTED_SYMBOLS = ["DataStore", "DataStoreAccess"];
+this.EXPORTED_SYMBOLS = ["DataStore", "DataStoreAccess"];
 
 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');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
 
 /* Helper function */
 function createDOMError(aWindow, aEvent) {
   return new aWindow.DOMError(aEvent.target.error.name);
 }
 
 /* DataStore object */
 
@@ -156,39 +161,45 @@ DataStore.prototype = {
     };
   },
 
   addRevision: function(aRevisionStore, aId, aType, aSuccessCb) {
     let self = this;
     this.db.addRevision(aRevisionStore, aId, aType,
       function(aRevisionId) {
         self.revisionId = aRevisionId;
+        self.sendNotification(aId, aType, aRevisionId);
         aSuccessCb();
       }
     );
   },
 
-  retrieveRevisionId: function(aSuccessCb) {
-    if (this.revisionId != null) {
+  retrieveRevisionId: function(aSuccessCb, aForced) {
+    if (this.revisionId != null && !aForced) {
       aSuccessCb();
       return;
     }
 
     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);
+            self.addRevision(aRevisionStore, 0, REVISION_VOID,
+              function(aRevisionId) {
+                self.revisionId = aRevisionId;
+                aSuccessCb();
+              }
+            );
             return;
           }
 
           self.revisionId = cursor.value.revisionId;
           aSuccessCb();
         };
       }
     );
@@ -202,16 +213,17 @@ DataStore.prototype = {
   throwReadOnly: function(aWindow) {
     return aWindow.Promise.reject(
       new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
   },
 
   exposeObject: function(aWindow, aReadOnly) {
     let self = this;
     let object = {
+      callbacks: [],
 
       // Public interface :
 
       get name() {
         return self.name;
       },
 
       get owner() {
@@ -393,40 +405,130 @@ DataStore.prototype = {
             function(aEvent) {
               debug("GetChanges transaction failed");
               aReject(createDOMError(aWindow, aEvent));
             }
           );
         });
       },
 
+      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);
+        }
+      },
+
       /* TODO:
-         attribute EventHandler onchange;
          getAll(), getLength()
        */
 
       __exposedProps__: {
         name: 'r',
         owner: 'r',
         readOnly: 'r',
         get: 'r',
         update: 'r',
         add: 'r',
         remove: 'r',
         clear: 'r',
         revisionId: 'r',
-        getChanges: 'r'
+        getChanges: 'r',
+        onchange: 'rw',
+        addEventListener: 'r',
+        removeEventListener: 'r'
+      },
+
+      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
+        );
       }
     };
 
+    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);
+      }
+    }, "inner-window-destroyed", false);
+
+    let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIDOMWindowUtils);
+    object.innerWindowID = util.currentInnerWindowID;
+
+    cpmm.addMessageListener("DataStore:Changed:Return:OK", object);
+    cpmm.sendAsyncMessage("DataStore:RegisterForMessages",
+                          { store: this.name, owner: this.owner });
+
     return object;
   },
 
   delete: function() {
     this.db.delete();
+  },
+
+  sendNotification: function(aId, aOperation, aRevisionId) {
+    debug("SendNotification");
+    if (aOperation != REVISION_VOID) {
+      cpmm.sendAsyncMessage("DataStore:Changed",
+                            { store: this.name, owner: this.owner,
+                              message: { revisionId: aRevisionId, id: aId,
+                                         operation: aOperation } } );
+    }
   }
 };
 
 /* DataStoreAccess */
 
 function DataStoreAccess(aAppId, aName, aOrigin, aReadOnly) {
   this.appId = aAppId;
   this.name = aName;
new file mode 100644
--- /dev/null
+++ b/dom/datastore/DataStoreChangeNotifier.jsm
@@ -0,0 +1,110 @@
+/* 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/. */
+
+"use strict"
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+this.EXPORTED_SYMBOLS = ["DataStoreChangeNotifier"];
+
+function debug(s) {
+  //dump('DEBUG DataStoreChangeNotifier: ' + s + '\n');
+}
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const kFromDataStoreChangeNotifier = "fromDataStoreChangeNotifier";
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageBroadcaster");
+
+this.DataStoreChangeNotifier = {
+  children: [],
+  messages: [ "DataStore:Changed", "DataStore:RegisterForMessages",
+              "child-process-shutdown" ],
+
+  init: function() {
+    debug("init");
+
+    this.messages.forEach((function(msgName) {
+      ppmm.addMessageListener(msgName, this);
+    }).bind(this));
+
+    Services.obs.addObserver(this, 'xpcom-shutdown', false);
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    debug("observe");
+
+    switch (aTopic) {
+      case 'xpcom-shutdown':
+        this.messages.forEach((function(msgName) {
+          ppmm.removeMessageListener(msgName, this);
+        }).bind(this));
+
+        Services.obs.removeObserver(this, 'xpcom-shutdown');
+        ppmm = null;
+        break;
+
+      default:
+        debug("Wrong observer topic: " + aTopic);
+        break;
+    }
+  },
+
+  broadcastMessage: function broadcastMessage(aMsgName, aData) {
+    debug("Broadast");
+    this.children.forEach(function(obj) {
+      if (obj.store == aData.store && obj.owner == aData.owner) {
+        obj.mm.sendAsyncMessage(aMsgName, aData.message);
+      }
+    });
+  },
+
+  receiveMessage: function(aMessage) {
+    debug("receiveMessage");
+
+    switch (aMessage.name) {
+      case "DataStore:Changed":
+        this.broadcastMessage("DataStore:Changed:Return:OK", aMessage.data);
+        break;
+
+      case "DataStore:RegisterForMessages":
+        debug("Register!");
+
+        for (let i = 0; i < this.children.length; ++i) {
+          if (this.children[i].mm == aMessage.target &&
+              this.children[i].store == aMessage.data.store &&
+              this.children[i].owner == aMessage.data.owner) {
+            return;
+          }
+        }
+
+        this.children.push({ mm: aMessage.target,
+                             store: aMessage.data.store,
+                             owner: aMessage.data.owner });
+        break;
+
+      case "child-process-shutdown":
+        debug("Unregister");
+
+        for (let i = 0; i < this.children.length;) {
+          if (this.children[i].mm == aMessage.target) {
+            debug("Unregister index: " + i);
+            this.children.splice(i, 1);
+          } else {
+            ++i;
+          }
+        }
+        break;
+
+      default:
+        debug("Wrong message: " + aMessage.name);
+    }
+  }
+}
+
+DataStoreChangeNotifier.init();
--- a/dom/datastore/DataStoreService.js
+++ b/dom/datastore/DataStoreService.js
@@ -122,19 +122,19 @@ DataStoreService.prototype = {
 
         matchingStores[i].store.retrieveRevisionId(
           function() {
             --callbackPending;
             if (!callbackPending) {
               resolve(results);
             }
           },
-          function() {
-            reject();
-          }
+          // if the revision is already known, we don't need to retrieve it
+          // again.
+          false
         );
       }
     });
   },
 
   getDataStoreAccess: function(aStore, aAppId) {
     if (!(aStore.name in this.accessStores) ||
         !(aAppId in this.accessStores[aStore.name])) {
--- a/dom/datastore/moz.build
+++ b/dom/datastore/moz.build
@@ -16,10 +16,11 @@ MODULE = 'dom'
 
 EXTRA_COMPONENTS += [
     'DataStore.manifest',
     'DataStoreService.js',
 ]
 
 EXTRA_JS_MODULES += [
     'DataStore.jsm',
+    'DataStoreChangeNotifier.jsm',
     'DataStoreDB.jsm',
 ]
--- a/dom/datastore/tests/Makefile.in
+++ b/dom/datastore/tests/Makefile.in
@@ -15,15 +15,17 @@ MOCHITEST_FILES = \
   test_app_install.html \
   file_app_install.html \
   test_readonly.html \
   file_readonly.html \
   test_basic.html \
   file_basic.html \
   test_revision.html \
   file_revision.html \
+  test_changes.html \
+  file_changes.html \
+  file_changes2.html \
   file_app.sjs \
   file_app.template.webapp \
   file_app2.template.webapp \
-  file_app2.template.webapp^headers^ \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/dom/datastore/tests/file_app.sjs
+++ b/dom/datastore/tests/file_app.sjs
@@ -1,20 +1,23 @@
 var gBasePath = "tests/dom/datastore/tests/";
-var gAppTemplatePath = "tests/dom/datastore/tests/file_app.template.webapp";
 
 function handleRequest(request, response) {
   var query = getQuery(request);
 
   var testToken = '';
   if ('testToken' in query) {
     testToken = query.testToken;
   }
 
-  var template = gBasePath + 'file_app.template.webapp';
+  var template = 'file_app.template.webapp';
+  if ('template' in query) {
+    template = query.template;
+  }
+  var template = gBasePath + template;
   response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
   response.write(readTemplate(template).replace(/TESTTOKEN/g, testToken));
 }
 
 // Copy-pasted incantations. There ought to be a better way to synchronously read
 // a file into a string, but I guess we're trying to discourage that.
 function readTemplate(path) {
   var file = Components.classes["@mozilla.org/file/directory_service;1"].
--- a/dom/datastore/tests/file_app.template.webapp
+++ b/dom/datastore/tests/file_app.template.webapp
@@ -1,14 +1,10 @@
 {
   "name": "Really Rapid Release (hosted)",
   "description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
   "launch_path": "/tests/dom/datastore/tests/TESTTOKEN",
   "icons": { "128": "default_icon" },
   "datastores-owned" : {
     "foo" : { "access": "readwrite", "description" : "This store is called foo" },
     "bar" : { "access": "readonly", "description" : "This store is called bar" }
-  },
-  "datastores-access" : {
-    "foo" : { "readonly": false, "description" : "This store is called foo" },
-    "bar" : { "readonly": true, "description" : "This store is called bar" }
   }
 }
--- a/dom/datastore/tests/file_app2.template.webapp
+++ b/dom/datastore/tests/file_app2.template.webapp
@@ -1,10 +1,10 @@
 {
   "name": "Really Rapid Release (hosted) - app 2",
   "description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
-  "launch_path": "/tests/dom/datastore/tests/file_readonly.html",
+  "launch_path": "/tests/dom/datastore/tests/TESTTOKEN",
   "icons": { "128": "default_icon" },
   "datastores-access" : {
     "foo" : { "readonly": false, "description" : "This store is called foo" },
     "bar" : { "readonly": true, "description" : "This store is called bar" }
   }
 }
deleted file mode 100644
--- a/dom/datastore/tests/file_app2.template.webapp^headers^
+++ /dev/null
@@ -1,1 +0,0 @@
-Content-Type: application/x-web-app-manifest+json
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/file_changes.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - basic operation on a readonly db</title>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+  <script type="application/javascript;version=1.7">
+
+  var gStore;
+  var gChangeId = null;
+  var gChangeOperation = null;
+
+  function is(a, b, msg) {
+    alert((a === b ? 'OK' : 'KO') + ' ' + msg)
+  }
+
+  function ok(a, msg) {
+    alert((a ? 'OK' : 'KO')+ ' ' + msg)
+  }
+
+  function cbError() {
+    alert('KO error');
+  }
+
+  function finish() {
+    alert('DONE');
+  }
+
+  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) {
+    gStore.add(value).then(function(id) {
+      is(id, expectedId, "store.add() is called");
+    }, cbError);
+  }
+
+  function testStoreUpdate(id, value) {
+    gStore.update(id, value).then(function(retId) {
+      is(id, retId, "store.update() is called with the right id");
+    }, cbError);
+  }
+
+  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);
+    runTest();
+  }
+
+  var tests = [
+    // Test for GetDataStore
+    testGetDataStores,
+
+    // Add onchange = function
+    function() {
+      gStore.onchange = eventListener;
+      runTest();
+    },
+
+    // Add
+    function() { gChangeId = 1; gChangeOperation = 'added';
+                 testStoreAdd({ number: 42 }, 1); },
+
+    // Update
+    function() { gChangeId = 1; gChangeOperation = 'updated';
+                 testStoreUpdate(1, { number: 43 }); },
+
+    // Remove
+    function() { gChangeId = 1; gChangeOperation = 'removed';
+                 testStoreRemove(1, true); },
+
+    // Remove onchange function and replace it with addEventListener
+    function() {
+      gStore.onchange = null;
+      gStore.addEventListener('change', eventListener);
+      runTest();
+    },
+
+    // Add
+    function() { gChangeId = 2; gChangeOperation = 'added';
+                 testStoreAdd({ number: 42 }, 2); },
+
+    // Update
+    function() { gChangeId = 2; gChangeOperation = 'updated';
+                 testStoreUpdate(2, { number: 43 }); },
+
+    // Remove
+    function() { gChangeId = 2; gChangeOperation = 'removed';
+                 testStoreRemove(2, true); },
+
+    // Remove event listener
+    function() {
+      gStore.removeEventListener('change', eventListener);
+      runTest();
+    },
+  ];
+
+  function runTest() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  runTest();
+  </script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/file_changes2.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - basic operation on a readonly db</title>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+  <script type="application/javascript;version=1.7">
+
+  function is(a, b, msg) {
+    alert((a === b ? 'OK' : 'KO') + ' ' + msg)
+  }
+
+  function ok(a, msg) {
+    alert((a ? 'OK' : 'KO')+ ' ' + msg)
+  }
+
+  function cbError() {
+    alert('KO error');
+  }
+
+  function finish() {
+    alert('DONE');
+  }
+
+  function eventListener(obj) {
+    ok(obj, "OnChangeListener is called with data");
+    finish();
+  }
+
+  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');
+
+    stores[0].onchange = eventListener;
+    alert('READY');
+  });
+  </script>
+</pre>
+</body>
+</html>
--- a/dom/datastore/tests/test_app_install.html
+++ b/dom/datastore/tests/test_app_install.html
@@ -5,16 +5,18 @@
   <title>Test for DataStore - install/uninstall apps</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 <div id="container"></div>
   <script type="application/javascript;version=1.7">
 
+ SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
+
   SimpleTest.waitForExplicitFinish();
 
   var gBaseURL = 'http://test/tests/dom/datastore/tests/';
   var gHostedManifestURL = gBaseURL + 'file_app.sjs?testToken=file_app_install.html';
   var gGenerator = runTest();
 
   SpecialPowers.pushPermissions(
     [{ "type": "browser", "allow": 1, "context": document },
--- a/dom/datastore/tests/test_basic.html
+++ b/dom/datastore/tests/test_basic.html
@@ -5,18 +5,17 @@
   <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>
 <div id="container"></div>
   <script type="application/javascript;version=1.7">
 
-  var gBaseURL = 'http://test/tests/dom/datastore/tests/';
-  var gHostedManifestURL = gBaseURL + 'file_app.sjs?testToken=file_basic.html';
+  var gHostedManifestURL = 'http://test/tests/dom/datastore/tests/file_app.sjs?testToken=file_basic.html';
   var gApp;
 
   function cbError() {
     ok(false, "Error callback invoked");
     finish();
   }
 
   function installApp() {
@@ -109,13 +108,14 @@
     var test = tests.shift();
     test();
   }
 
   function finish() {
     SimpleTest.finish();
   }
 
+  SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
   SimpleTest.waitForExplicitFinish();
   runTest();
   </script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/test_changes.html
@@ -0,0 +1,177 @@
+<!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 gHostedManifestURL = 'http://test/tests/dom/datastore/tests/file_app.sjs?testToken=file_changes.html';
+  var gHostedManifestURL2 = 'http://example.com/tests/dom/datastore/tests/file_app.sjs?testToken=file_changes2.html&template=file_app2.template.webapp';
+  var gApps = [];
+  var gApp2Events = 0;
+  var gStore;
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function installApp(aApp) {
+    var request = navigator.mozApps.install(aApp);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      gApps.push(request.result);
+      runTest();
+    }
+  }
+
+  function uninstallApps() {
+    if (!gApps.length) {
+      ok(true, "All done!");
+      runTest();
+      return;
+    }
+
+    var app = gApps.pop();
+    var request = navigator.mozApps.mgmt.uninstall(app);
+    request.onerror = cbError;
+    request.onsuccess = uninstallApps;
+  }
+
+  function setupApp2() {
+    var ifr = document.createElement('iframe');
+    ifr.setAttribute('mozbrowser', 'true');
+    ifr.setAttribute('mozapp', gApps[1].manifestURL);
+    ifr.setAttribute('src', gApps[1].manifest.launch_path);
+    var domParent = document.getElementById('content');
+
+    // Set us up to listen for messages from the app.
+    var listener = function(e) {
+      var message = e.detail.message;
+      if (/^OK/.exec(message)) {
+        ok(true, "Message from app: " + message);
+      } else if (/KO/.exec(message)) {
+        ok(false, "Message from app: " + message);
+      } else if (/READY/.exec(message)) {
+        ok(true, "App2 ready");
+        runTest();
+      } else if (/DONE/.exec(message)) {
+        ok(true, "Messaging from app complete");
+        ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
+        domParent.removeChild(ifr);
+        gApp2Events++;
+      }
+    }
+
+    // This event is triggered when the app calls "alert".
+    ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
+    domParent.appendChild(ifr);
+  }
+
+  function testApp1() {
+    var ifr = document.createElement('iframe');
+    ifr.setAttribute('mozbrowser', 'true');
+    ifr.setAttribute('mozapp', gApps[0].manifestURL);
+    ifr.setAttribute('src', gApps[0].manifest.launch_path);
+    var domParent = document.getElementById('content');
+
+    // Set us up to listen for messages from the app.
+    var listener = function(e) {
+      var message = e.detail.message;
+      if (/^OK/.exec(message)) {
+        ok(true, "Message from app: " + message);
+      } else if (/KO/.exec(message)) {
+        ok(false, "Message from app: " + message);
+      } else if (/DONE/.exec(message)) {
+        ok(true, "Messaging from app complete");
+        ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
+        domParent.removeChild(ifr);
+        runTest();
+      }
+    }
+
+    // This event is triggered when the app calls "alert".
+    ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
+    domParent.appendChild(ifr);
+  }
+
+  function checkApp2() {
+    ok(gApp2Events, "App2 received events");
+    runTest();
+  }
+
+  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);
+    },
+
+    // Enabling mozBrowser
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.mozBrowserFramesEnabled", true]]}, runTest);
+    },
+
+    // No confirmation needed when an app is installed
+    function() {
+      SpecialPowers.autoConfirmAppInstall(runTest);
+    },
+
+    // Installing the app1
+    function() { installApp(gHostedManifestURL); },
+
+    // Installing the app2
+    function() { installApp(gHostedManifestURL2); },
+
+    // Setup app2 for receving events
+    setupApp2,
+
+    // Run tests in app
+    testApp1,
+
+    // Check app2
+    checkApp2,
+
+    // Uninstall the apps
+    uninstallApps,
+  ];
+
+  function runTest() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
+  SimpleTest.waitForExplicitFinish();
+  runTest();
+  </script>
+</pre>
+</body>
+</html>
+
+
--- a/dom/datastore/tests/test_readonly.html
+++ b/dom/datastore/tests/test_readonly.html
@@ -4,19 +4,18 @@
   <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>
   <div id="container"></div>
   <script type="application/javascript;version=1.7">
-  var gBaseURL = 'http://test/tests/dom/datastore/tests/';
-  var gHostedManifestURL = gBaseURL + 'file_app.sjs?testToken=file_readonly.html';
-  var gHostedManifestURL2 = 'http://example.com/tests/dom/datastore/tests/file_app2.template.webapp';
+  var gHostedManifestURL = 'http://test/tests/dom/datastore/tests/file_app.sjs?testToken=file_readonly.html';
+  var gHostedManifestURL2 = 'http://example.com/tests/dom/datastore/tests/file_app.sjs?testToken=file_readonly.html&template=file_app2.template.webapp';
   var gGenerator = runTest();
 
   SpecialPowers.pushPermissions(
     [{ "type": "browser", "allow": 1, "context": document },
      { "type": "embed-apps", "allow": 1, "context": document },
      { "type": "webapps-manage", "allow": 1, "context": document }],
     function() { gGenerator.next() });
 
@@ -48,17 +47,17 @@
     request.onsuccess = continueTest;
     yield undefined;
 
     var app2 = request.result;
 
     var ifr = document.createElement('iframe');
     ifr.setAttribute('mozbrowser', 'true');
     ifr.setAttribute('mozapp', app2.manifestURL);
-    ifr.setAttribute('src', 'http://example.com/tests/dom/datastore/tests/file_readonly.html');
+    ifr.setAttribute('src', app2.manifest.launch_path);
     var domParent = document.getElementById('container');
 
     // Set us up to listen for messages from the app.
     var listener = function(e) {
       var message = e.detail.message;
       if (/^OK/.exec(message)) {
         ok(true, "Message from app: " + message);
       } else if (/KO/.exec(message)) {
@@ -90,13 +89,14 @@
     domParent.appendChild(ifr);
   }
 
   function finish() {
     SpecialPowers.clearUserPref("dom.mozBrowserFramesEnabled");
     SimpleTest.finish();
   }
 
+  SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
   SimpleTest.waitForExplicitFinish();
   SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest);
   </script>
 </body>
 </html>
--- a/dom/datastore/tests/test_revision.html
+++ b/dom/datastore/tests/test_revision.html
@@ -117,15 +117,16 @@
     var test = tests.shift();
     test();
   }
 
   function finish() {
     SimpleTest.finish();
   }
 
+  SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
   SimpleTest.waitForExplicitFinish();
   runTest();
   </script>
 </pre>
 </body>
 </html>