Bug 1095425 - Convert PlacesTransactions to the new Bookmarks.jsm API. r=standard8 draft
authorMarco Bonardo <mbonardo@mozilla.com> <mbonardo@mozilla.com>
Tue, 28 Apr 2015 15:19:43 +0300
changeset 602671 1f8a73a5e84f34cd683abfba854c8413b0cd49b8
parent 602670 d536973fe668c6c6046fc3fda82e24f3379e3713
child 602947 715f9ab26505dedaa4e32a5bcac8053b8f2bf6cd
push id66480
push userbmo:mak77@bonardo.net
push dateFri, 30 Jun 2017 13:35:41 +0000
reviewersstandard8
bugs1095425
milestone56.0a1
Bug 1095425 - Convert PlacesTransactions to the new Bookmarks.jsm API. r=standard8 MozReview-Commit-ID: 12pPCGmzV4X
browser/components/migration/tests/unit/test_automigration.js
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/PlacesTransactions.jsm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/tests/unit/test_async_transactions.js
--- a/browser/components/migration/tests/unit/test_automigration.js
+++ b/browser/components/migration/tests/unit/test_automigration.js
@@ -661,17 +661,20 @@ add_task(async function checkUndoVisitsS
                "2 example.org visits should have persisted (out of 4).");
   Assert.equal(await visitsForURL("http://www.unrelated.org/"), 1,
                "1 unrelated.org visits should have persisted as it's not involved in the import.");
   await PlacesTestUtils.clearHistory();
 });
 
 add_task(async function checkHistoryRemovalCompletion() {
   AutoMigrate._errorMap = {bookmarks: 0, visits: 0, logins: 0};
-  await AutoMigrate._removeSomeVisits([{url: "http://www.example.com/", limit: -1}]);
+  await AutoMigrate._removeSomeVisits([{url: "http://www.example.com/",
+                                        first: 0,
+                                        last: PlacesUtils.toPRTime(new Date()),
+                                        limit: -1}]);
   ok(true, "Removing visits should complete even if removing some visits failed.");
   Assert.equal(AutoMigrate._errorMap.visits, 1, "Should have logged the error for visits.");
 
   // Unfortunately there's not a reliable way to make removing bookmarks be
   // unhappy unless the DB is messed up (e.g. contains children but has
   // parents removed already).
   await AutoMigrate._removeUnchangedBookmarks([
     {guid: PlacesUtils.bookmarks, lastModified: new Date(0), parentGuid: 0},
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -1987,17 +1987,19 @@ function removeSameValueProperties(dest,
  */
 function rowsToItemsArray(rows) {
   return rows.map(row => {
     let item = {};
     for (let prop of ["guid", "index", "type"]) {
       item[prop] = row.getResultByName(prop);
     }
     for (let prop of ["dateAdded", "lastModified"]) {
-      item[prop] = PlacesUtils.toDate(row.getResultByName(prop));
+      let value = row.getResultByName(prop);
+      if (value)
+        item[prop] = PlacesUtils.toDate(value);
     }
     for (let prop of ["title", "parentGuid", "url" ]) {
       let val = row.getResultByName(prop);
       if (val)
         item[prop] = prop === "url" ? new URL(val) : val;
     }
     for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId",
                       "_syncStatus"]) {
--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -130,17 +130,17 @@ this.EXPORTED_SYMBOLS = ["PlacesTransact
  *
  * WARNING: "nested" batches are not supported, if you call batch while another
  * batch is still running, the new batch is enqueued with all other PTM work
  * and thus not run until the running batch ends. The same goes for undo, redo
  * and clearTransactionsHistory (note batches cannot be done partially, meaning
  * undo and redo calls that during a batch are just enqueued).
  *
  * *****************************************************************************
- * IT"S PARTICULARLY IMPORTANT NOT TO YIELD ANY PROMISE RETURNED BY ANY OF
+ * IT'S PARTICULARLY IMPORTANT NOT TO await ANY PROMISE RETURNED BY ANY OF
  * THESE METHODS (undo, redo, clearTransactionsHistory) FROM A BATCH FUNCTION.
  * UNTIL WE FIND A WAY TO THROW IN THAT CASE (SEE BUG 1091446) DOING SO WILL
  * COMPLETELY BREAK PTM UNTIL SHUTDOWN, NOT ALLOWING THE EXECUTION OF ANY
  * TRANSACTION!
  * *****************************************************************************
  *
  * Serialization
  * -------------
@@ -169,26 +169,33 @@ this.EXPORTED_SYMBOLS = ["PlacesTransact
  *   [2nd redo txn, 1st redo txn],  <= 1st redo entry
  *   [1st undo txn, 2nd undo txn],  <= 1st undo entry
  *   [1st undo txn, 2nd undo txn]   <= 2nd undo entry ]
  * undoPostion: 2.
  *
  * Note that when a new entry is created, all redo entries are removed.
  */
 
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-Components.utils.import("resource://gre/modules/Services.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+const TRANSACTIONS_QUEUE_TIMEOUT_MS = 240000; // 4 Mins.
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/Console.jsm");
 
-Components.utils.importGlobalProperties(["URL"]);
+Cu.importGlobalProperties(["URL"]);
+
+function setTimeout(callback, ms) {
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
+}
 
 var TransactionsHistory = [];
 TransactionsHistory.__proto__ = {
   __proto__: Array.prototype,
 
   // The index of the first undo entry (if any) - See the documentation
   // at the top of this file.
   _undoPosition: 0,
@@ -207,74 +214,74 @@ TransactionsHistory.__proto__ = {
   // Outside of this module, the API of transactions is inaccessible, and so
   // are any internal properties.  To achieve that, transactions are proxified
   // in their constructors.  This maps the proxies to their respective raw
   // objects.
   proxifiedToRaw: new WeakMap(),
 
   /**
    * Proxify a transaction object for consumers.
-   * @param aRawTransaction
+   * @param rawTransaction
    *        the raw transaction object.
    * @return the proxified transaction object.
    * @see getRawTransaction for retrieving the raw transaction.
    */
-  proxifyTransaction(aRawTransaction) {
+  proxifyTransaction(rawTransaction) {
     let proxy = Object.freeze({
       transact() {
         return TransactionsManager.transact(this);
       }
     });
-    this.proxifiedToRaw.set(proxy, aRawTransaction);
+    this.proxifiedToRaw.set(proxy, rawTransaction);
     return proxy;
   },
 
   /**
    * Check if the given object is a the proxy object for some transaction.
    * @param aValue
    *        any JS value.
    * @return true if aValue is the proxy object for some transaction, false
    * otherwise.
    */
-  isProxifiedTransactionObject(aValue) {
-    return this.proxifiedToRaw.has(aValue);
+  isProxifiedTransactionObject(value) {
+    return this.proxifiedToRaw.has(value);
   },
 
   /**
    * Get the raw transaction for the given proxy.
    * @param aProxy
    *        the proxy object
    * @return the transaction proxified by aProxy; |undefined| is returned if
    * aProxy is not a proxified transaction.
    */
-  getRawTransaction(aProxy) {
-    return this.proxifiedToRaw.get(aProxy);
+  getRawTransaction(proxy) {
+    return this.proxifiedToRaw.get(proxy);
   },
 
   /**
    * Add a transaction either as a new entry, if forced or if there are no undo
    * entries, or to the top undo entry.
    *
    * @param aProxifiedTransaction
    *        the proxified transaction object to be added to the transaction
    *        history.
    * @param [optional] aForceNewEntry
    *        Force a new entry for the transaction. Default: false.
    *        If false, an entry will we created only if there's no undo entry
    *        to extend.
    */
-  add(aProxifiedTransaction, aForceNewEntry = false) {
-    if (!this.isProxifiedTransactionObject(aProxifiedTransaction))
+  add(proxifiedTransaction, forceNewEntry = false) {
+    if (!this.isProxifiedTransactionObject(proxifiedTransaction))
       throw new Error("aProxifiedTransaction is not a proxified transaction");
 
-    if (this.length == 0 || aForceNewEntry) {
+    if (this.length == 0 || forceNewEntry) {
       this.clearRedoEntries();
-      this.unshift([aProxifiedTransaction]);
+      this.unshift([proxifiedTransaction]);
     } else {
-      this[this.undoPosition].unshift(aProxifiedTransaction);
+      this[this.undoPosition].unshift(proxifiedTransaction);
     }
   },
 
   /**
    * Clear all undo entries.
    */
   clearUndoEntries() {
     if (this.undoPosition < this.length)
@@ -302,40 +309,40 @@ TransactionsHistory.__proto__ = {
   }
 };
 
 
 var PlacesTransactions = {
   /**
    * @see Batches in the module documentation.
    */
-  batch(aToBatch) {
-    if (Array.isArray(aToBatch)) {
-      if (aToBatch.length == 0)
-        throw new Error("aToBatch must not be an empty array");
+  batch(transactionsToBatch) {
+    if (Array.isArray(transactionsToBatch)) {
+      if (transactionsToBatch.length == 0)
+        throw new Error("Must pass a non-empty array");
 
-      if (aToBatch.some(
+      if (transactionsToBatch.some(
            o => !TransactionsHistory.isProxifiedTransactionObject(o))) {
-        throw new Error("aToBatch contains non-transaction element");
+        throw new Error("Must pass only transaction entries");
       }
       return TransactionsManager.batch(async function() {
-        for (let txn of aToBatch) {
+        for (let txn of transactionsToBatch) {
           try {
             await txn.transact();
           } catch (ex) {
             console.error(ex);
           }
         }
       });
     }
-    if (typeof(aToBatch) == "function") {
-      return TransactionsManager.batch(aToBatch);
+    if (typeof(transactionsToBatch) == "function") {
+      return TransactionsManager.batch(transactionsToBatch);
     }
 
-    throw new Error("aToBatch must be either a function or a transactions array");
+    throw new Error("Must pass either a function or a transactions array");
   },
 
   /**
    * Asynchronously undo the transaction immediately after the current undo
    * position in the transactions history in the reverse order, if any, and
    * adjusts the undo position.
    *
    * @return {Promises).  The promise always resolves.
@@ -358,54 +365,54 @@ var PlacesTransactions = {
   redo() {
     return TransactionsManager.redo();
   },
 
   /**
    * Asynchronously clear the undo, redo, or all entries from the transactions
    * history.
    *
-   * @param [optional] aUndoEntries
+   * @param [optional] undoEntries
    *        Whether or not to clear undo entries.  Default: true.
-   * @param [optional] aRedoEntries
+   * @param [optional] redoEntries
    *        Whether or not to clear undo entries.  Default: true.
    *
    * @return {Promises).  The promise always resolves.
    * @throws if both aUndoEntries and aRedoEntries are false.
    * @note All undo manager operations are queued. This means that transactions
    * history may change by the time your request is fulfilled.
    */
-  clearTransactionsHistory(aUndoEntries = true, aRedoEntries = true) {
-    return TransactionsManager.clearTransactionsHistory(aUndoEntries, aRedoEntries);
+  clearTransactionsHistory(undoEntries = true, redoEntries = true) {
+    return TransactionsManager.clearTransactionsHistory(undoEntries, redoEntries);
   },
 
   /**
    * The numbers of entries in the transactions history.
    */
   get length() {
     return TransactionsHistory.length;
   },
 
   /**
    * Get the transaction history entry at a given index.  Each entry consists
    * of one or more transaction objects.
    *
-   * @param aIndex
+   * @param index
    *        the index of the entry to retrieve.
    * @return an array of transaction objects in their undo order (that is,
    * reversely to the order they were executed).
    * @throw if aIndex is invalid (< 0 or >= length).
    * @note the returned array is a clone of the history entry and is not
    * kept in sync with the original entry if it changes.
    */
-  entry(aIndex) {
-    if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
+  entry(index) {
+    if (!Number.isInteger(index) || index < 0 || index >= this.length)
       throw new Error("Invalid index");
 
-    return TransactionsHistory[aIndex];
+    return TransactionsHistory[index];
   },
 
   /**
    * The index of the top undo entry in the transactions history.
    * If there are no undo entries, it equals to |length|.
    * Entries past this point
    * Entries at and past this point are redo entries.
    */
@@ -440,41 +447,58 @@ var PlacesTransactions = {
 function Enqueuer() {
   this._promise = Promise.resolve();
 }
 Enqueuer.prototype = {
   /**
    * Spawn a functions once all previous functions enqueued are done running,
    * and all promises passed to alsoWaitFor are no longer pending.
    *
-   * @param   aFunc
+   * @param   func
    *          a function returning a promise.
    * @return  a promise that resolves once aFunc is done running. The promise
    *          "mirrors" the promise returned by aFunc.
    */
-  enqueue(aFunc) {
-    let promise = this._promise.then(aFunc);
+  enqueue(func) {
+    // If a transaction awaits on a never resolved promise, or is mistakenly
+    // nested, it could hang the transactions queue forever.  Thus we timeout
+    // the execution after a meaningful amount of time, to ensure in any case
+    // we'll proceed after a while.
+    let timeoutPromise = new Promise((resolve, reject) => {
+      setTimeout(() => reject(new Error("PlacesTransaction timeout, most likely caused by unresolved pending work.")),
+                 TRANSACTIONS_QUEUE_TIMEOUT_MS);
+    });
+    let promise = this._promise.then(() => Promise.race([func(), timeoutPromise]));
 
     // Propagate exceptions to the caller, but dismiss them internally.
     this._promise = promise.catch(console.error);
     return promise;
   },
 
   /**
    * Same as above, but for a promise returned by a function that already run.
    * This is useful, for example, for serializing transact calls with undo calls,
    * even though transact has its own Enqueuer.
    *
-   * @param aPromise
+   * @param otherPromise
    *        any promise.
    */
-  alsoWaitFor(aPromise) {
+  alsoWaitFor(otherPromise) {
     // We don't care if aPromise resolves or rejects, but just that is not
     // pending anymore.
-    let promise = aPromise.catch(console.error);
+    // If a transaction awaits on a never resolved promise, or is mistakenly
+    // nested, it could hang the transactions queue forever.  Thus we timeout
+    // the execution after a meaningful amount of time, to ensure in any case
+    // we'll proceed after a while.
+    let timeoutPromise = new Promise((resolve, reject) => {
+      setTimeout(() => reject(new Error("PlacesTransaction timeout, most likely caused by unresolved pending work.")),
+                 TRANSACTIONS_QUEUE_TIMEOUT_MS);
+    });
+    let promise = Promise.race([otherPromise, timeoutPromise])
+                         .catch(console.error);
     this._promise = Promise.all([this._promise, promise]);
   },
 
   /**
    * The promise for this queue.
    */
   get promise() {
     return this._promise;
@@ -496,53 +520,53 @@ var TransactionsManager = {
   // executed successfully).
   _createdBatchEntry: false,
 
   // Transactions object should never be recycled (that is, |execute| should
   // only be called once (or not at all) after they're constructed.
   // This keeps track of all transactions which were executed.
   _executedTransactions: new WeakSet(),
 
-  transact(aTxnProxy) {
-    let rawTxn = TransactionsHistory.getRawTransaction(aTxnProxy);
+  transact(txnProxy) {
+    let rawTxn = TransactionsHistory.getRawTransaction(txnProxy);
     if (!rawTxn)
       throw new Error("|transact| was called with an unexpected object");
 
     if (this._executedTransactions.has(rawTxn))
       throw new Error("Transactions objects may not be recycled.");
 
     // Add it in advance so one doesn't accidentally do
     // sameTxn.transact(); sameTxn.transact();
     this._executedTransactions.add(rawTxn);
 
     let promise = this._transactEnqueuer.enqueue(async () => {
       // Don't try to catch exceptions. If execute fails, we better not add the
       // transaction to the undo stack.
       let retval = await rawTxn.execute();
 
       let forceNewEntry = !this._batching || !this._createdBatchEntry;
-      TransactionsHistory.add(aTxnProxy, forceNewEntry);
+      TransactionsHistory.add(txnProxy, forceNewEntry);
       if (this._batching)
         this._createdBatchEntry = true;
 
       this._updateCommandsOnActiveWindow();
       return retval;
     });
     this._mainEnqueuer.alsoWaitFor(promise);
     return promise;
   },
 
-  batch(aTask) {
+  batch(task) {
     return this._mainEnqueuer.enqueue(async () => {
       this._batching = true;
       this._createdBatchEntry = false;
       let rv;
       try {
         // We should return here, but bug 958949 makes that impossible.
-        rv = await aTask();
+        rv = await task();
       } finally {
         this._batching = false;
         this._createdBatchEntry = false;
       }
       return rv;
     });
   },
 
@@ -556,18 +580,17 @@ var TransactionsManager = {
         return;
 
       for (let txnProxy of entry) {
         try {
           await TransactionsHistory.getRawTransaction(txnProxy).undo();
         } catch (ex) {
           // If one transaction is broken, it's not safe to work with any other
           // undo entry.  Report the error and clear the undo history.
-          console.error(ex,
-                        "Couldn't undo a transaction, clearing all undo entries.");
+          console.error(ex, "Can't undo a transaction, clearing undo entries.");
           TransactionsHistory.clearUndoEntries();
           return;
         }
       }
       TransactionsHistory._undoPosition++;
       this._updateCommandsOnActiveWindow();
     });
     this._transactEnqueuer.alsoWaitFor(promise);
@@ -581,44 +604,44 @@ var TransactionsManager = {
     let promise = this._mainEnqueuer.enqueue(async () => {
       let entry = TransactionsHistory.topRedoEntry;
       if (!entry)
         return;
 
       for (let i = entry.length - 1; i >= 0; i--) {
         let transaction = TransactionsHistory.getRawTransaction(entry[i]);
         try {
-          if (transaction.redo)
+          if (transaction.redo) {
             await transaction.redo();
-          else
+          } else {
             await transaction.execute();
+          }
         } catch (ex) {
           // If one transaction is broken, it's not safe to work with any other
           // redo entry. Report the error and clear the undo history.
-          console.error(ex,
-                        "Couldn't redo a transaction, clearing all redo entries.");
+          console.error(ex, "Can't redo a transaction, clearing redo entries.");
           TransactionsHistory.clearRedoEntries();
           return;
         }
       }
       TransactionsHistory._undoPosition--;
       this._updateCommandsOnActiveWindow();
     });
 
     this._transactEnqueuer.alsoWaitFor(promise);
     return promise;
   },
 
-  clearTransactionsHistory(aUndoEntries, aRedoEntries) {
+  clearTransactionsHistory(undoEntries, redoEntries) {
     let promise = this._mainEnqueuer.enqueue(function() {
-      if (aUndoEntries && aRedoEntries)
+      if (undoEntries && redoEntries)
         TransactionsHistory.clearAllEntries();
-      else if (aUndoEntries)
+      else if (undoEntries)
         TransactionsHistory.clearUndoEntries();
-      else if (aRedoEntries)
+      else if (redoEntries)
         TransactionsHistory.clearRedoEntries();
       else
         throw new Error("either aUndoEntries or aRedoEntries should be true");
     });
 
     this._transactEnqueuer.alsoWaitFor(promise);
     return promise;
   },
@@ -626,17 +649,19 @@ var TransactionsManager = {
   // Updates commands in the undo group of the active window commands.
   // Inactive windows commands will be updated on focus.
   _updateCommandsOnActiveWindow() {
     // Updating "undo" will cause a group update including "redo".
     try {
       let win = Services.focus.activeWindow;
       if (win)
         win.updateCommands("undo");
-    } catch (ex) { console.error(ex, "Couldn't update undo commands"); }
+    } catch (ex) {
+      console.error(ex, "Couldn't update undo commands.");
+    }
   }
 };
 
 /**
  * Internal helper for defining the standard transactions and their input.
  * It takes the required and optional properties, and generates the public
  * constructor (which takes the input in the form of a plain object) which,
  * when called, creates the argument-less "public" |execute| method by binding
@@ -646,222 +671,210 @@ var TransactionsManager = {
  * If this seems confusing, look at the consumers.
  *
  * This magic serves two purposes:
  * (1) It completely hides the transactions' internals from the module
  *     consumers.
  * (2) It keeps each transaction implementation to what is about, bypassing
  *     all this bureaucracy while still validating input appropriately.
  */
-function DefineTransaction(aRequiredProps = [], aOptionalProps = []) {
-  for (let prop of [...aRequiredProps, ...aOptionalProps]) {
+function DefineTransaction(requiredProps = [], optionalProps = []) {
+  for (let prop of [...requiredProps, ...optionalProps]) {
     if (!DefineTransaction.inputProps.has(prop))
       throw new Error("Property '" + prop + "' is not defined");
   }
 
-  let ctor = function(aInput) {
+  let ctor = function(input) {
     // We want to support both syntaxes:
     // let t = new PlacesTransactions.NewBookmark(),
     // let t = PlacesTransactions.NewBookmark()
     if (this == PlacesTransactions)
-      return new ctor(aInput);
+      return new ctor(input);
 
-    if (aRequiredProps.length > 0 || aOptionalProps.length > 0) {
+    if (requiredProps.length > 0 || optionalProps.length > 0) {
       // Bind the input properties to the arguments of execute.
-      let input = DefineTransaction.verifyInput(aInput, aRequiredProps,
-                                                aOptionalProps);
-      let executeArgs = [this,
-                         ...aRequiredProps.map(prop => input[prop]),
-                         ...aOptionalProps.map(prop => input[prop])];
-      this.execute = Function.bind.apply(this.execute, executeArgs);
+      input = DefineTransaction.verifyInput(input, requiredProps, optionalProps);
+      this.execute = this.execute.bind(this, input);
     }
     return TransactionsHistory.proxifyTransaction(this);
   };
   return ctor;
 }
 
-function simpleValidateFunc(aCheck) {
+function simpleValidateFunc(checkFn) {
   return v => {
-    if (!aCheck(v))
+    if (!checkFn(v))
       throw new Error("Invalid value");
     return v;
   };
 }
 
 DefineTransaction.strValidate = simpleValidateFunc(v => typeof(v) == "string");
 DefineTransaction.strOrNullValidate =
   simpleValidateFunc(v => typeof(v) == "string" || v === null);
 DefineTransaction.indexValidate =
   simpleValidateFunc(v => Number.isInteger(v) &&
-                          v >= PlacesUtils.bookmarks.DEFAULT_INDEX);
+                     v >= PlacesUtils.bookmarks.DEFAULT_INDEX);
 DefineTransaction.guidValidate =
   simpleValidateFunc(v => /^[a-zA-Z0-9\-_]{12}$/.test(v));
 
 function isPrimitive(v) {
   return v === null || (typeof(v) != "object" && typeof(v) != "function");
 }
 
 DefineTransaction.annotationObjectValidate = function(obj) {
-  let checkProperty = (aPropName, aRequired, aCheckFunc) => {
-    if (aPropName in obj)
-      return aCheckFunc(obj[aPropName]);
+  let checkProperty = (prop, required, checkFn) => {
+    if (prop in obj)
+      return checkFn(obj[prop]);
 
-    return !aRequired;
+    return !required;
   };
 
   if (obj &&
       checkProperty("name", true, v => typeof(v) == "string" && v.length > 0) &&
       checkProperty("expires", false, Number.isInteger) &&
       checkProperty("flags", false, Number.isInteger) &&
       checkProperty("value", false, isPrimitive) ) {
     // Nothing else should be set
     let validKeys = ["name", "value", "flags", "expires"];
-    if (Object.keys(obj).every( (k) => validKeys.includes(k)))
+    if (Object.keys(obj).every(k => validKeys.includes(k)))
       return obj;
   }
   throw new Error("Invalid annotation object");
 };
 
 DefineTransaction.urlValidate = function(url) {
-  // When this module is updated to use Bookmarks.jsm, we should actually
-  // convert nsIURIs/spec to URL objects.
-  if (url instanceof Components.interfaces.nsIURI)
-    return url;
-  let spec = url instanceof URL ? url.href : url;
-  return NetUtil.newURI(spec);
+  if (url instanceof Ci.nsIURI)
+    return new URL(url.spec);
+  return new URL(url);
 };
 
 DefineTransaction.inputProps = new Map();
-DefineTransaction.defineInputProps =
-function(aNames, aValidationFunction, aDefaultValue) {
-  for (let name of aNames) {
+DefineTransaction.defineInputProps = function(names, validateFn, defaultValue) {
+  for (let name of names) {
     this.inputProps.set(name, {
-      validateValue(aValue) {
-        if (aValue === undefined)
-          return aDefaultValue;
+      validateValue(value) {
+        if (value === undefined)
+          return defaultValue;
         try {
-          return aValidationFunction(aValue);
+          return validateFn(value);
         } catch (ex) {
           throw new Error(`Invalid value for input property ${name}`);
         }
       },
 
-      validateInput(aInput, aRequired) {
-        if (aRequired && !(name in aInput))
+      validateInput(input, required) {
+        if (required && !(name in input))
           throw new Error(`Required input property is missing: ${name}`);
-        return this.validateValue(aInput[name]);
+        return this.validateValue(input[name]);
       },
 
       isArrayProperty: false
     });
   }
 };
 
-DefineTransaction.defineArrayInputProp =
-function(aName, aBasePropertyName) {
-  let baseProp = this.inputProps.get(aBasePropertyName);
+DefineTransaction.defineArrayInputProp = function(name, basePropertyName) {
+  let baseProp = this.inputProps.get(basePropertyName);
   if (!baseProp)
-    throw new Error(`Unknown input property: ${aBasePropertyName}`);
+    throw new Error(`Unknown input property: ${basePropertyName}`);
 
-  this.inputProps.set(aName, {
+  this.inputProps.set(name, {
     validateValue(aValue) {
       if (aValue == undefined)
         return [];
 
       if (!Array.isArray(aValue))
-        throw new Error(`${aName} input property value must be an array`);
+        throw new Error(`${name} input property value must be an array`);
 
       // This also takes care of abandoning the global scope of the input
       // array (through Array.prototype).
       return aValue.map(baseProp.validateValue);
     },
 
     // We allow setting either the array property itself (e.g. urls), or a
     // single element of it (url, in that example), that is then transformed
     // into a single-element array.
-    validateInput(aInput, aRequired) {
-      if (aName in aInput) {
+    validateInput(input, required) {
+      if (name in input) {
         // It's not allowed to set both though.
-        if (aBasePropertyName in aInput) {
-          throw new Error(`It is not allowed to set both ${aName} and
-                          ${aBasePropertyName} as  input properties`);
+        if (basePropertyName in input) {
+          throw new Error(`It is not allowed to set both ${name} and
+                          ${basePropertyName} as  input properties`);
         }
-        let array = this.validateValue(aInput[aName]);
-        if (aRequired && array.length == 0) {
+        let array = this.validateValue(input[name]);
+        if (required && array.length == 0) {
           throw new Error(`Empty array passed for required input property:
-                           ${aName}`);
+                           ${name}`);
         }
         return array;
       }
       // If the property is required and it's not set as is, check if the base
       // property is set.
-      if (aRequired && !(aBasePropertyName in aInput))
-        throw new Error(`Required input property is missing: ${aName}`);
+      if (required && !(basePropertyName in input))
+        throw new Error(`Required input property is missing: ${name}`);
 
-      if (aBasePropertyName in aInput)
-        return [baseProp.validateValue(aInput[aBasePropertyName])];
+      if (basePropertyName in input)
+        return [baseProp.validateValue(input[basePropertyName])];
 
       return [];
     },
 
     isArrayProperty: true
   });
 };
 
-DefineTransaction.validatePropertyValue =
-function(aProp, aInput, aRequired) {
-  return this.inputProps.get(aProp).validateInput(aInput, aRequired);
+DefineTransaction.validatePropertyValue = function(prop, input, required) {
+  return this.inputProps.get(prop).validateInput(input, required);
 };
 
-DefineTransaction.getInputObjectForSingleValue =
-function(aInput, aRequiredProps, aOptionalProps) {
+DefineTransaction.getInputObjectForSingleValue = function(input,
+                                                          requiredProps,
+                                                          optionalProps) {
   // The following input forms may be deduced from a single value:
   // * a single required property with or without optional properties (the given
   //   value is set to the required property).
   // * a single optional property with no required properties.
-  if (aRequiredProps.length > 1 ||
-      (aRequiredProps.length == 0 && aOptionalProps.length > 1)) {
+  if (requiredProps.length > 1 ||
+      (requiredProps.length == 0 && optionalProps.length > 1)) {
     throw new Error("Transaction input isn't an object");
   }
 
-  let propName = aRequiredProps.length == 1 ?
-                 aRequiredProps[0] : aOptionalProps[0];
+  let propName = requiredProps.length == 1 ? requiredProps[0]
+                                           : optionalProps[0];
   let propValue =
-    this.inputProps.get(propName).isArrayProperty && !Array.isArray(aInput) ?
-    [aInput] : aInput;
+    this.inputProps.get(propName).isArrayProperty && !Array.isArray(input) ?
+    [input] : input;
   return { [propName]: propValue };
 };
 
-DefineTransaction.verifyInput =
-function(aInput, aRequiredProps = [], aOptionalProps = []) {
-  if (aRequiredProps.length == 0 && aOptionalProps.length == 0)
+DefineTransaction.verifyInput = function(input,
+                                         requiredProps = [],
+                                         optionalProps = []) {
+  if (requiredProps.length == 0 && optionalProps.length == 0)
     return {};
 
   // If there's just a single required/optional property, we allow passing it
-  // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGuid)
-  // rather than PlacesTransactions.RemoveItem({ guid: myGuid}).
+  // as is, so, for example, one could do PlacesTransactions.Remove(myGuid)
+  // rather than PlacesTransactions.Remove({ guid: myGuid}).
   // This shortcut isn't supported for "complex" properties - e.g. one cannot
   // pass an annotation object this way (note there is no use case for this at
   // the moment anyway).
-  let input = aInput;
-  let isSinglePropertyInput =
-    isPrimitive(aInput) ||
-    Array.isArray(aInput) ||
-    (aInput instanceof Components.interfaces.nsISupports);
+  let isSinglePropertyInput = isPrimitive(input) ||
+                              Array.isArray(input) ||
+                              (input instanceof Ci.nsISupports);
   if (isSinglePropertyInput) {
-    input =  this.getInputObjectForSingleValue(aInput,
-                                               aRequiredProps,
-                                               aOptionalProps);
+    input = this.getInputObjectForSingleValue(input, requiredProps, optionalProps);
   }
 
-  let fixedInput = { };
-  for (let prop of aRequiredProps) {
+  let fixedInput = {};
+  for (let prop of requiredProps) {
     fixedInput[prop] = this.validatePropertyValue(prop, input, true);
   }
-  for (let prop of aOptionalProps) {
+  for (let prop of optionalProps) {
     fixedInput[prop] = this.validatePropertyValue(prop, input, false);
   }
 
   return fixedInput;
 };
 
 // Update the documentation at the top of this module if you add or
 // remove properties.
@@ -882,176 +895,145 @@ DefineTransaction.defineInputProps(["ann
 DefineTransaction.defineArrayInputProp("guids", "guid");
 DefineTransaction.defineArrayInputProp("urls", "url");
 DefineTransaction.defineArrayInputProp("tags", "tag");
 DefineTransaction.defineArrayInputProp("annotations", "annotation");
 DefineTransaction.defineArrayInputProp("excludingAnnotations",
                                        "excludingAnnotation");
 
 /**
- * Internal helper for implementing the execute method of NewBookmark, NewFolder
- * and NewSeparator.
- *
- * @param aTransaction
- *        The transaction object
- * @param aParentGuid
- *        The GUID of the parent folder
- * @param aCreateItemFunction(aParentId, aGuidToRestore)
- *        The function to be called for creating the item on execute and redo.
- *        It should return the itemId for the new item
- *        - aGuidToRestore - the GUID to set for the item (used for redo).
- * @param [optional] aOnUndo
- *        an additional function to call after undo
- * @param [optional] aOnRedo
- *        an additional function to call after redo
- */
-async function ExecuteCreateItem(aTransaction, aParentGuid, aCreateItemFunction,
-                            aOnUndo = null, aOnRedo = null) {
-  let parentId = await PlacesUtils.promiseItemId(aParentGuid),
-      itemId = await aCreateItemFunction(parentId, ""),
-      guid = await PlacesUtils.promiseItemGuid(itemId);
-
-  // On redo, we'll restore the date-added and last-modified properties.
-  let dateAdded = 0, lastModified = 0;
-  aTransaction.undo = async function() {
-    if (dateAdded == 0) {
-      dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId);
-      lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId);
-    }
-    PlacesUtils.bookmarks.removeItem(itemId);
-    if (aOnUndo) {
-      await aOnUndo();
-    }
-  };
-  aTransaction.redo = async function() {
-    parentId = await PlacesUtils.promiseItemId(aParentGuid);
-    itemId = await aCreateItemFunction(parentId, guid);
-    if (aOnRedo)
-      await aOnRedo();
-
-    // aOnRedo is called first to make sure it doesn't override
-    // lastModified.
-    PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded);
-    PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
-    PlacesUtils.bookmarks.setItemLastModified(parentId, dateAdded);
-  };
-  return guid;
-}
-
-/**
  * Creates items (all types) from a bookmarks tree representation, as defined
  * in PlacesUtils.promiseBookmarksTree.
  *
- * @param aBookmarksTree
+ * @param tree
  *        the bookmarks tree object.  You may pass either a bookmarks tree
  *        returned by promiseBookmarksTree, or a manually defined one.
- * @param [optional] aRestoring (default: false)
+ * @param [optional] restoring (default: false)
  *        Whether or not the items are restored.  Only in restore mode, are
  *        the guid, dateAdded and lastModified properties honored.
- * @param [optional] aExcludingAnnotations
+ * @param [optional] excludingAnnotations
  *        Array of annotations names to ignore in aBookmarksTree. This argument
  *        is ignored if aRestoring is set.
  * @note the id, root and charset properties of items in aBookmarksTree are
  *       always ignored.  The index property is ignored for all items but the
  *       root one.
  * @return {Promise}
+ * @resolves to the guid of the new item.
  */
-async function createItemsFromBookmarksTree(aBookmarksTree, aRestoring = false,
-                                       aExcludingAnnotations = []) {
-  function extractLivemarkDetails(aAnnos) {
+// TODO: Replace most of this with insertTree.
+async function createItemsFromBookmarksTree(tree, restoring = false,
+                                            excludingAnnotations = []) {
+  function extractLivemarkDetails(annos) {
     let feedURI = null, siteURI = null;
-    aAnnos = aAnnos.filter(
-      aAnno => {
-        switch (aAnno.name) {
+    annos = annos.filter(anno => {
+      switch (anno.name) {
         case PlacesUtils.LMANNO_FEEDURI:
-          feedURI = NetUtil.newURI(aAnno.value);
+          feedURI = Services.io.newURI(anno.value);
           return false;
         case PlacesUtils.LMANNO_SITEURI:
-          siteURI = NetUtil.newURI(aAnno.value);
+          siteURI = Services.io.newURI(anno.value);
           return false;
         default:
           return true;
-        }
-      } );
-    return [feedURI, siteURI];
+      }
+    });
+    return [feedURI, siteURI, annos];
   }
 
-  async function createItem(aItem,
-                       aParentGuid,
-                       aIndex = PlacesUtils.bookmarks.DEFAULT_INDEX) {
-    let itemId;
-    let guid = aRestoring ? aItem.guid : undefined;
-    let parentId = await PlacesUtils.promiseItemId(aParentGuid);
-    let annos = aItem.annos ? [...aItem.annos] : [];
-    switch (aItem.type) {
+  async function createItem(item,
+                            parentGuid,
+                            index = PlacesUtils.bookmarks.DEFAULT_INDEX) {
+    let guid;
+    let info = { parentGuid, index };
+    if (restoring) {
+      info.guid = item.guid;
+      info.dateAdded = PlacesUtils.toDate(item.dateAdded);
+      info.lastModified = PlacesUtils.toDate(item.lastModified);
+    }
+    let annos = item.annos ? [...item.annos] : [];
+    let shouldResetLastModified = false;
+    switch (item.type) {
       case PlacesUtils.TYPE_X_MOZ_PLACE: {
-        let uri = NetUtil.newURI(aItem.uri);
-        itemId = PlacesUtils.bookmarks.insertBookmark(
-          parentId, uri, aIndex, aItem.title, guid);
-        if ("keyword" in aItem) {
-          await PlacesUtils.keywords.insert({
-            keyword: aItem.keyword,
-            url: uri.spec
-          });
+        info.url = item.uri;
+        if (typeof(item.title) == "string")
+          info.title = item.title;
+
+        guid = (await PlacesUtils.bookmarks.insert(info)).guid;
+
+        if ("keyword" in item) {
+          let { uri: url, keyword, postData } = item;
+          await PlacesUtils.keywords.insert({ url, keyword, postData });
         }
-        if ("tags" in aItem) {
-          PlacesUtils.tagging.tagURI(uri, aItem.tags.split(","));
+        if ("tags" in item) {
+          PlacesUtils.tagging.tagURI(Services.io.newURI(item.uri),
+                                     item.tags.split(","));
+          if (restoring)
+            shouldResetLastModified = true;
         }
         break;
       }
       case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: {
         // Either a folder or a livemark
-        let [feedURI, siteURI] = extractLivemarkDetails(annos);
+        let feedURI, siteURI;
+        [feedURI, siteURI, annos] = extractLivemarkDetails(annos);
         if (!feedURI) {
-          itemId = PlacesUtils.bookmarks.createFolder(
-              parentId, aItem.title, aIndex, guid);
-          if (guid === undefined)
-            guid = await PlacesUtils.promiseItemGuid(itemId);
-          if ("children" in aItem) {
-            for (let child of aItem.children) {
+          info.type = PlacesUtils.bookmarks.TYPE_FOLDER;
+          if (typeof(item.title) == "string")
+            info.title = item.title;
+          guid = (await PlacesUtils.bookmarks.insert(info)).guid;
+          if ("children" in item) {
+            for (let child of item.children) {
               await createItem(child, guid);
             }
           }
+          if (restoring)
+            shouldResetLastModified = true;
         } else {
-          let livemark =
-            await PlacesUtils.livemarks.addLivemark({ title: aItem.title,
-                                                      feedURI,
-                                                      siteURI,
-                                                      parentId,
-                                                      index: aIndex,
-                                                      guid});
-          itemId = livemark.id;
+          info.parentId = await PlacesUtils.promiseItemId(parentGuid);
+          info.feedURI = feedURI;
+          info.siteURI = siteURI;
+          if (info.dateAdded)
+            info.dateAdded = PlacesUtils.toPRTime(info.dateAdded);
+          if (info.lastModified)
+            info.lastModified = PlacesUtils.toPRTime(info.lastModified);
+          if (typeof(item.title) == "string")
+            info.title = item.title;
+          guid = (await PlacesUtils.livemarks.addLivemark(info)).guid;
         }
         break;
       }
       case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: {
-        itemId = PlacesUtils.bookmarks.insertSeparator(parentId, aIndex, guid);
+        info.type = PlacesUtils.bookmarks.TYPE_SEPARATOR;
+        guid = (await PlacesUtils.bookmarks.insert(info)).guid;
         break;
       }
     }
     if (annos.length > 0) {
-      if (!aRestoring && aExcludingAnnotations.length > 0) {
-        annos = annos.filter(a => !aExcludingAnnotations.includes(a.name));
-
+      if (!restoring && excludingAnnotations.length > 0) {
+        annos = annos.filter(a => !excludingAnnotations.includes(a.name));
       }
 
-      PlacesUtils.setAnnotationsForItem(itemId, annos);
+      if (annos.length > 0) {
+        let itemId = await PlacesUtils.promiseItemId(guid);
+        PlacesUtils.setAnnotationsForItem(itemId, annos);
+        if (restoring)
+          shouldResetLastModified = true;
+      }
     }
 
-    if (aRestoring) {
-      if ("dateAdded" in aItem)
-        PlacesUtils.bookmarks.setItemDateAdded(itemId, aItem.dateAdded);
-      if ("lastModified" in aItem)
-        PlacesUtils.bookmarks.setItemLastModified(itemId, aItem.lastModified);
+    if (shouldResetLastModified) {
+      let lastModified = PlacesUtils.toDate(item.lastModified);
+      await PlacesUtils.bookmarks.update({ guid, lastModified });
     }
-    return itemId;
+
+    return guid;
   }
-  return await createItem(aBookmarksTree,
-                          aBookmarksTree.parentGuid,
-                          aBookmarksTree.index);
+  return await createItem(tree,
+                          tree.parentGuid,
+                          tree.index);
 }
 
 /** ***************************************************************************
  * The Standard Places Transactions.
  *
  * See the documentation at the top of this file. The valid values for input
  * are also documented there.
  *****************************************************************************/
@@ -1062,138 +1044,148 @@ var PT = PlacesTransactions;
  * Transaction for creating a bookmark.
  *
  * Required Input Properties: url, parentGuid.
  * Optional Input Properties: index, title, keyword, annotations, tags.
  *
  * When this transaction is executed, it's resolved to the new bookmark's GUID.
  */
 PT.NewBookmark = DefineTransaction(["parentGuid", "url"],
-                                   ["index", "title", "keyword", "postData",
-                                    "annotations", "tags"]);
+                                   ["index", "title", "annotations", "tags"]);
 PT.NewBookmark.prototype = Object.seal({
-  execute(aParentGuid, aURI, aIndex, aTitle,
-                    aKeyword, aPostData, aAnnos, aTags) {
-    return ExecuteCreateItem(this, aParentGuid,
-      async function(parentId, guidToRestore = "") {
-        let itemId = PlacesUtils.bookmarks.insertBookmark(
-          parentId, aURI, aIndex, aTitle, guidToRestore);
+  async execute({ parentGuid, url, index, title, annotations, tags }) {
+    let info = { parentGuid, index, url, title };
+    // Filter tags to exclude already existing ones.
+    if (tags.length > 0) {
+      let currentTags = PlacesUtils.tagging
+                                   .getTagsForURI(Services.io.newURI(url.href));
+      tags = tags.filter(t => !currentTags.includes(t));
+    }
 
-        if (aKeyword) {
-          await PlacesUtils.keywords.insert({
-            url: aURI.spec,
-            keyword: aKeyword,
-            postData: aPostData
-          });
-        }
-        if (aAnnos.length) {
-          PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
-        }
-        if (aTags.length > 0) {
-          let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
-          aTags = aTags.filter(t => !currentTags.includes(t));
-          PlacesUtils.tagging.tagURI(aURI, aTags);
-        }
+    async function createItem() {
+      info = await PlacesUtils.bookmarks.insert(info);
+      if (annotations.length > 0) {
+        let itemId = await PlacesUtils.promiseItemId(info.guid);
+        PlacesUtils.setAnnotationsForItem(itemId, annotations);
+      }
+      if (tags.length > 0) {
+        PlacesUtils.tagging.tagURI(Services.io.newURI(url.href), tags);
+      }
+    }
+
+    await createItem();
 
-        return itemId;
-      },
-      function _additionalOnUndo() {
-        if (aTags.length > 0) {
-          PlacesUtils.tagging.untagURI(aURI, aTags);
-        }
-      });
+    this.undo = async function() {
+      // Pick up the removed info so we have the accurate last-modified value,
+      // which could be affected by any annotation we set in createItem.
+      await PlacesUtils.bookmarks.remove(info);
+      if (tags.length > 0) {
+        PlacesUtils.tagging.untagURI(Services.io.newURI(url.href), tags);
+      }
+    };
+    this.redo = async function() {
+      await createItem();
+      // CreateItem will update the lastModified value if tags or annotations
+      // are present, but we don't care to restore it. The likely of a user
+      // creating a bookmark, undoing and redoing that, and still caring
+      // about lastModified is basically non-existant.
+    };
+    return info.guid;
   }
 });
 
 /**
  * Transaction for creating a folder.
  *
  * Required Input Properties: title, parentGuid.
  * Optional Input Properties: index, annotations.
  *
  * When this transaction is executed, it's resolved to the new folder's GUID.
  */
 PT.NewFolder = DefineTransaction(["parentGuid", "title"],
                                  ["index", "annotations"]);
 PT.NewFolder.prototype = Object.seal({
-  execute(aParentGuid, aTitle, aIndex, aAnnos) {
-    return ExecuteCreateItem(this, aParentGuid,
-      function(parentId, guidToRestore = "") {
-        let itemId = PlacesUtils.bookmarks.createFolder(
-          parentId, aTitle, aIndex, guidToRestore);
-        if (aAnnos.length > 0)
-          PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
-        return itemId;
-      });
+  async execute({ parentGuid, title, index, annotations }) {
+    let info = { type: PlacesUtils.bookmarks.TYPE_FOLDER,
+                 parentGuid, index, title };
+
+    async function createItem() {
+      info = await PlacesUtils.bookmarks.insert(info);
+      if (annotations.length > 0) {
+        let itemId = await PlacesUtils.promiseItemId(info.guid);
+        PlacesUtils.setAnnotationsForItem(itemId, annotations);
+      }
+    }
+    await createItem();
+
+    this.undo = async function() {
+      await PlacesUtils.bookmarks.remove(info);
+    };
+    this.redo = async function() {
+      await createItem();
+      // See the reasoning in CreateItem for why we don't care
+      // about precisely resetting the lastModified value.
+    };
+    return info.guid;
   }
 });
 
 /**
  * Transaction for creating a separator.
  *
  * Required Input Properties: parentGuid.
  * Optional Input Properties: index.
  *
  * When this transaction is executed, it's resolved to the new separator's
  * GUID.
  */
 PT.NewSeparator = DefineTransaction(["parentGuid"], ["index"]);
 PT.NewSeparator.prototype = Object.seal({
-  execute(aParentGuid, aIndex) {
-    return ExecuteCreateItem(this, aParentGuid,
-      function(parentId, guidToRestore = "") {
-        let itemId = PlacesUtils.bookmarks.insertSeparator(
-          parentId, aIndex, guidToRestore);
-        return itemId;
-      });
+  async execute(info) {
+    info.type = PlacesUtils.bookmarks.TYPE_SEPARATOR;
+    info = await PlacesUtils.bookmarks.insert(info);
+    this.undo = PlacesUtils.bookmarks.remove.bind(PlacesUtils.bookmarks, info);
+    this.redo = PlacesUtils.bookmarks.insert.bind(PlacesUtils.bookmarks, info);
+    return info.guid;
   }
 });
 
 /**
  * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the
  * semantics).
  *
  * Required Input Properties: feedUrl, title, parentGuid.
  * Optional Input Properties: siteUrl, index, annotations.
  *
  * When this transaction is executed, it's resolved to the new livemark's
  * GUID.
  */
 PT.NewLivemark = DefineTransaction(["feedUrl", "title", "parentGuid"],
                                    ["siteUrl", "index", "annotations"]);
 PT.NewLivemark.prototype = Object.seal({
-  async execute(aFeedURI, aTitle, aParentGuid, aSiteURI, aIndex, aAnnos) {
-    let livemarkInfo = { title: aTitle,
-                         feedURI: aFeedURI,
-                         siteURI: aSiteURI,
-                         index: aIndex };
+  async execute({ feedUrl, title, parentGuid, siteUrl, index, annotations }) {
+    let livemarkInfo = { title,
+                         feedURI: Services.io.newURI(feedUrl.href),
+                         siteURI: siteUrl ? Services.io.newURI(siteUrl.href) : null,
+                         index,
+                         parentGuid};
     let createItem = async function() {
-      livemarkInfo.parentId = await PlacesUtils.promiseItemId(aParentGuid);
       let livemark = await PlacesUtils.livemarks.addLivemark(livemarkInfo);
-      if (aAnnos.length > 0)
-        PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos);
-
-      if ("dateAdded" in livemarkInfo) {
-        PlacesUtils.bookmarks.setItemDateAdded(livemark.id,
-                                               livemarkInfo.dateAdded);
-        PlacesUtils.bookmarks.setItemLastModified(livemark.id,
-                                                  livemarkInfo.lastModified);
+      if (annotations.length > 0) {
+        PlacesUtils.setAnnotationsForItem(livemark.id, annotations);
       }
       return livemark;
     };
 
     let livemark = await createItem();
+    livemarkInfo.guid = livemark.guid;
+    livemarkInfo.dateAdded = livemark.dateAdded;
+    livemarkInfo.lastModified = livemark.lastModified;
+
     this.undo = async function() {
-      livemarkInfo.guid = livemark.guid;
-      if (!("dateAdded" in livemarkInfo)) {
-        livemarkInfo.dateAdded =
-          PlacesUtils.bookmarks.getItemDateAdded(livemark.id);
-        livemarkInfo.lastModified =
-          PlacesUtils.bookmarks.getItemLastModified(livemark.id);
-      }
       await PlacesUtils.livemarks.removeLivemark(livemark);
     };
     this.redo = async function() {
       livemark = await createItem();
     };
     return livemark.guid;
   }
 });
@@ -1201,323 +1193,331 @@ PT.NewLivemark.prototype = Object.seal({
 /**
  * Transaction for moving an item.
  *
  * Required Input Properties: guid, newParentGuid.
  * Optional Input Properties  newIndex.
  */
 PT.Move = DefineTransaction(["guid", "newParentGuid"], ["newIndex"]);
 PT.Move.prototype = Object.seal({
-  async execute(aGuid, aNewParentGuid, aNewIndex) {
-    let itemId = await PlacesUtils.promiseItemId(aGuid),
-        oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId),
-        oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId),
-        newParentId = await PlacesUtils.promiseItemId(aNewParentGuid);
-
-    PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex);
+  async execute({ guid, newParentGuid, newIndex }) {
+    let originalInfo = await PlacesUtils.bookmarks.fetch(guid);
+    if (!originalInfo)
+      throw new Error("Cannot move a non-existent item");
+    let updateInfo = { guid, parentGuid: newParentGuid, index: newIndex }
+    updateInfo = await PlacesUtils.bookmarks.update(updateInfo);
 
-    let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId);
-    this.undo = () => {
-      // Moving down in the same parent takes in count removal of the item
-      // so to revert positions we must move to oldIndex + 1
-      if (newParentId == oldParentId && oldIndex > undoIndex)
-        PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1);
-      else
-        PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex);
-    };
-    return aGuid;
+    // Moving down in the same parent takes in count removal of the item
+    // so to revert positions we must move to oldIndex + 1.
+    if (newParentGuid == originalInfo.parentGuid &&
+        originalInfo.index > updateInfo.index) {
+      originalInfo.index++;
+    }
+
+    this.undo = PlacesUtils.bookmarks.update.bind(PlacesUtils.bookmarks, originalInfo);
+    this.redo = PlacesUtils.bookmarks.update.bind(PlacesUtils.bookmarks, updateInfo);
+    return guid;
   }
 });
 
 /**
  * Transaction for setting the title for an item.
  *
  * Required Input Properties: guid, title.
  */
 PT.EditTitle = DefineTransaction(["guid", "title"]);
 PT.EditTitle.prototype = Object.seal({
-  async execute(aGuid, aTitle) {
-    let itemId = await PlacesUtils.promiseItemId(aGuid),
-        oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
-    PlacesUtils.bookmarks.setItemTitle(itemId, aTitle);
-    this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); };
+  async execute({ guid, title }) {
+    let originalInfo = await PlacesUtils.bookmarks.fetch(guid);
+    if (!originalInfo)
+      throw new Error("cannot update a non-existent item");
+
+    let updateInfo = { guid, title };
+    updateInfo = await PlacesUtils.bookmarks.update(updateInfo);
+
+    this.undo = PlacesUtils.bookmarks.update.bind(PlacesUtils.bookmarks, originalInfo);
+    this.redo = PlacesUtils.bookmarks.update.bind(PlacesUtils.bookmarks, updateInfo);
   }
 });
 
 /**
  * Transaction for setting the URI for an item.
  *
  * Required Input Properties: guid, url.
  */
 PT.EditUrl = DefineTransaction(["guid", "url"]);
 PT.EditUrl.prototype = Object.seal({
-  async execute(aGuid, aURI) {
-    let itemId = await PlacesUtils.promiseItemId(aGuid),
-        oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId),
-        oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI),
-        newURIAdditionalTags = null;
-    PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI);
+  async execute({ guid, url }) {
+    let originalInfo = await PlacesUtils.bookmarks.fetch(guid);
+    if (!originalInfo)
+      throw new Error("cannot update a non-existent item");
+    if (originalInfo.type != PlacesUtils.bookmarks.TYPE_BOOKMARK)
+      throw new Error("Cannot edit url for non-bookmark items");
 
-    // Move tags from old URI to new URI.
-    if (oldURITags.length > 0) {
-      // Only untag the old URI if this is the only bookmark.
-      if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0)
-        PlacesUtils.tagging.untagURI(oldURI, oldURITags);
+    let uri = Services.io.newURI(url.href);
+    let originalURI = Services.io.newURI(originalInfo.url.href);
+    let originalTags = PlacesUtils.tagging.getTagsForURI(originalURI);
+    let updatedInfo = { guid, url };
+    let newURIAdditionalTags = null;
 
-      let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI);
-      newURIAdditionalTags = oldURITags.filter(t => !currentNewURITags.includes(t));
-      if (newURIAdditionalTags)
-        PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags);
+    async function updateItem() {
+      updatedInfo = await PlacesUtils.bookmarks.update(updatedInfo);
+      // Move tags from the original URI to the new URI.
+      if (originalTags.length > 0) {
+        // Untag the original URI only if this was the only bookmark.
+        if (!(await PlacesUtils.bookmarks.fetch({ url: originalInfo.url })))
+          PlacesUtils.tagging.untagURI(originalURI, originalTags);
+        let currentNewURITags = PlacesUtils.tagging.getTagsForURI(uri);
+        newURIAdditionalTags = originalTags.filter(t => !currentNewURITags.includes(t));
+        if (newURIAdditionalTags)
+          PlacesUtils.tagging.tagURI(uri, newURIAdditionalTags);
+      }
     }
+    await updateItem();
 
-    this.undo = () => {
-      PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI);
-      // Move tags from new URI to old URI.
-      if (oldURITags.length > 0) {
-        // Only untag the new URI if this is the only bookmark.
-        if (newURIAdditionalTags && newURIAdditionalTags.length > 0 &&
-            PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) {
-          PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags);
-        }
+    this.undo = async function() {
+      await PlacesUtils.bookmarks.update(originalInfo);
+      // Move tags from new URI to original URI.
+      if (originalTags.length > 0) {
+         // Only untag the new URI if this is the only bookmark.
+         if (newURIAdditionalTags && newURIAdditionalTags.length > 0 &&
+            !(await PlacesUtils.bookmarks.fetch({ url }))) {
+          PlacesUtils.tagging.untagURI(uri, newURIAdditionalTags);
+         }
+        PlacesUtils.tagging.tagURI(originalURI, originalTags);
+      }
+    };
 
-        PlacesUtils.tagging.tagURI(oldURI, oldURITags);
-      }
+    this.redo = async function() {
+      updatedInfo = await updateItem();
     };
   }
 });
 
 /**
  * Transaction for setting annotations for an item.
  *
  * Required Input Properties: guid, annotationObject
  */
 PT.Annotate = DefineTransaction(["guids", "annotations"]);
 PT.Annotate.prototype = {
-  async execute(aGuids, aNewAnnos) {
-    let undoAnnosForItem = new Map(); // itemId => undoAnnos;
-    for (let guid of aGuids) {
+  async execute({ guids, annotations }) {
+    let undoAnnosForItemId = new Map();
+    for (let guid of guids) {
       let itemId = await PlacesUtils.promiseItemId(guid);
       let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
 
       let undoAnnos = [];
-      for (let newAnno of aNewAnnos) {
+      for (let newAnno of annotations) {
         let currentAnno = currentAnnos.find(a => a.name == newAnno.name);
         if (currentAnno) {
           undoAnnos.push(currentAnno);
         } else {
           // An unset value removes the annotation.
           undoAnnos.push({ name: newAnno.name });
         }
       }
-      undoAnnosForItem.set(itemId, undoAnnos);
+      undoAnnosForItemId.set(itemId, undoAnnos);
 
-      PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+      PlacesUtils.setAnnotationsForItem(itemId, annotations);
     }
 
     this.undo = function() {
-      for (let [itemId, undoAnnos] of undoAnnosForItem) {
+      for (let [itemId, undoAnnos] of undoAnnosForItemId) {
         PlacesUtils.setAnnotationsForItem(itemId, undoAnnos);
       }
     };
     this.redo = async function() {
-      for (let guid of aGuids) {
+      for (let guid of guids) {
         let itemId = await PlacesUtils.promiseItemId(guid);
-        PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+        PlacesUtils.setAnnotationsForItem(itemId, annotations);
       }
     };
   }
 };
 
 /**
  * Transaction for setting the keyword for a bookmark.
  *
  * Required Input Properties: guid, keyword.
+ * Optional Input Properties: postData, oldKeyword.
  */
 PT.EditKeyword = DefineTransaction(["guid", "keyword"],
                                    ["postData", "oldKeyword"]);
 PT.EditKeyword.prototype = Object.seal({
-  async execute(aGuid, aKeyword, aPostData, aOldKeyword) {
+  async execute({ guid, keyword, postData, oldKeyword }) {
     let url;
     let oldKeywordEntry;
-    if (aOldKeyword) {
-      oldKeywordEntry = await PlacesUtils.keywords.fetch(aOldKeyword);
+    if (oldKeyword) {
+      oldKeywordEntry = await PlacesUtils.keywords.fetch(oldKeyword);
       url = oldKeywordEntry.url;
-      await PlacesUtils.keywords.remove(aOldKeyword);
+      await PlacesUtils.keywords.remove(oldKeyword);
     }
 
-    if (aKeyword) {
+    if (keyword) {
       if (!url) {
-        url = (await PlacesUtils.bookmarks.fetch(aGuid)).url;
+        url = (await PlacesUtils.bookmarks.fetch(guid)).url;
       }
       await PlacesUtils.keywords.insert({
         url,
-        keyword: aKeyword,
-        postData: aPostData || (oldKeywordEntry ? oldKeywordEntry.postData : "")
+        keyword,
+        postData: postData || (oldKeywordEntry ? oldKeywordEntry.postData : "")
       });
     }
 
     this.undo = async function() {
-      if (aKeyword) {
-        await PlacesUtils.keywords.remove(aKeyword);
+      if (keyword) {
+        await PlacesUtils.keywords.remove(keyword);
       }
       if (oldKeywordEntry) {
         await PlacesUtils.keywords.insert(oldKeywordEntry);
       }
     };
   }
 });
 
 /**
  * Transaction for sorting a folder by name.
  *
  * Required Input Properties: guid.
  */
 PT.SortByName = DefineTransaction(["guid"]);
 PT.SortByName.prototype = {
-  async execute(aGuid) {
-    let itemId = await PlacesUtils.promiseItemId(aGuid),
-        oldOrder = [],  // [itemId] = old index
-        contents = PlacesUtils.getFolderContents(itemId, false, false).root,
-        count = contents.childCount;
-
-    // Sort between separators.
-    let newOrder = [], // nodes, in the new order.
-        preSep   = []; // Temporary array for sorting each group of nodes.
-    let sortingMethod = (a, b) => {
-      if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
+  async execute({ guid }) {
+    let sortingMethod = (node_a, node_b) => {
+      if (PlacesUtils.nodeIsContainer(node_a) && !PlacesUtils.nodeIsContainer(node_b))
         return -1;
-      if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
+      if (!PlacesUtils.nodeIsContainer(node_a) && PlacesUtils.nodeIsContainer(node_b))
         return 1;
-      return a.title.localeCompare(b.title);
+      return node_a.title.localeCompare(node_b.title);
     };
+    let oldOrderGuids = [];
+    let newOrderGuids = [];
+    let preSepNodes = [];
 
-    for (let i = 0; i < count; ++i) {
-      let node = contents.getChild(i);
-      oldOrder[node.itemId] = i;
+    // This is not great, since it does main-thread IO.
+    // PromiseBookmarksTree can't be used, since it' won't stop at the first level'.
+    let folderId = await PlacesUtils.promiseItemId(guid);
+    let root = PlacesUtils.getFolderContents(folderId, false, false).root;
+    for (let i = 0; i < root.childCount; ++i) {
+      let node = root.getChild(i);
+      oldOrderGuids.push(node.bookmarkGuid);
       if (PlacesUtils.nodeIsSeparator(node)) {
-        if (preSep.length > 0) {
-          preSep.sort(sortingMethod);
-          newOrder = newOrder.concat(preSep);
-          preSep.splice(0, preSep.length);
+        if (preSepNodes.length > 0) {
+          preSepNodes.sort(sortingMethod);
+          newOrderGuids.push(...preSepNodes.map(n => n.bookmarkGuid));
+          preSepNodes = [];
         }
-        newOrder.push(node);
-      } else
-        preSep.push(node);
+        newOrderGuids.push(node.bookmarkGuid);
+      } else {
+        preSepNodes.push(node);
+      }
     }
-    contents.containerOpen = false;
-
-    if (preSep.length > 0) {
-      preSep.sort(sortingMethod);
-      newOrder = newOrder.concat(preSep);
+    root.containerOpen = false;
+    if (preSepNodes.length > 0) {
+      preSepNodes.sort(sortingMethod);
+      newOrderGuids.push(...preSepNodes.map(n => n.bookmarkGuid));
     }
+    await PlacesUtils.bookmarks.reorder(guid, newOrderGuids);
 
-    // Set the nex indexes.
-    let callback = {
-      runBatched() {
-        for (let i = 0; i < newOrder.length; ++i) {
-          PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
-        }
-      }
+    this.undo = async function() {
+      await PlacesUtils.bookmarks.reorder(guid, oldOrderGuids);
     };
-    PlacesUtils.bookmarks.runInBatchMode(callback, null);
-
-    this.undo = () => {
-      let callback = {
-        runBatched() {
-          for (let item in oldOrder) {
-            PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]);
-          }
-        }
-      };
-      PlacesUtils.bookmarks.runInBatchMode(callback, null);
+    this.redo = async function() {
+      await PlacesUtils.bookmarks.reorder(guid, newOrderGuids);
     };
   }
 };
 
 /**
  * Transaction for removing an item (any type).
  *
  * Required Input Properties: guids.
  */
 PT.Remove = DefineTransaction(["guids"]);
 PT.Remove.prototype = {
-  async execute(aGuids) {
-    function promiseBookmarksTree(guid) {
+  async execute({ guids }) {
+    let promiseBookmarksTree = async function(guid) {
+      let tree;
       try {
-        return PlacesUtils.promiseBookmarksTree(guid);
+        tree = await PlacesUtils.promiseBookmarksTree(guid);
       } catch (ex) {
         throw new Error("Failed to get info for the specified item (guid: " +
-                        guid + "). Ex: " + ex);
+                          guid + "): " + ex);
       }
+      return tree;
+    };
+    let removedItems = [];
+    for (let guid of guids) {
+      removedItems.push(await promiseBookmarksTree(guid));
     }
-
-    let toRestore = [];
-    for (let guid of aGuids) {
-      toRestore.push(await promiseBookmarksTree(guid));
-    }
-
     let removeThem = async function() {
-      for (let guid of aGuids) {
-        PlacesUtils.bookmarks.removeItem(await PlacesUtils.promiseItemId(guid));
+      for (let info of removedItems) {
+        if (info.annos &&
+            info.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
+          await PlacesUtils.livemarks.removeLivemark({ guid: info.guid });
+        } else {
+          await PlacesUtils.bookmarks.remove({ guid: info.guid });
+        }
       }
     };
     await removeThem();
 
     this.undo = async function() {
-      for (let info of toRestore) {
+      for (let info of removedItems) {
         await createItemsFromBookmarksTree(info, true);
       }
     };
     this.redo = removeThem;
   }
 };
 
 /**
  * Transactions for removing all bookmarks for one or more urls.
  *
  * Required Input Properties: urls.
  */
 PT.RemoveBookmarksForUrls = DefineTransaction(["urls"]);
 PT.RemoveBookmarksForUrls.prototype = {
-  async execute(aUrls) {
+  async execute({ urls }) {
     let guids = [];
-    for (let url of aUrls) {
-      await PlacesUtils.bookmarks.fetch({ url }, info => {
-        guids.push(info.guid);
-      });
+    for (let url of urls) {
+      await PlacesUtils.bookmarks.fetch({ url }, b => guids.push(b.guid));
     }
     let removeTxn = TransactionsHistory.getRawTransaction(PT.Remove(guids));
     await removeTxn.execute();
     this.undo = removeTxn.undo.bind(removeTxn);
     this.redo = removeTxn.redo.bind(removeTxn);
   }
 };
 
 /**
  * Transaction for tagging urls.
  *
  * Required Input Properties: urls, tags.
  */
 PT.Tag = DefineTransaction(["urls", "tags"]);
 PT.Tag.prototype = {
-  async execute(aURIs, aTags) {
+  async execute({ urls, tags }) {
     let onUndo = [], onRedo = [];
-    for (let uri of aURIs) {
-      if (!(await PlacesUtils.bookmarks.fetch({ url: uri }))) {
+    for (let url of urls) {
+      if (!(await PlacesUtils.bookmarks.fetch({ url }))) {
         // Tagging is only allowed for bookmarked URIs (but see 424160).
         let createTxn = TransactionsHistory.getRawTransaction(
-          PT.NewBookmark({ url: uri,
-                           tags: aTags,
-                           parentGuid: PlacesUtils.bookmarks.unfiledGuid }));
+          PT.NewBookmark({ url,
+                           tags,
+                           parentGuid: PlacesUtils.bookmarks.unfiledGuid })
+        );
         await createTxn.execute();
         onUndo.unshift(createTxn.undo.bind(createTxn));
         onRedo.push(createTxn.redo.bind(createTxn));
       } else {
+        let uri = Services.io.newURI(url.href);
         let currentTags = PlacesUtils.tagging.getTagsForURI(uri);
-        let newTags = aTags.filter(t => !currentTags.includes(t));
+        let newTags = tags.filter(t => !currentTags.includes(t));
         PlacesUtils.tagging.tagURI(uri, newTags);
         onUndo.unshift(() => {
           PlacesUtils.tagging.untagURI(uri, newTags);
         });
         onRedo.push(() => {
           PlacesUtils.tagging.tagURI(uri, newTags);
         });
       }
@@ -1540,25 +1540,27 @@ PT.Tag.prototype = {
  *
  * Required Input Properties: urls.
  * Optional Input Properties: tags.
  *
  * If |tags| is not set, all tags set for |url| are removed.
  */
 PT.Untag = DefineTransaction(["urls"], ["tags"]);
 PT.Untag.prototype = {
-  execute(aURIs, aTags) {
+  async execute({ urls, tags }) {
     let onUndo = [], onRedo = [];
-    for (let uri of aURIs) {
+    for (let url of urls) {
+      let uri = Services.io.newURI(url.href);
       let tagsToRemove;
       let tagsSet = PlacesUtils.tagging.getTagsForURI(uri);
-      if (aTags.length > 0)
-        tagsToRemove = aTags.filter(t => tagsSet.includes(t));
-      else
+      if (tags.length > 0) {
+        tagsToRemove = tags.filter(t => tagsSet.includes(t));
+      } else {
         tagsToRemove = tagsSet;
+      }
       PlacesUtils.tagging.untagURI(uri, tagsToRemove);
       onUndo.unshift(() => {
         PlacesUtils.tagging.tagURI(uri, tagsToRemove);
       });
       onRedo.push(() => {
         PlacesUtils.tagging.untagURI(uri, tagsToRemove);
       });
     }
@@ -1579,37 +1581,35 @@ PT.Untag.prototype = {
  * Transaction for copying an item.
  *
  * Required Input Properties: guid, newParentGuid
  * Optional Input Properties: newIndex, excludingAnnotations.
  */
 PT.Copy = DefineTransaction(["guid", "newParentGuid"],
                             ["newIndex", "excludingAnnotations"]);
 PT.Copy.prototype = {
-  async execute(aGuid, aNewParentGuid, aNewIndex, aExcludingAnnotations) {
+  async execute({ guid, newParentGuid, newIndex, excludingAnnotations }) {
     let creationInfo = null;
     try {
-      creationInfo = await PlacesUtils.promiseBookmarksTree(aGuid);
+      creationInfo = await PlacesUtils.promiseBookmarksTree(guid);
     } catch (ex) {
       throw new Error("Failed to get info for the specified item (guid: " +
-                      aGuid + "). Ex: " + ex);
+                      guid + "). Ex: " + ex);
     }
-    creationInfo.parentGuid = aNewParentGuid;
-    creationInfo.index = aNewIndex;
+    creationInfo.parentGuid = newParentGuid;
+    creationInfo.index = newIndex;
 
-    let newItemId =
-      await createItemsFromBookmarksTree(creationInfo, false,
-                                         aExcludingAnnotations);
+    let newItemGuid = await createItemsFromBookmarksTree(creationInfo, false,
+                                                         excludingAnnotations);
     let newItemInfo = null;
     this.undo = async function() {
       if (!newItemInfo) {
-        let newItemGuid = await PlacesUtils.promiseItemGuid(newItemId);
         newItemInfo = await PlacesUtils.promiseBookmarksTree(newItemGuid);
       }
-      PlacesUtils.bookmarks.removeItem(newItemId);
+      await PlacesUtils.bookmarks.remove(newItemGuid);
     };
     this.redo = async function() {
-      newItemId = await createItemsFromBookmarksTree(newItemInfo, true);
+      await createItemsFromBookmarksTree(newItemInfo, true);
     }
 
-    return await PlacesUtils.promiseItemGuid(newItemId);
+    return newItemGuid;
   }
 };
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -384,27 +384,31 @@ this.PlacesUtils = {
   /**
    * Convert a Date object to a PRTime (microseconds).
    *
    * @param date
    *        the Date object to convert.
    * @return microseconds from the epoch.
    */
   toPRTime(date) {
+    if (typeof date != "number" && date.constructor.name != "Date")
+      throw new Error("Invalid value passed to toPRTime");
     return date * 1000;
   },
 
   /**
    * Convert a PRTime to a Date object.
    *
    * @param time
    *        microseconds from the epoch.
    * @return a Date object.
    */
   toDate(time) {
+    if (typeof time != "number")
+      throw new Error("Invalid value passed to toDate");
     return new Date(parseInt(time / 1000));
   },
 
   /**
    * Wraps a string in a nsISupportsString wrapper.
    * @param   aString
    *          The string to wrap.
    * @returns A nsISupportsString object containing a string.
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -258,23 +258,26 @@ async function ensureLivemarkCreatedByAd
   // This throws otherwise.
   await PlacesUtils.livemarks.getLivemark({ guid: aLivemarkGuid });
 }
 
 // Checks if two bookmark trees (as returned by promiseBookmarksTree) are the
 // same.
 // false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set.
 async function ensureEqualBookmarksTrees(aOriginal,
-                                    aNew,
-                                    aIsRestoredItem = true,
-                                    aCheckParentAndPosition = false) {
+                                         aNew,
+                                         aIsRestoredItem = true,
+                                         aCheckParentAndPosition = false) {
   // Note "id" is not-enumerable, and is therefore skipped by Object.keys (both
   // ours and the one at deepEqual). This is fine for us because ids are not
   // restored by Redo.
   if (aIsRestoredItem) {
+    // Ignore lastModified for newly created items, for performance reasons.
+    if (!aOriginal.lastModified)
+      aNew.lastModified = aOriginal.lastModified;
     Assert.deepEqual(aOriginal, aNew);
     if (isLivemarkTree(aNew))
       await ensureLivemarkCreatedByAddLivemark(aNew.guid);
     return;
   }
 
   for (let property of Object.keys(aOriginal)) {
     if (property == "children") {
@@ -368,22 +371,26 @@ add_task(async function test_recycled_tr
 add_task(async function test_new_folder_with_annotation() {
   const ANNO = { name: "TestAnno", value: "TestValue" };
   let folder_info = createTestFolderInfo();
   folder_info.index = bmStartIndex;
   folder_info.annotations = [ANNO];
   ensureUndoState();
   let txn = PT.NewFolder(folder_info);
   folder_info.guid = await txn.transact();
+  let originalInfo = await PlacesUtils.promiseBookmarksTree(folder_info.guid);
   let ensureDo = async function(aRedo = false) {
     ensureUndoState([[txn]], 0);
     await ensureItemsAdded(folder_info);
     ensureAnnotationsSet(folder_info.guid, [ANNO]);
-    if (aRedo)
-      ensureTimestampsUpdated(folder_info.guid, true);
+    if (aRedo) {
+      // Ignore lastModified in the comparison, for performance reasons.
+      originalInfo.lastModified = null;
+      await ensureBookmarksTreeRestoredCorrectly(originalInfo);
+    }
     observer.reset();
   };
 
   let ensureUndo = () => {
     ensureUndoState([[txn]], 1);
     ensureItemsRemoved({ guid:       folder_info.guid,
                          parentGuid: folder_info.parentGuid,
                          index:      bmStartIndex });
@@ -406,21 +413,22 @@ add_task(async function test_new_bookmar
                   url:        NetUtil.newURI("http://test_create_item.com"),
                   index:      bmStartIndex,
                   title:      "Test creating an item" };
 
   ensureUndoState();
   let txn = PT.NewBookmark(bm_info);
   bm_info.guid = await txn.transact();
 
+  let originalInfo = await PlacesUtils.promiseBookmarksTree(bm_info.guid);
   let ensureDo = async function(aRedo = false) {
     ensureUndoState([[txn]], 0);
     await ensureItemsAdded(bm_info);
     if (aRedo)
-      ensureTimestampsUpdated(bm_info.guid, true);
+      await ensureBookmarksTreeRestoredCorrectly(originalInfo);
     observer.reset();
   };
   let ensureUndo = () => {
     ensureUndoState([[txn]], 1);
     ensureItemsRemoved({ guid:       bm_info.guid,
                          parentGuid: bm_info.parentGuid,
                          index:      bmStartIndex });
     observer.reset();
@@ -594,64 +602,69 @@ add_task(async function test_remove_fold
     let folder_level_1_txn  = PT.NewFolder(folder_level_1_info);
     folder_level_1_info.guid = await folder_level_1_txn.transact();
     folder_level_2_info.parentGuid = folder_level_1_info.guid;
     let folder_level_2_txn = PT.NewFolder(folder_level_2_info);
     folder_level_2_info.guid = await folder_level_2_txn.transact();
     return [folder_level_1_txn, folder_level_2_txn];
   });
 
+  let original_folder_level_1_tree =
+    await PlacesUtils.promiseBookmarksTree(folder_level_1_info.guid);
+  let original_folder_level_2_tree =
+    Object.assign({ parentGuid: original_folder_level_1_tree.guid },
+                  original_folder_level_1_tree.children[0]);
+
   ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]);
   await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
   observer.reset();
 
   let remove_folder_2_txn = PT.Remove(folder_level_2_info);
   await remove_folder_2_txn.transact();
 
   ensureUndoState([ [remove_folder_2_txn],
                     [folder_level_2_txn_result, folder_level_1_txn_result] ]);
   await ensureItemsRemoved(folder_level_2_info);
 
   // Undo Remove "Folder Level 2"
   await PT.undo();
   ensureUndoState([ [remove_folder_2_txn],
                     [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
   await ensureItemsAdded(folder_level_2_info);
-  ensureTimestampsUpdated(folder_level_2_info.guid, true);
+  await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
   observer.reset();
 
   // Redo Remove "Folder Level 2"
   await PT.redo();
   ensureUndoState([ [remove_folder_2_txn],
                     [folder_level_2_txn_result, folder_level_1_txn_result] ]);
   await ensureItemsRemoved(folder_level_2_info);
   observer.reset();
 
   // Undo it again
   await PT.undo();
   ensureUndoState([ [remove_folder_2_txn],
                     [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
   await ensureItemsAdded(folder_level_2_info);
-  ensureTimestampsUpdated(folder_level_2_info.guid, true);
+  await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
   observer.reset();
 
   // Undo the creation of both folders
   await PT.undo();
   ensureUndoState([ [remove_folder_2_txn],
                     [folder_level_2_txn_result, folder_level_1_txn_result] ], 2);
   await ensureItemsRemoved(folder_level_2_info, folder_level_1_info);
   observer.reset();
 
   // Redo the creation of both folders
   await PT.redo();
   ensureUndoState([ [remove_folder_2_txn],
                     [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
   await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
-  ensureTimestampsUpdated(folder_level_1_info.guid, true);
-  ensureTimestampsUpdated(folder_level_2_info.guid, true);
+  await ensureBookmarksTreeRestoredCorrectly(original_folder_level_1_tree);
   observer.reset();
 
   // Redo Remove "Folder Level 2"
   await PT.redo();
   ensureUndoState([ [remove_folder_2_txn],
                     [folder_level_2_txn_result, folder_level_1_txn_result] ]);
   await ensureItemsRemoved(folder_level_2_info);
   observer.reset();
@@ -672,103 +685,91 @@ add_task(async function test_remove_fold
   await PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(async function test_add_and_remove_bookmarks_with_additional_info() {
   const testURI = NetUtil.newURI("http://add.remove.tag");
   const TAG_1 = "TestTag1";
   const TAG_2 = "TestTag2";
-  const KEYWORD = "test_keyword";
-  const POST_DATA = "post_data";
   const ANNO = { name: "TestAnno", value: "TestAnnoValue" };
 
   let folder_info = createTestFolderInfo();
   folder_info.guid = await PT.NewFolder(folder_info).transact();
   let ensureTags = ensureTagsForURI.bind(null, testURI);
 
   // Check that the NewBookmark transaction preserves tags.
   observer.reset();
   let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
   b1_info.guid = await PT.NewBookmark(b1_info).transact();
+  let b1_originalInfo = await PlacesUtils.promiseBookmarksTree(b1_info.guid);
   ensureTags([TAG_1]);
   await PT.undo();
   ensureTags([]);
 
   observer.reset();
   await PT.redo();
-  ensureTimestampsUpdated(b1_info.guid, true);
+  await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
   ensureTags([TAG_1]);
 
   // Check if the Remove transaction removes and restores tags of children
   // correctly.
   await PT.Remove(folder_info.guid).transact();
   ensureTags([]);
 
   observer.reset();
   await PT.undo();
-  ensureTimestampsUpdated(b1_info.guid, true);
+  await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
   ensureTags([TAG_1]);
 
   await PT.redo();
   ensureTags([]);
 
   observer.reset();
   await PT.undo();
-  ensureTimestampsUpdated(b1_info.guid, true);
+  await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
   ensureTags([TAG_1]);
 
   // * Check that no-op tagging (the uri is already tagged with TAG_1) is
   //   also a no-op on undo.
-  // * Test the "keyword" property of the NewBookmark transaction.
   observer.reset();
   let b2_info = { parentGuid:  folder_info.guid,
-                  url:         testURI, tags: [TAG_1, TAG_2],
-                  keyword:     KEYWORD,
-                  postData:    POST_DATA,
+                  url:         testURI,
+                  tags:        [TAG_1, TAG_2],
                   annotations: [ANNO] };
   b2_info.guid = await PT.NewBookmark(b2_info).transact();
   let b2_post_creation_changes = [
    { guid: b2_info.guid,
      isAnnoProperty: true,
      property: ANNO.name,
-     newValue: ANNO.value },
-   { guid: b2_info.guid,
-     property: "keyword",
-     newValue: KEYWORD } ];
+     newValue: ANNO.value } ];
   ensureItemsChanged(...b2_post_creation_changes);
   ensureTags([TAG_1, TAG_2]);
 
   observer.reset();
   await PT.undo();
   await ensureItemsRemoved(b2_info);
   ensureTags([TAG_1]);
 
-  // Check if Remove correctly restores keywords, tags and annotations.
-  // Since both bookmarks share the same uri, they also share the keyword that
-  // is not removed along with one of the bookmarks.
+  // Check if Remove correctly restores tags and annotations.
   observer.reset();
   await PT.redo();
   ensureItemsChanged({ guid: b2_info.guid,
                        isAnnoProperty: true,
                        property: ANNO.name,
                        newValue: ANNO.value });
   ensureTags([TAG_1, TAG_2]);
 
   // Test Remove for multiple items.
   observer.reset();
   await PT.Remove(b1_info.guid).transact();
   await PT.Remove(b2_info.guid).transact();
   await PT.Remove(folder_info.guid).transact();
   await ensureItemsRemoved(b1_info, b2_info, folder_info);
   ensureTags([]);
-  // There is no keyword removal notification cause all bookmarks are removed
-  // before the keyword itself, so there's no one to notify.
-  let entry = await PlacesUtils.keywords.fetch(KEYWORD);
-  Assert.equal(entry, null, "keyword has been removed");
 
   observer.reset();
   await PT.undo();
   await ensureItemsAdded(folder_info);
   ensureTags([]);
 
   observer.reset();
   await PT.undo();
@@ -875,16 +876,17 @@ add_task(async function test_add_and_rem
   let guid = await createLivemarkTxn.transact();
   let originalInfo = await PlacesUtils.promiseBookmarksTree(guid);
   Assert.ok(originalInfo);
   await ensureLivemarkCreatedByAddLivemark(guid);
 
   let removeTxn = PT.Remove(guid);
   await removeTxn.transact();
   await ensureNonExistent(guid);
+
   async function undo() {
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
     await PT.undo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
     await ensureBookmarksTreeRestoredCorrectly(originalInfo);
     await PT.undo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
     await ensureNonExistent(guid);