mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
Bug 1038716 - add a contacts API. r=abr,dolske,mak,bholley
This commit is contained in:
parent
303c7b3da3
commit
07fefbe809
834
browser/components/loop/LoopContacts.jsm
Normal file
834
browser/components/loop/LoopContacts.jsm
Normal file
@ -0,0 +1,834 @@
|
||||
/* 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} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "console",
|
||||
"resource://gre/modules/devtools/Console.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
|
||||
"resource:///modules/loop/LoopStorage.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
return new EventEmitter();
|
||||
});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopContacts"];
|
||||
|
||||
const kObjectStoreName = "contacts";
|
||||
|
||||
/*
|
||||
* The table used to store contacts information contains two identifiers,
|
||||
* both of which can be used to look up entries in the table. The table
|
||||
* key path (primary index, which must be unique) is "_guid", and is
|
||||
* automatically generated by IndexedDB when an entry is first inserted.
|
||||
* The other identifier, "id", is the supposedly unique key assigned to this
|
||||
* entry by whatever service generated it (e.g., Google Contacts). While
|
||||
* this key should, in theory, be completely unique, we don't use it
|
||||
* as the key path to avoid generating errors when an external database
|
||||
* violates this constraint. This second ID is referred to as the "serviceId".
|
||||
*/
|
||||
const kKeyPath = "_guid";
|
||||
const kServiceIdIndex = "id";
|
||||
|
||||
/**
|
||||
* Contacts validation.
|
||||
*
|
||||
* To allow for future integration with the Contacts API and/ or potential
|
||||
* integration with contact synchronization across devices (including Firefox OS
|
||||
* devices), we are using objects with properties having the same names and
|
||||
* structure as those used by mozContact.
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/mozContact for more
|
||||
* information.
|
||||
*/
|
||||
const kFieldTypeString = "string";
|
||||
const kFieldTypeNumber = "number";
|
||||
const kFieldTypeNumberOrString = "number|string";
|
||||
const kFieldTypeArray = "array";
|
||||
const kFieldTypeBool = "boolean";
|
||||
const kContactFields = {
|
||||
"id": {
|
||||
// Because "id" is externally generated, it might be numeric
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"published": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"updated": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"bday": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"blocked": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"adr": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"countryName": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"locality": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"postalCode": {
|
||||
// In some (but not all) locations, postal codes can be strictly numeric
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"region": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"streetAddress": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"value": {
|
||||
type: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"tel": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"value": {
|
||||
type: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"honorificPrefix": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"givenName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"additionalName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"familyName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"honorificSuffix": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"category": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"org": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"jobTitle": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"note": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares the properties contained in an object to the definition as defined in
|
||||
* `kContactFields`.
|
||||
* If a property is encountered that is not found in the spec, an Error is thrown.
|
||||
* If a property is encountered with an invalid value, an Error is thrown.
|
||||
*
|
||||
* Please read the spec at https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information.
|
||||
*
|
||||
* @param {Object} obj The contact object, or part of it when called recursively
|
||||
* @param {Object} def The definition of properties to validate against. Defaults
|
||||
* to `kContactFields`
|
||||
*/
|
||||
const validateContact = function(obj, def = kContactFields) {
|
||||
for (let propName of Object.getOwnPropertyNames(obj)) {
|
||||
// Ignore internal properties.
|
||||
if (propName.startsWith("_")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let propDef = def[propName];
|
||||
if (!propDef) {
|
||||
throw new Error("Field '" + propName + "' is not supported for contacts");
|
||||
}
|
||||
|
||||
let val = obj[propName];
|
||||
|
||||
switch (propDef.type) {
|
||||
case kFieldTypeString:
|
||||
if (typeof val != kFieldTypeString) {
|
||||
throw new Error("Field '" + propName + "' must be of type String");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeNumberOrString:
|
||||
let type = typeof val;
|
||||
if (type != kFieldTypeNumber && type != kFieldTypeString) {
|
||||
throw new Error("Field '" + propName + "' must be of type Number or String");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeBool:
|
||||
if (typeof val != kFieldTypeBool) {
|
||||
throw new Error("Field '" + propName + "' must be of type Boolean");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeArray:
|
||||
if (!Array.isArray(val)) {
|
||||
throw new Error("Field '" + propName + "' must be an Array");
|
||||
}
|
||||
|
||||
let contains = propDef.contains;
|
||||
// If the type of `contains` is a scalar value, it means that the array
|
||||
// consists of items of only that type.
|
||||
let isScalarCheck = (typeof contains == kFieldTypeString);
|
||||
for (let arrayValue of val) {
|
||||
if (isScalarCheck) {
|
||||
if (typeof arrayValue != contains) {
|
||||
throw new Error("Field '" + propName + "' must be of type " + contains);
|
||||
}
|
||||
} else {
|
||||
validateContact(arrayValue, contains);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a method to perform multiple operations in a single transaction on the
|
||||
* contacts store.
|
||||
*
|
||||
* @param {String} operation Name of an operation supported by `IDBObjectStore`
|
||||
* @param {Array} data List of objects that will be passed to the object
|
||||
* store operation
|
||||
* @param {Function} callback Function that will be invoked once the operations
|
||||
* have finished. The first argument passed will be
|
||||
* an `Error` object or `null`. The second argument
|
||||
* will be the `data` Array, if all operations finished
|
||||
* successfully.
|
||||
*/
|
||||
const batch = function(operation, data, callback) {
|
||||
let processed = [];
|
||||
if (!LoopContactsInternal.hasOwnProperty(operation) ||
|
||||
typeof LoopContactsInternal[operation] != 'function') {
|
||||
callback(new Error ("LoopContactsInternal does not contain a '" +
|
||||
operation + "' method"));
|
||||
return;
|
||||
}
|
||||
LoopStorage.asyncForEach(data, (item, next) => {
|
||||
LoopContactsInternal[operation](item, (err, result) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
processed.push(result);
|
||||
next();
|
||||
});
|
||||
}, err => {
|
||||
if (err) {
|
||||
callback(err, processed);
|
||||
return;
|
||||
}
|
||||
callback(null, processed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend a `target` object with the properties defined in `source`.
|
||||
*
|
||||
* @param {Object} target The target object to receive properties defined in `source`
|
||||
* @param {Object} source The source object to copy properties from
|
||||
*/
|
||||
const extend = function(target, source) {
|
||||
for (let key of Object.getOwnPropertyNames(source)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
LoopStorage.on("upgrade", function(e, db) {
|
||||
if (db.objectStoreNames.contains(kObjectStoreName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the 'contacts' store as it doesn't exist yet.
|
||||
let store = db.createObjectStore(kObjectStoreName, {
|
||||
keyPath: kKeyPath,
|
||||
autoIncrement: true
|
||||
});
|
||||
store.createIndex(kServiceIdIndex, kServiceIdIndex, {unique: false});
|
||||
});
|
||||
|
||||
/**
|
||||
* The Contacts class.
|
||||
*
|
||||
* Each method that is a member of this class requires the last argument to be a
|
||||
* callback Function. MozLoopAPI will cause things to break if this invariant is
|
||||
* violated. You'll notice this as well in the documentation for each method.
|
||||
*/
|
||||
let LoopContactsInternal = Object.freeze({
|
||||
/**
|
||||
* Add a contact to the data store.
|
||||
*
|
||||
* @param {Object} details An object that will be added to the data store
|
||||
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information of this objects' structure
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if it was stored successfully.
|
||||
*/
|
||||
add: function(details, callback) {
|
||||
if (!(kServiceIdIndex in details)) {
|
||||
callback(new Error("No '" + kServiceIdIndex + "' field present"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateContact(details);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, details);
|
||||
let now = Date.now();
|
||||
// The data source should have included "published" and "updated" values
|
||||
// for any imported records, and we need to keep track of those dated for
|
||||
// sync purposes (i.e., when we add functionality to push local changes to
|
||||
// a remote server from which we originally got a contact). We also need
|
||||
// to track the time at which *we* added and most recently changed the
|
||||
// contact, so as to determine whether the local or the remote store has
|
||||
// fresher data.
|
||||
//
|
||||
// For clarity: the fields "published" and "updated" indicate when the
|
||||
// *remote* data source published and updated the contact. The fields
|
||||
// "_date_add" and "_date_lch" track when the *local* data source
|
||||
// created and updated the contact.
|
||||
contact.published = contact.published ? new Date(contact.published).getTime() : now;
|
||||
contact.updated = contact.updated ? new Date(contact.updated).getTime() : now;
|
||||
contact._date_add = contact._date_lch = now;
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.add(contact);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
contact[kKeyPath] = event.target.result;
|
||||
eventEmitter.emit("add", contact);
|
||||
callback(null, contact);
|
||||
};
|
||||
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a batch of contacts to the data store.
|
||||
*
|
||||
* @param {Array} contacts A list of contact objects to be added
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of added contacts.
|
||||
*/
|
||||
addMany: function(contacts, callback) {
|
||||
batch("add", contacts, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a contact from the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to remove
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation.
|
||||
*/
|
||||
remove: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.delete(guid);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("remove", contact);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a batch of contacts from the data store.
|
||||
*
|
||||
* @param {Array} guids A list of IDs of the contacts to remove
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of IDs, if successfull.
|
||||
*/
|
||||
removeMany: function(guids, callback) {
|
||||
batch("remove", guids, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove _all_ contacts from the data store.
|
||||
* CAUTION: this method will clear the whole data store - you won't have any
|
||||
* contacts left!
|
||||
*
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation, if successfull.
|
||||
*/
|
||||
removeAll: function(callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.clear();
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("removeAll", event.target.result);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a specific contact from the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to retrieve
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successful.
|
||||
*/
|
||||
get: function(guid, callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.get(guid);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
if (!event.target.result) {
|
||||
callback(new Error("Contact with " + kKeyPath + " '" +
|
||||
guid + "' could not be found"));;
|
||||
return;
|
||||
}
|
||||
let contact = extend({}, event.target.result);
|
||||
contact[kKeyPath] = guid;
|
||||
callback(null, contact);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a specific contact from the data store using the kServiceIdIndex
|
||||
* property.
|
||||
*
|
||||
* @param {String} serviceId String identifier of the contact to retrieve
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
getByServiceId: function(serviceId, callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let index = store.index(kServiceIdIndex);
|
||||
let request;
|
||||
try {
|
||||
request = index.get(serviceId);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
if (!event.target.result) {
|
||||
callback(new Error("Contact with " + kServiceIdIndex + " '" +
|
||||
serviceId + "' could not be found"));
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, event.target.result);
|
||||
callback(null, contact);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve _all_ contacts from the data store.
|
||||
* CAUTION: If the amount of contacts is very large (say > 100000), this method
|
||||
* may slow down your application!
|
||||
*
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
getAll: function(callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let cursorRequest = store.openCursor();
|
||||
let contactsList = [];
|
||||
|
||||
cursorRequest.onsuccess = event => {
|
||||
let cursor = event.target.result;
|
||||
// No more results, return the list.
|
||||
if (!cursor) {
|
||||
callback(null, contactsList);
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, cursor.value);
|
||||
contact[kKeyPath] = cursor.key;
|
||||
contactsList.push(contact);
|
||||
|
||||
cursor.continue();
|
||||
};
|
||||
|
||||
cursorRequest.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve an arbitrary amount of contacts from the data store.
|
||||
* CAUTION: If the amount of contacts is very large (say > 1000), this method
|
||||
* may slow down your application!
|
||||
*
|
||||
* @param {Array} guids List of contact IDs to retrieve contact objects of
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
getMany: function(guids, callback) {
|
||||
let contacts = [];
|
||||
LoopStorage.asyncParallel(guids, (guid, next) => {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
contacts.push(contact);
|
||||
next();
|
||||
});
|
||||
}, err => {
|
||||
callback(err, !err ? contacts : null);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a specific contact in the data store.
|
||||
* The contact object is modified by replacing the fields passed in the `details`
|
||||
* param and any fields not passed in are left unchanged.
|
||||
*
|
||||
* @param {Object} details An object that will be updated in the data store
|
||||
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information of this objects' structure
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
update: function(details, callback) {
|
||||
if (!(kKeyPath in details)) {
|
||||
callback(new Error("No '" + kKeyPath + "' field present"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateContact(details);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
let guid = details[kKeyPath];
|
||||
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let previous = extend({}, contact);
|
||||
// Update the contact with properties provided by `details`.
|
||||
extend(contact, details);
|
||||
|
||||
details._date_lch = Date.now();
|
||||
let request;
|
||||
try {
|
||||
request = store.put(contact);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("update", contact, previous);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Block a specific contact in the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to block
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
block: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
contact.blocked = true;
|
||||
this.update(contact, callback);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Un-block a specific contact in the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to unblock
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
unblock: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
contact.blocked = false;
|
||||
this.update(contact, callback);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Import a list of (new) contacts from an external data source.
|
||||
*
|
||||
* @param {Object} options Property bag of options for the importer
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation, if successfull.
|
||||
*/
|
||||
startImport: function(options, callback) {
|
||||
//TODO in bug 972000.
|
||||
callback(new Error("Not implemented yet!"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Search through the data store for contacts that match a certain (sub-)string.
|
||||
*
|
||||
* @param {String} query Needle to search for in our haystack of contacts
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
search: function(query, callback) {
|
||||
//TODO in bug 1037114.
|
||||
callback(new Error("Not implemented yet!"));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Public Loop Contacts API.
|
||||
*
|
||||
* LoopContacts implements the EventEmitter interface by exposing three methods -
|
||||
* `on`, `once` and `off` - to subscribe to events.
|
||||
* At this point the following events may be subscribed to:
|
||||
* - 'add': A new contact object was successfully added to the data store.
|
||||
* - 'remove': A contact was successfully removed from the data store.
|
||||
* - 'removeAll': All contacts were successfully removed from the data store.
|
||||
* - 'update': A contact object was successfully updated with changed
|
||||
* properties in the data store.
|
||||
*/
|
||||
this.LoopContacts = Object.freeze({
|
||||
add: function(details, callback) {
|
||||
return LoopContactsInternal.add(details, callback);
|
||||
},
|
||||
|
||||
addMany: function(contacts, callback) {
|
||||
return LoopContactsInternal.addMany(contacts, callback);
|
||||
},
|
||||
|
||||
remove: function(guid, callback) {
|
||||
return LoopContactsInternal.remove(guid, callback);
|
||||
},
|
||||
|
||||
removeMany: function(guids, callback) {
|
||||
return LoopContactsInternal.removeMany(guids, callback);
|
||||
},
|
||||
|
||||
removeAll: function(callback) {
|
||||
return LoopContactsInternal.removeAll(callback);
|
||||
},
|
||||
|
||||
get: function(guid, callback) {
|
||||
return LoopContactsInternal.get(guid, callback);
|
||||
},
|
||||
|
||||
getByServiceId: function(serviceId, callback) {
|
||||
return LoopContactsInternal.getByServiceId(serviceId, callback);
|
||||
},
|
||||
|
||||
getAll: function(callback) {
|
||||
return LoopContactsInternal.getAll(callback);
|
||||
},
|
||||
|
||||
getMany: function(guids, callback) {
|
||||
return LoopContactsInternal.getMany(guids, callback);
|
||||
},
|
||||
|
||||
update: function(details, callback) {
|
||||
return LoopContactsInternal.update(details, callback);
|
||||
},
|
||||
|
||||
block: function(guid, callback) {
|
||||
return LoopContactsInternal.block(guid, callback);
|
||||
},
|
||||
|
||||
unblock: function(guid, callback) {
|
||||
return LoopContactsInternal.unblock(guid, callback);
|
||||
},
|
||||
|
||||
startImport: function(options, callback) {
|
||||
return LoopContactsInternal.startImport(options, callback);
|
||||
},
|
||||
|
||||
search: function(query, callback) {
|
||||
return LoopContactsInternal.search(query, callback);
|
||||
},
|
||||
|
||||
on: (...params) => eventEmitter.on(...params),
|
||||
|
||||
once: (...params) => eventEmitter.once(...params),
|
||||
|
||||
off: (...params) => eventEmitter.off(...params)
|
||||
});
|
319
browser/components/loop/LoopStorage.jsm
Normal file
319
browser/components/loop/LoopStorage.jsm
Normal file
@ -0,0 +1,319 @@
|
||||
/* 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} = Components;
|
||||
|
||||
Cu.importGlobalProperties(["indexedDB"]);
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
return new EventEmitter();
|
||||
});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopStorage"];
|
||||
|
||||
const kDatabaseName = "loop";
|
||||
const kDatabaseVersion = 1;
|
||||
|
||||
let gWaitForOpenCallbacks = new Set();
|
||||
let gDatabase = null;
|
||||
let gClosed = false;
|
||||
|
||||
/**
|
||||
* Properly shut the database instance down. This is done on application shutdown.
|
||||
*/
|
||||
const closeDatabase = function() {
|
||||
Services.obs.removeObserver(closeDatabase, "quit-application");
|
||||
if (!gDatabase) {
|
||||
return;
|
||||
}
|
||||
gDatabase.close();
|
||||
gDatabase = null;
|
||||
gClosed = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a connection to the IndexedDB database.
|
||||
* This function is different than IndexedDBHelper.jsm provides, as it ensures
|
||||
* only one connection is open during the lifetime of this API. Callbacks are
|
||||
* queued when a connection attempt is in progress and are invoked once the
|
||||
* connection is established.
|
||||
*
|
||||
* @param {Function} onOpen Callback to be invoked once a database connection is
|
||||
* established. It takes an Error object as first argument
|
||||
* and the database connection object as second argument,
|
||||
* if successful.
|
||||
*/
|
||||
const ensureDatabaseOpen = function(onOpen) {
|
||||
if (gClosed) {
|
||||
onOpen(new Error("Database already closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gDatabase) {
|
||||
onOpen(null, gDatabase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gWaitForOpenCallbacks.has(onOpen)) {
|
||||
gWaitForOpenCallbacks.add(onOpen);
|
||||
|
||||
if (gWaitForOpenCallbacks.size !== 1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let invokeCallbacks = err => {
|
||||
for (let callback of gWaitForOpenCallbacks) {
|
||||
callback(err, gDatabase);
|
||||
}
|
||||
gWaitForOpenCallbacks.clear();
|
||||
};
|
||||
|
||||
let openRequest = indexedDB.open(kDatabaseName, kDatabaseVersion);
|
||||
|
||||
openRequest.onblocked = function(event) {
|
||||
invokeCallbacks(new Error("Database cannot be upgraded cause in use: " + event.target.error));
|
||||
};
|
||||
|
||||
openRequest.onerror = function(event) {
|
||||
// Try to delete the old database so that we can start this process over
|
||||
// next time.
|
||||
indexedDB.deleteDatabase(kDatabaseName);
|
||||
invokeCallbacks(new Error("Error while opening database: " + event.target.errorCode));
|
||||
};
|
||||
|
||||
openRequest.onupgradeneeded = function(event) {
|
||||
let db = event.target.result;
|
||||
eventEmitter.emit("upgrade", db, event.oldVersion, kDatabaseVersion);
|
||||
};
|
||||
|
||||
openRequest.onsuccess = function(event) {
|
||||
gDatabase = event.target.result;
|
||||
invokeCallbacks();
|
||||
// Close the database instance properly on application shutdown.
|
||||
Services.obs.addObserver(closeDatabase, "quit-application", false);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return it.
|
||||
*
|
||||
* @param {String} store Name of the object store to start a transaction on
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* transaction object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
const getTransaction = function(store, callback, mode) {
|
||||
ensureDatabaseOpen((err, db) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let trans;
|
||||
try {
|
||||
trans = db.transaction(store, mode);
|
||||
} catch(ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
callback(null, trans);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return the requested store.
|
||||
*
|
||||
* @param {String} store Name of the object store to retrieve
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* store object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
const getStore = function(store, callback, mode) {
|
||||
getTransaction(store, (err, trans) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, trans.objectStore(store));
|
||||
}, mode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Public Loop Storage API.
|
||||
*
|
||||
* Since IndexedDB transaction can not stand a spin of the event loop _before_
|
||||
* using a IDBTransaction object, we can't use Promise.jsm promises. Therefore
|
||||
* LoopStorage provides two async helper functions, `asyncForEach` and `asyncParallel`.
|
||||
*
|
||||
* LoopStorage implements the EventEmitter interface by exposing two methods, `on`
|
||||
* and `off`, to subscribe to events.
|
||||
* At this point only the `upgrade` event will be emitted. This happens when the
|
||||
* database is loaded in memory and consumers will be able to change its structure.
|
||||
*/
|
||||
this.LoopStorage = Object.freeze({
|
||||
/**
|
||||
* Open a connection to the IndexedDB database and return the database object.
|
||||
*
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established. It takes an Error object as first
|
||||
* argument and the database connection object as
|
||||
* second argument, if successful.
|
||||
*/
|
||||
getSingleton: function(callback) {
|
||||
ensureDatabaseOpen(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return it.
|
||||
* If only two arguments are passed, the default mode will be assumed and the
|
||||
* second argument is assumed to be a callback.
|
||||
*
|
||||
* @param {String} store Name of the object store to start a transaction on
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* transaction object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
getTransaction: function(store, callback, mode = "readonly") {
|
||||
getTransaction(store, callback, mode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return the requested store.
|
||||
* If only two arguments are passed, the default mode will be assumed and the
|
||||
* second argument is assumed to be a callback.
|
||||
*
|
||||
* @param {String} store Name of the object store to retrieve
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* store object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
getStore: function(store, callback, mode = "readonly") {
|
||||
getStore(store, callback, mode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an async function in serial on each of the list items and call a
|
||||
* callback Function when all list items are done.
|
||||
* IMPORTANT: only use this iteration method if you are sure that the operations
|
||||
* performed in `onItem` are guaranteed to be async in the success case.
|
||||
*
|
||||
* @param {Array} list Non-empty list of items to iterate
|
||||
* @param {Function} onItem Callback to invoke for each item in the list. It
|
||||
* takes the item is first argument and a callback
|
||||
* function as second, which is to be invoked once
|
||||
* the consumer is done with its async operation. If
|
||||
* an error is passed as the first argument to this
|
||||
* callback function, the iteration will stop and
|
||||
* `onDone` callback will be invoked with that error.
|
||||
* @param {callback} onDone Callback to invoke when the list is completed or
|
||||
* on error. It takes an Error object as first
|
||||
* argument.
|
||||
*/
|
||||
asyncForEach: function(list, onItem, onDone) {
|
||||
let i = 0;
|
||||
let len = list.length;
|
||||
|
||||
if (!len) {
|
||||
onDone(new Error("Argument error: empty list"));
|
||||
return;
|
||||
}
|
||||
|
||||
onItem(list[i], function handler(err) {
|
||||
if (err) {
|
||||
onDone(err);
|
||||
return;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (i < len) {
|
||||
onItem(list[i], handler, i);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
}, i);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an async function in parallel on each of the list items and call a
|
||||
* callback Function when all list items are done.
|
||||
* IMPORTANT: only use this iteration method if you are sure that the operations
|
||||
* performed in `onItem` are guaranteed to be async in the success case.
|
||||
*
|
||||
* @param {Array} list Non-empty list of items to iterate
|
||||
* @param {Function} onItem Callback to invoke for each item in the list. It
|
||||
* takes the item is first argument and a callback
|
||||
* function as second, which is to be invoked once
|
||||
* the consumer is done with its async operation. If
|
||||
* an error is passed as the first argument to this
|
||||
* callback function, the iteration will stop and
|
||||
* `onDone` callback will be invoked with that error.
|
||||
* @param {callback} onDone Callback to invoke when the list is completed or
|
||||
* on error. It takes an Error object as first
|
||||
* argument.
|
||||
*/
|
||||
asyncParallel: function(list, onItem, onDone) {
|
||||
let i = 0;
|
||||
let done = 0;
|
||||
let callbackCalled = false;
|
||||
let len = list.length;
|
||||
|
||||
if (!len) {
|
||||
onDone(new Error("Argument error: empty list"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (; i < len; ++i) {
|
||||
onItem(list[i], function handler(err) {
|
||||
if (callbackCalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
onDone(err);
|
||||
callbackCalled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (++done === len) {
|
||||
onDone();
|
||||
callbackCalled = true;
|
||||
}
|
||||
}, i);
|
||||
}
|
||||
},
|
||||
|
||||
on: (...params) => eventEmitter.on(...params),
|
||||
|
||||
off: (...params) => eventEmitter.off(...params)
|
||||
});
|
@ -9,6 +9,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/loop/MozLoopService.jsm");
|
||||
Cu.import("resource:///modules/loop/LoopContacts.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
|
||||
"resource://gre/modules/MozSocialAPI.jsm");
|
||||
@ -22,6 +23,62 @@ XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
|
||||
"nsIClipboardHelper");
|
||||
this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
|
||||
|
||||
/**
|
||||
* Trying to clone an Error object into a different container will yield an error.
|
||||
* We can work around this by copying the properties we care about onto a regular
|
||||
* object.
|
||||
*
|
||||
* @param {Error} error Error object to copy
|
||||
* @param {nsIDOMWindow} targetWindow The content window to attach the API
|
||||
*/
|
||||
const cloneErrorObject = function(error, targetWindow) {
|
||||
let obj = new targetWindow.Error();
|
||||
for (let prop of Object.getOwnPropertyNames(error)) {
|
||||
obj[prop] = String(error[prop]);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject any API containing _only_ function properties into the given window.
|
||||
*
|
||||
* @param {Object} api Object containing functions that need to
|
||||
* be exposed to content
|
||||
* @param {nsIDOMWindow} targetWindow The content window to attach the API
|
||||
*/
|
||||
const injectObjectAPI = function(api, targetWindow) {
|
||||
let injectedAPI = {};
|
||||
// Wrap all the methods in `api` to help results passed to callbacks get
|
||||
// through the priv => unpriv barrier with `Cu.cloneInto()`.
|
||||
Object.keys(api).forEach(func => {
|
||||
injectedAPI[func] = function(...params) {
|
||||
let callback = params.pop();
|
||||
api[func](...params, function(...results) {
|
||||
results = results.map(result => {
|
||||
if (result && typeof result == "object") {
|
||||
// Inspect for an error this way, because the Error object is special.
|
||||
if (result.constructor.name == "Error") {
|
||||
return cloneErrorObject(result.message)
|
||||
}
|
||||
return Cu.cloneInto(result, targetWindow);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
callback(...results);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
let contentObj = Cu.cloneInto(injectedAPI, targetWindow, {cloneFunctions: true});
|
||||
// Since we deny preventExtensions on XrayWrappers, because Xray semantics make
|
||||
// it difficult to act like an object has actually been frozen, we try to seal
|
||||
// the `contentObj` without Xrays.
|
||||
try {
|
||||
Object.seal(Cu.waiveXrays(contentObj));
|
||||
} catch (ex) {}
|
||||
return contentObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject the loop API into the given window. The caller must be sure the
|
||||
* window is a loop content window (eg, a panel, chatwindow, or similar).
|
||||
@ -34,6 +91,7 @@ function injectLoopAPI(targetWindow) {
|
||||
let ringer;
|
||||
let ringerStopper;
|
||||
let appVersionInfo;
|
||||
let contactsAPI;
|
||||
|
||||
let api = {
|
||||
/**
|
||||
@ -61,6 +119,21 @@ function injectLoopAPI(targetWindow) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the contacts API.
|
||||
*
|
||||
* @returns {Object} The contacts API object
|
||||
*/
|
||||
contacts: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (contactsAPI) {
|
||||
return contactsAPI;
|
||||
}
|
||||
return contactsAPI = injectObjectAPI(LoopContacts, targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns translated strings associated with an element. Designed
|
||||
* for use with l10n.js
|
||||
|
@ -13,6 +13,8 @@ BROWSER_CHROME_MANIFESTS += [
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.loop += [
|
||||
'LoopContacts.jsm',
|
||||
'LoopStorage.jsm',
|
||||
'MozLoopAPI.jsm',
|
||||
'MozLoopPushHandler.jsm',
|
||||
'MozLoopWorker.js',
|
||||
|
Loading…
Reference in New Issue
Block a user