Bug 1078309: use a different database for each Fx Account. r=abr,paolo

This commit is contained in:
Mike de Boer 2014-10-09 13:11:24 +02:00
parent bb02e2d8cf
commit f09732faab
7 changed files with 218 additions and 27 deletions

View File

@ -25,7 +25,9 @@ XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
this.EXPORTED_SYMBOLS = ["LoopStorage"];
const kDatabaseName = "loop";
const kDatabasePrefix = "loop-";
const kDefaultDatabaseName = "default";
let gDatabaseName = kDatabasePrefix + kDefaultDatabaseName;
const kDatabaseVersion = 1;
let gWaitForOpenCallbacks = new Set();
@ -83,7 +85,7 @@ const ensureDatabaseOpen = function(onOpen) {
gWaitForOpenCallbacks.clear();
};
let openRequest = indexedDB.open(kDatabaseName, kDatabaseVersion);
let openRequest = indexedDB.open(gDatabaseName, kDatabaseVersion);
openRequest.onblocked = function(event) {
invokeCallbacks(new Error("Database cannot be upgraded cause in use: " + event.target.error));
@ -92,7 +94,7 @@ const ensureDatabaseOpen = function(onOpen) {
openRequest.onerror = function(event) {
// Try to delete the old database so that we can start this process over
// next time.
indexedDB.deleteDatabase(kDatabaseName);
indexedDB.deleteDatabase(gDatabaseName);
invokeCallbacks(new Error("Error while opening database: " + event.target.errorCode));
};
@ -109,6 +111,33 @@ const ensureDatabaseOpen = function(onOpen) {
};
};
/**
* Switch to a database with a different name by closing the current connection
* and making sure that the next connection attempt will be made using the updated
* name.
*
* @param {String} name New name of the database to switch to.
*/
const switchDatabase = function(name) {
if (!name) {
name = kDefaultDatabaseName;
}
name = kDatabasePrefix + name;
if (name == gDatabaseName) {
// This is already the current database, so there's no need to switch.
return;
}
gDatabaseName = name;
if (gDatabase) {
try {
gDatabase.close();
} finally {
gDatabase = null;
}
}
};
/**
* Start a transaction on the loop database and return it.
*
@ -179,6 +208,14 @@ const getStore = function(store, callback, mode) {
* database is loaded in memory and consumers will be able to change its structure.
*/
this.LoopStorage = Object.freeze({
/**
* @var {String} databaseName The name of the database that is currently active,
* WITHOUT the prefix
*/
get databaseName() {
return gDatabaseName.substr(kDatabasePrefix.length);
},
/**
* Open a connection to the IndexedDB database and return the database object.
*
@ -191,6 +228,16 @@ this.LoopStorage = Object.freeze({
ensureDatabaseOpen(callback);
},
/**
* Switch to a database with a different name.
*
* @param {String} name New name of the database to switch to. Defaults to
* `kDefaultDatabaseName`
*/
switchDatabase: function(name = kDefaultDatabaseName) {
switchDatabase(name);
},
/**
* Start a transaction on the loop database and return it.
* If only two arguments are passed, the default mode will be assumed and the

View File

@ -56,6 +56,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
"resource://services-common/hawkrequest.js");
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");
@ -367,6 +370,8 @@ let MozLoopServiceInternal = {
notifyStatusChanged: function(aReason = null) {
log.debug("notifyStatusChanged with reason:", aReason);
let profile = MozLoopService.userProfile;
LoopStorage.switchDatabase(profile ? profile.uid : null);
Services.obs.notifyObservers(null, "loop-status-changed", aReason);
},

View File

@ -153,14 +153,14 @@ loop.contacts = (function(_, mozL10n) {
document.body.removeEventListener("click", this._onBodyClick);
},
componentShouldUpdate: function(nextProps, nextState) {
shouldComponentUpdate: function(nextProps, nextState) {
let currContact = this.props.contact;
let nextContact = nextProps.contact;
return (
currContact.name[0] !== nextContact.name[0] ||
currContact.blocked !== nextContact.blocked ||
getPreferredEmail(currContact).value !==
getPreferredEmail(nextContact).value
getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value ||
nextState.showMenu !== this.state.showMenu
);
},
@ -215,20 +215,32 @@ loop.contacts = (function(_, mozL10n) {
const ContactsList = React.createClass({displayName: 'ContactsList',
mixins: [React.addons.LinkedStateMixin],
/**
* Contacts collection object
*/
contacts: null,
/**
* User profile
*/
_userProfile: null,
getInitialState: function() {
return {
contacts: {},
importBusy: false,
filter: "",
};
},
componentDidMount: function() {
refresh: function(callback = function() {}) {
let contactsAPI = navigator.mozLoop.contacts;
this.handleContactRemoveAll();
contactsAPI.getAll((err, contacts) => {
if (err) {
throw err;
callback(err);
return;
}
// Add contacts already present in the DB. We do this in timed chunks to
@ -239,11 +251,32 @@ loop.contacts = (function(_, mozL10n) {
});
if (contacts.length) {
setTimeout(addContactsInChunks, 0);
} else {
callback();
}
this.forceUpdate();
};
addContactsInChunks(contacts);
});
},
componentWillMount: function() {
// Take the time to initialize class variables that are used outside
// `this.state`.
this.contacts = {};
this._userProfile = navigator.mozLoop.userProfile;
},
componentDidMount: function() {
window.addEventListener("LoopStatusChanged", this._onStatusChanged);
this.refresh(err => {
if (err) {
throw err;
}
let contactsAPI = navigator.mozLoop.contacts;
// Listen for contact changes/ updates.
contactsAPI.on("add", (eventName, contact) => {
@ -261,8 +294,24 @@ loop.contacts = (function(_, mozL10n) {
});
},
componentWillUnmount: function() {
window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
},
_onStatusChanged: function() {
let profile = navigator.mozLoop.userProfile;
let currUid = this._userProfile ? this._userProfile.uid : null;
let newUid = profile ? profile.uid : null;
if (currUid != newUid) {
// On profile change (login, logout), reload all contacts.
this._userProfile = profile;
// The following will do a forceUpdate() for us.
this.refresh();
}
},
handleContactAddOrUpdate: function(contact, render = true) {
let contacts = this.state.contacts;
let contacts = this.contacts;
let guid = String(contact._guid);
contacts[guid] = contact;
if (render) {
@ -271,7 +320,7 @@ loop.contacts = (function(_, mozL10n) {
},
handleContactRemove: function(contact) {
let contacts = this.state.contacts;
let contacts = this.contacts;
let guid = String(contact._guid);
if (!contacts[guid]) {
return;
@ -281,7 +330,9 @@ loop.contacts = (function(_, mozL10n) {
},
handleContactRemoveAll: function() {
this.setState({contacts: {}});
// Do not allow any race conditions when removing all contacts.
this.contacts = {};
this.forceUpdate();
},
handleImportButtonClick: function() {
@ -364,11 +415,11 @@ loop.contacts = (function(_, mozL10n) {
handleContactAction: this.handleContactAction})
};
let shownContacts = _.groupBy(this.state.contacts, function(contact) {
let shownContacts = _.groupBy(this.contacts, function(contact) {
return contact.blocked ? "blocked" : "available";
});
let showFilter = Object.getOwnPropertyNames(this.state.contacts).length >=
let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
MIN_CONTACTS_FOR_FILTERING;
if (showFilter) {
let filter = this.state.filter.trim().toLocaleLowerCase();

View File

@ -153,14 +153,14 @@ loop.contacts = (function(_, mozL10n) {
document.body.removeEventListener("click", this._onBodyClick);
},
componentShouldUpdate: function(nextProps, nextState) {
shouldComponentUpdate: function(nextProps, nextState) {
let currContact = this.props.contact;
let nextContact = nextProps.contact;
return (
currContact.name[0] !== nextContact.name[0] ||
currContact.blocked !== nextContact.blocked ||
getPreferredEmail(currContact).value !==
getPreferredEmail(nextContact).value
getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value ||
nextState.showMenu !== this.state.showMenu
);
},
@ -215,20 +215,32 @@ loop.contacts = (function(_, mozL10n) {
const ContactsList = React.createClass({
mixins: [React.addons.LinkedStateMixin],
/**
* Contacts collection object
*/
contacts: null,
/**
* User profile
*/
_userProfile: null,
getInitialState: function() {
return {
contacts: {},
importBusy: false,
filter: "",
};
},
componentDidMount: function() {
refresh: function(callback = function() {}) {
let contactsAPI = navigator.mozLoop.contacts;
this.handleContactRemoveAll();
contactsAPI.getAll((err, contacts) => {
if (err) {
throw err;
callback(err);
return;
}
// Add contacts already present in the DB. We do this in timed chunks to
@ -239,11 +251,32 @@ loop.contacts = (function(_, mozL10n) {
});
if (contacts.length) {
setTimeout(addContactsInChunks, 0);
} else {
callback();
}
this.forceUpdate();
};
addContactsInChunks(contacts);
});
},
componentWillMount: function() {
// Take the time to initialize class variables that are used outside
// `this.state`.
this.contacts = {};
this._userProfile = navigator.mozLoop.userProfile;
},
componentDidMount: function() {
window.addEventListener("LoopStatusChanged", this._onStatusChanged);
this.refresh(err => {
if (err) {
throw err;
}
let contactsAPI = navigator.mozLoop.contacts;
// Listen for contact changes/ updates.
contactsAPI.on("add", (eventName, contact) => {
@ -261,8 +294,24 @@ loop.contacts = (function(_, mozL10n) {
});
},
componentWillUnmount: function() {
window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
},
_onStatusChanged: function() {
let profile = navigator.mozLoop.userProfile;
let currUid = this._userProfile ? this._userProfile.uid : null;
let newUid = profile ? profile.uid : null;
if (currUid != newUid) {
// On profile change (login, logout), reload all contacts.
this._userProfile = profile;
// The following will do a forceUpdate() for us.
this.refresh();
}
},
handleContactAddOrUpdate: function(contact, render = true) {
let contacts = this.state.contacts;
let contacts = this.contacts;
let guid = String(contact._guid);
contacts[guid] = contact;
if (render) {
@ -271,7 +320,7 @@ loop.contacts = (function(_, mozL10n) {
},
handleContactRemove: function(contact) {
let contacts = this.state.contacts;
let contacts = this.contacts;
let guid = String(contact._guid);
if (!contacts[guid]) {
return;
@ -281,7 +330,9 @@ loop.contacts = (function(_, mozL10n) {
},
handleContactRemoveAll: function() {
this.setState({contacts: {}});
// Do not allow any race conditions when removing all contacts.
this.contacts = {};
this.forceUpdate();
},
handleImportButtonClick: function() {
@ -364,11 +415,11 @@ loop.contacts = (function(_, mozL10n) {
handleContactAction={this.handleContactAction} />
};
let shownContacts = _.groupBy(this.state.contacts, function(contact) {
let shownContacts = _.groupBy(this.contacts, function(contact) {
return contact.blocked ? "blocked" : "available";
});
let showFilter = Object.getOwnPropertyNames(this.state.contacts).length >=
let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
MIN_CONTACTS_FOR_FILTERING;
if (showFilter) {
let filter = this.state.filter.trim().toLocaleLowerCase();

View File

@ -625,7 +625,9 @@ loop.panel = (function(_, mozL10n) {
_onStatusChanged: function() {
var profile = navigator.mozLoop.userProfile;
if (profile != this.state.userProfile) {
var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
var newUid = profile ? profile.uid : null;
if (currUid != newUid) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab("call");
}

View File

@ -625,7 +625,9 @@ loop.panel = (function(_, mozL10n) {
_onStatusChanged: function() {
var profile = navigator.mozLoop.userProfile;
if (profile != this.state.userProfile) {
var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
var newUid = profile ? profile.uid : null;
if (currUid != newUid) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab("call");
}

View File

@ -2,6 +2,11 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
const {LoopContacts} = Cu.import("resource:///modules/loop/LoopContacts.jsm", {});
const {LoopStorage} = Cu.import("resource:///modules/loop/LoopStorage.jsm", {});
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const kContacts = [{
id: 1,
@ -400,3 +405,31 @@ add_task(function* () {
Assert.strictEqual(gExpectedRemovals.length, 0, "No contact removals should be expected anymore");
Assert.strictEqual(gExpectedUpdates.length, 0, "No contact updates should be expected anymore");
});
// Test switching between different databases.
add_task(function* () {
Assert.equal(LoopStorage.databaseName, "default", "First active partition should be the default");
yield promiseLoadContacts();
let uuid = uuidgen.generateUUID().toString().replace(/[{}]+/g, "");
LoopStorage.switchDatabase(uuid);
Assert.equal(LoopStorage.databaseName, uuid, "The active partition should have changed");
yield promiseLoadContacts();
let contacts = yield promiseLoadContacts();
for (let i = 0, l = contacts.length; i < l; ++i) {
compareContacts(contacts[i], kContacts[i]);
}
LoopStorage.switchDatabase();
Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed");
LoopContacts.getAll(function(err, contacts) {
Assert.equal(err, null, "There shouldn't be an error");
for (let i = 0, l = contacts.length; i < l; ++i) {
compareContacts(contacts[i], kContacts[i]);
}
});
});