Bug 871445 patch 5 - DataStore: onchange, r=ehsan, r=bent

This commit is contained in:
Andrea Marchesini 2013-09-11 15:47:56 +02:00
parent 907c386bbd
commit d4a12c91c0
17 changed files with 597 additions and 24 deletions

View File

@ -6,6 +6,7 @@
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');

View File

@ -6,7 +6,7 @@
'use strict'
var EXPORTED_SYMBOLS = ["DataStore", "DataStoreAccess"];
this.EXPORTED_SYMBOLS = ["DataStore", "DataStoreAccess"];
function debug(s) {
// dump('DEBUG DataStore: ' + s + '\n');
@ -22,6 +22,11 @@ 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) {
@ -161,13 +166,14 @@ DataStore.prototype = {
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;
}
@ -183,7 +189,12 @@ DataStore.prototype = {
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;
}
@ -207,6 +218,7 @@ DataStore.prototype = {
exposeObject: function(aWindow, aReadOnly) {
let self = this;
let object = {
callbacks: [],
// Public interface :
@ -398,8 +410,34 @@ DataStore.prototype = {
});
},
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()
*/
@ -413,15 +451,79 @@ DataStore.prototype = {
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 } } );
}
}
};

View File

@ -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();

View File

@ -137,9 +137,9 @@ DataStoreService.prototype = {
resolver.resolve(results);
}
},
function() {
resolver.reject();
}
// if the revision is already known, we don't need to retrieve it
// again.
false
);
}
});

View File

@ -21,5 +21,6 @@ EXTRA_COMPONENTS += [
EXTRA_JS_MODULES += [
'DataStore.jsm',
'DataStoreChangeNotifier.jsm',
'DataStoreDB.jsm',
]

View File

@ -20,10 +20,12 @@ MOCHITEST_FILES = \
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

View File

@ -1,5 +1,4 @@
var gBasePath = "tests/dom/datastore/tests/";
var gAppTemplatePath = "tests/dom/datastore/tests/file_app.template.webapp";
function handleRequest(request, response) {
var query = getQuery(request);
@ -9,7 +8,11 @@ function handleRequest(request, response) {
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));
}

View File

@ -6,9 +6,5 @@
"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" }
}
}

View File

@ -1,7 +1,7 @@
{
"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" },

View File

@ -1 +0,0 @@
Content-Type: application/x-web-app-manifest+json

View File

@ -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>

View File

@ -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>

View File

@ -10,6 +10,8 @@
<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/';

View File

@ -10,8 +10,7 @@
<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() {
@ -114,6 +113,7 @@
SimpleTest.finish();
}
SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
SimpleTest.waitForExplicitFinish();
runTest();
</script>

View File

@ -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>

View File

@ -9,9 +9,8 @@
<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(
@ -53,7 +52,7 @@
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.
@ -95,6 +94,7 @@
SimpleTest.finish();
}
SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest);
</script>

View File

@ -122,6 +122,7 @@
SimpleTest.finish();
}
SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
SimpleTest.waitForExplicitFinish();
runTest();
</script>