Bug 982115 - PlacesTransactions: (1) Decouple |transact| and |batch|, and make batch work like Task.spawn, rather than the custom solution we had so far (yielding transactions rather than calling |transact| for the batched transactions). so that |transact| may be called during a batch from any function, not limiting the batch 'context' to the function passed to |batch|; (2) Rename the uri input property to URL, and allow passing DOM URL objects for it (in addition to plain specs and nsIURI objects) - this matches recent Bookmark.jsm and History.jsm work; (3) Improve the module documentation. r=mak.
authorAsaf Romano <mano@mozilla.com>
Mon, 10 Nov 2014 10:36:56 +0200
changeset 226231 d265e96ff5577e7d8c1d92e3d3f0a76af7aef9fd
parent 226230 ec860d76ab557f5eb06bd9c4f242042204050a21
child 226232 a6775c581217c578df3db91adff7f50bde94b9b9
push id36
push userdburns@mozilla.com
push dateMon, 10 Nov 2014 15:14:02 +0000
reviewersmak
bugs982115
milestone36.0a1
Bug 982115 - PlacesTransactions: (1) Decouple |transact| and |batch|, and make batch work like Task.spawn, rather than the custom solution we had so far (yielding transactions rather than calling |transact| for the batched transactions). so that |transact| may be called during a batch from any function, not limiting the batch 'context' to the function passed to |batch|; (2) Rename the uri input property to URL, and allow passing DOM URL objects for it (in addition to plain specs and nsIURI objects) - this matches recent Bookmark.jsm and History.jsm work; (3) Improve the module documentation. r=mak.
browser/components/places/PlacesUIUtils.jsm
browser/components/places/content/controller.js
browser/components/places/content/moveBookmarks.js
toolkit/components/places/PlacesTransactions.jsm
toolkit/components/places/tests/unit/test_async_transactions.js
toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -88,17 +88,17 @@ this.PlacesUIUtils = {
 
   LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
   DESCRIPTION_ANNO: "bookmarkProperties/description",
 
   /**
    * Makes a URI from a spec, and do fixup
    * @param   aSpec
    *          The string spec of the URI
-   * @returns A URI object for the spec.
+   * @return A URI object for the spec.
    */
   createFixedURI: function PUIU_createFixedURI(aSpec) {
     return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
   },
 
   getFormattedString: function PUIU_getFormattedString(key, params) {
     return bundle.formatStringFromName(key, params, params.length);
   },
@@ -330,18 +330,18 @@ this.PlacesUIUtils = {
    * @param   type
    *          The content type of the data
    * @param   container
    *          The container the data was dropped or pasted into
    * @param   index
    *          The index within the container the item was dropped or pasted at
    * @param   copy
    *          The drag action was copy, so don't move folders or links.
-   * @returns An object implementing nsITransaction that can perform
-   *          the move/insert.
+   * @return An object implementing nsITransaction that can perform
+   *         the move/insert.
    */
   makeTransaction:
   function PUIU_makeTransaction(data, type, container, index, copy)
   {
     switch (data.type) {
       case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
         if (copy) {
           return this._getFolderCopyTransaction(data, container, index);
@@ -382,29 +382,29 @@ this.PlacesUIUtils = {
   },
 
   /**
    * ********* PlacesTransactions version of the function defined above ********
    *
    * Constructs a Places Transaction for the drop or paste of a blob of data
    * into a container.
    *
-   * @param aData
-   *        The unwrapped data blob of dropped or pasted data.
-   * @param aType
-   *        The content type of the data.
-   * @param aNewParentGuid
-   *        GUID of the container the data was dropped or pasted into.
-   * @param aIndex
-   *        The index within the container the item was dropped or pasted at.
-   * @param aCopy
-   *        The drag action was copy, so don't move folders or links.
+   * @param   aData
+   *          The unwrapped data blob of dropped or pasted data.
+   * @param   aType
+   *          The content type of the data.
+   * @param   aNewParentGuid
+   *          GUID of the container the data was dropped or pasted into.
+   * @param   aIndex
+   *          The index within the container the item was dropped or pasted at.
+   * @param   aCopy
+   *          The drag action was copy, so don't move folders or links.
    *
-   * @returns a Places Transaction that can be passed to
-   *          PlacesTranactions.transact for performing the move/insert command.
+   * @return  a Places Transaction that can be transacted for performing the
+   *          move/insert command.
    */
   getTransactionForData: function(aData, aType, aNewParentGuid, aIndex, aCopy) {
     if (this.SUPPORTED_FLAVORS.indexOf(aData.type) == -1)
       throw new Error(`Unsupported '${aData.type}' data type`);
 
     if ("itemGuid" in aData) {
       if (this.PLACES_FLAVORS.indexOf(aData.type) == -1)
         throw new Error (`itemGuid unexpectedly set on ${aData.type} data`);
@@ -565,35 +565,35 @@ this.PlacesUIUtils = {
     return true;
   },
 
   /**
    * Get the description associated with a document, as specified in a <META>
    * element.
    * @param   doc
    *          A DOM Document to get a description for
-   * @returns A description string if a META element was discovered with a
-   *          "description" or "httpequiv" attribute, empty string otherwise.
+   * @return A description string if a META element was discovered with a
+   *         "description" or "httpequiv" attribute, empty string otherwise.
    */
   getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) {
     var metaElements = doc.getElementsByTagName("META");
     for (var i = 0; i < metaElements.length; ++i) {
       if (metaElements[i].name.toLowerCase() == "description" ||
           metaElements[i].httpEquiv.toLowerCase() == "description") {
         return metaElements[i].content;
       }
     }
     return "";
   },
 
   /**
    * Retrieve the description of an item
    * @param aItemId
    *        item identifier
-   * @returns the description of the given item, or an empty string if it is
+   * @return the description of the given item, or an empty string if it is
    * not set.
    */
   getItemDescription: function PUIU_getItemDescription(aItemId) {
     if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO))
       return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO);
     return "";
   },
 
@@ -1158,17 +1158,17 @@ this.PlacesUIUtils = {
     return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"];
   },
 
   /**
    * If an item is a left-pane query, returns the name of the query
    * or an empty string if not.
    *
    * @param aItemId id of a container
-   * @returns the name of the query, or empty string if not a left-pane query
+   * @return the name of the query, or empty string if not a left-pane query
    */
   getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) {
     var queryName = "";
     // If the let pane hasn't been built, use the annotation service
     // directly, to avoid building the left pane too early.
     if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) {
       try {
         queryName = PlacesUtils.annotations.
@@ -1329,17 +1329,17 @@ XPCOMUtils.defineLazyGetter(PlacesUIUtil
 
     /**
      * Transaction for setting/unsetting Load-in-sidebar annotation.
      *
      * @param aBookmarkId
      *        id of the bookmark where to set Load-in-sidebar annotation.
      * @param aLoadInSidebar
      *        boolean value.
-     * @returns nsITransaction object.
+     * @return nsITransaction object.
      */
     setLoadInSidebar: function(aItemId, aLoadInSidebar)
     {
       let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
                       type: Ci.nsIAnnotationService.TYPE_INT32,
                       flags: 0,
                       value: aLoadInSidebar,
                       expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
@@ -1348,17 +1348,17 @@ XPCOMUtils.defineLazyGetter(PlacesUIUtil
 
    /**
     * Transaction for editing the description of a bookmark or a folder.
     *
     * @param aItemId
     *        id of the item to edit.
     * @param aDescription
     *        new description.
-    * @returns nsITransaction object.
+    * @return nsITransaction object.
     */
     editItemDescription: function(aItemId, aDescription)
     {
       let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO,
                       type: Ci.nsIAnnotationService.TYPE_STRING,
                       flags: 0,
                       value: aDescription,
                       expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -762,17 +762,17 @@ PlacesController.prototype = {
       let insertedNodeId = PlacesUtils.bookmarks
                                       .getIdForItemAt(ip.itemId, ip.index);
       this._view.selectItems([insertedNodeId], false);
       return;
     }
 
     let txn = PlacesTransactions.NewSeparator({ parentGuid: yield ip.promiseGuid()
                                               , index: ip.index });
-    let guid = yield PlacesTransactions.transact(txn);
+    let guid = yield txn.transact();
     let itemId = yield PlacesUtils.promiseItemId(guid);
     // Select the new item.
     this._view.selectItems([itemId], false);
   }),
 
   /**
    * Opens a dialog for moving the selected nodes.
    */
@@ -788,17 +788,17 @@ PlacesController.prototype = {
   sortFolderByName: Task.async(function* () {
     let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
     if (!PlacesUIUtils.useAsyncTransactions) {
       var txn = new PlacesSortFolderByNameTransaction(itemId);
       PlacesUtils.transactionManager.doTransaction(txn);
       return;
     }
     let guid = yield PlacesUtils.promiseItemGuid(itemId);
-    yield PlacesTransactions.transact(PlacesTransactions.SortByName(guid));
+    yield PlacesTransactions.SortByName(guid).transact();
   }),
 
   /**
    * Walk the list of folders we're removing in this delete operation, and
    * see if the selected node specified is already implicitly being removed
    * because it is a child of that folder.
    * @param   node
    *          Node to check for containment.
@@ -935,17 +935,17 @@ PlacesController.prototype = {
     var transactions = [];
     var removedFolders = [];
 
     for (var i = 0; i < ranges.length; i++)
       this._removeRange(ranges[i], transactions, removedFolders);
 
     if (transactions.length > 0) {
       if (PlacesUIUtils.useAsyncTransactions) {
-        yield PlacesTransactions.transact(transactions);
+        yield PlacesTransactions.batch(transactions);
       }
       else {
         var txn = new PlacesAggregatedTransaction(txnName, transactions);
         PlacesUtils.transactionManager.doTransaction(txn);
       }
     }
   }),
 
@@ -1299,37 +1299,36 @@ PlacesController.prototype = {
       return;
     }
 
     let itemsToSelect = [];
     if (PlacesUIUtils.useAsyncTransactions) {
       if (ip.isTag) {
         let uris = [for (item of items) if ("uri" in item)
                     NetUtil.newURI(item.uri)];
-        yield PlacesTransactions.transact(
-          PlacesTransactions.Tag({ uris: uris, tag: ip.tagName }));
+        yield PlacesTransactions.Tag({ uris: uris, tag: ip.tagName }).transact();
       }
       else {
-        yield PlacesTransactions.transact(function* () {
+        yield PlacesTransactions.batch(function* () {
           let insertionIndex = ip.index;
           let parent = yield ip.promiseGuid();
 
           for (let item of items) {
             let doCopy = action == "copy";
 
             // If this is not a copy, check for safety that we can move the
             // source, otherwise report an error and fallback to a copy.
             if (!doCopy &&
                 !PlacesControllerDragHelper.canMoveUnwrappedNode(item)) {
               Cu.reportError("Tried to move an unmovable Places node, " +
                              "reverting to a copy operation.");
               doCopy = true;
             }
             let guid = yield PlacesUIUtils.getTransactionForData(
-              item, type, parent, insertionIndex, doCopy);
+              item, type, parent, insertionIndex, doCopy).transact();
             itemsToSelect.push(yield PlacesUtils.promiseItemId(guid));
 
             // Adjust index to make sure items are pasted in the correct
             // position.  If index is DEFAULT_INDEX, items are just appended.
             if (insertionIndex != PlacesUtils.bookmarks.DEFAULT_INDEX)
               insertionIndex++;
           }
         });
@@ -1650,17 +1649,17 @@ let PlacesControllerDragHelper = {
           transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
                               flavor, insertionPoint.itemId,
                               index, doCopy));
         }
       }
     }
 
     if (PlacesUIUtils.useAsyncTransactions) {
-      yield PlacesTransactions.transact(transactions);
+      yield PlacesTransactions.batch(transactions);
     }
     else {
       let txn = new PlacesAggregatedTransaction("DropItems", transactions);
       PlacesUtils.transactionManager.doTransaction(txn);
     }
   }),
 
   /**
--- a/browser/components/places/content/moveBookmarks.js
+++ b/browser/components/places/content/moveBookmarks.js
@@ -40,24 +40,24 @@ var gMoveBookmarksDialog = {
       }
       if (transactions.length != 0) {
         let txn = new PlacesAggregatedTransaction("Move Items", transactions);
         PlacesUtils.transactionManager.doTransaction(txn);
       }
       return;
     }
 
-    PlacesTransactions.transact(function* () {
+    PlacesTransactions.batch(function* () {
       let newParentGuid = yield PlacesUtils.promiseItemGuid(selectedFolderId);
       for (let node of this._nodes) {
         // Nothing to do if the node is already under the selected folder.
         if (node.parent.itemId == selectedFolderId)
           continue;
         yield PlacesTransactions.Move({ guid: node.bookmarkGuid
-                                      , newParentGuid: newParentGuid });
+                                      , newParentGuid }).transact();
       }
     }.bind(this)).then(null, Components.utils.reportError);
   },
 
   newFolder: function MBD_newFolder() {
     // The command is disabled when the tree is not focused
     this.foldersTree.focus();
     goDoCommand("placesCmd_new:folder");
--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -4,103 +4,165 @@
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["PlacesTransactions"];
 
 /**
  * Overview
  * --------
- * This modules serves as the transactions manager for Places, and implements
- * all the standard transactions for its UI commands (creating items, editing
- * various properties, etc.). It shares most of its semantics with common
- * command pattern implementations, the HTML5 Undo Manager in particular.
- * However, the asynchronous design of [future] Places APIs, combined with the
- * commitment to serialize all UI operations, makes things a little bit
- * different.  For example, when |undo| is called in order to undo the top undo
- * entry, the caller cannot tell for sure what entry would it be because the
- * execution of some transaction is either in process, or queued.
+ * This modules serves as the transactions manager for Places (hereinafter PTM).
+ * It implements all the elementary transactions for its UI commands: creating
+ * items, editing their various properties, and so forth.
+ *
+ * Note that since the effect of invoking a Places command is not limited to the
+ * window in which it was performed (e.g. a folder created in the Library may be
+ * the parent of a bookmark created in some browser window), PTM is a singleton.
+ * It's therefore unnecessary to initialize PTM in any way apart importing this
+ * module.
  *
- * GUIDs and item-ids
- * -------------------
- * The Bookmarks API still relies heavily on item-ids, but since those do not
- * play nicely with the concept of undo and redo (especially not in an
- * asynchronous environment), this API only accepts bookmark GUIDs, both for
- * input (e.g. for specifying the parent folder for a new bookmark) and for
- * output (when the GUID for such a bookmark is propagated).
+ * PTM shares most of its semantics with common command pattern implementations.
+ * However, the asynchronous design of contemporary and future APIs, combined
+ * with the commitment to serialize all UI operations, does make things a little
+ * bit different.  For example, when |undo| is called in order to undo the top
+ * undo entry, the caller cannot tell for sure what entry would it be, because
+ * the execution of some transactions is either in process, or enqueued to be.
+ *
+ * Also note that unlike the nsITransactionManager, for example, this API is by
+ * no means generic.  That is, it cannot be used to execute anything but the
+ * elementary transactions implemented here (Please file a bug if you find
+ * anything uncovered).  More-complex transactions (e.g. creating a folder and
+ * moving a bookmark into it) may be implemented as a batch (see below).
  *
- * GUIDs are readily available when dealing with the "output" of this API and
- * when result nodes are used (see nsINavHistoryResultNode::bookmarkGuid).
- * If you only have item-ids in hand, use PlacesUtils.promiseItemGuid for
- * converting them.  Should you need to convert them back into itemIds, use
- * PlacesUtils.promiseItemId.
+ * A note about GUIDs and item-ids
+ * -------------------------------
+ * There's an ongoing effort (see bug 1071511) to deprecate item-ids in Places
+ * in favor of GUIDs.  Both because new APIs (e.g. Bookmark.jsm) expose them to
+ * the minimum necessary, and because GUIDs play much better with implementing
+ * |redo|, this API doesn't support item-ids at all, and only accepts bookmark
+ * GUIDs, both for input (e.g. for setting the parent folder for a new bookmark)
+ * and for output (when the GUID for such a bookmark is propagated).
  *
- * The Standard Transactions
+ * When working in conjugation with older Places API which only expose item ids,
+ * use PlacesUtils.promiseItemGuid for converting those to GUIDs (note that
+ * for result nodes, the guid is available through their bookmarkGuid getter).
+ * Should you need to convert GUIDs to item-ids, use PlacesUtils.promiseItemId.
+ *
+ * Constructing transactions
  * -------------------------
- * At the bottom of this module you will find implementations for all Places UI
- * commands (One should almost never fallback to raw Places APIs.  Please file
- * a bug if you find anything uncovered). The transactions' constructors are
- * set on the PlacesTransactions object (e.g. PlacesTransactions.NewFolder).
- * The input for this constructors is taken in the form of a single argument
- * plain object.  Input properties may be either required (e.g. the |keyword|
- * property for the EditKeyword transaction) or optional (e.g. the |keyword|
- * property for NewBookmark).  Once a transaction is created, you may pass it
- * to |transact| or use it in the for batching (see next section).
- *
- * The constructors throw right away when any required input is missing or when
- * some input is invalid "on the surface" (e.g. GUID values are validated to be
- * 12-characters strings, but are not validated to point to existing item.  Such
- * an error will reveal when the transaction is executed).
+ * At the bottom of this module you will find transactions for all Places UI
+ * commands.  They are exposed as constructors set on the PlacesTransactions
+ * object (e.g. PlacesTransactions.NewFolder).  The input for this constructors
+ * is taken in the form of a single argument, a plain object consisting of the
+ * properties for the transaction.  Input properties may be either required or
+ * optional (for example, |keyword| is required for the EditKeyword transaction,
+ * but optional for the NewBookmark transaction).
  *
  * To make things simple, a given input property has the same basic meaning and
  * valid values across all transactions which accept it in the input object.
  * Here is a list of all supported input properties along with their expected
  * values:
- *  - uri: an nsIURI object or an uri spec string.
- *  - feedURI: an nsIURI object, holding the url for a live bookmark.
- *  - siteURI: an nsIURI object, holding the url for the site with which
- *             a live bookmark is associated.
+ *  - url: a URL object, an nsIURI object, or a href.
+ *  - urls: an array of urls, as above.
+ *  - feedUrl: an url (as above), holding the url for a live bookmark.
+ *  - siteUrl an url (as above), holding the url for the site with which
+ *            a live bookmark is associated.
  *  - tag - a string.
  *  - tags: an array of strings.
  *  - guid, parentGuid, newParentGuid: a valid places GUID string.
  *  - title: a string
  *  - index, newIndex: the position of an item in its containing folder,
  *    starting from 0.
  *    integer and PlacesUtils.bookmarks.DEFAULT_INDEX
  *  - annotation: see PlacesUtils.setAnnotationsForItem
  *  - annotations: an array of annotation objects as above.
  *  - excludingAnnotation: a string (annotation name).
  *  - excludingAnnotations: an array of string (annotation names).
  *
- * Batching transactions
- * ---------------------
- * Sometimes it is useful to "batch" or "merge" transactions. For example,
- * "Bookmark All Tabs" may be implemented as one NewFolder transaction followed
- * by numerous NewBookmark transactions - all to be undone or redone in a single
- * command.  The |transact| method makes this possible using a generator
- * function as an input.  These generators have the same semantics as in
- * Task.jsm except that when you yield a transaction, it's executed, and the
- * resolution (e.g. the new bookmark GUID) is sent to the generator so you can
- * use it as the input for another transaction.  See |transact| for the details.
+ * If a required property is missing in the input object (e.g. not specifying
+ * parentGuid for NewBookmark), or if the value for any of the input properties
+ * is invalid "on the surface" (e.g. a numeric value for GUID, or a string that
+ * isn't 12-characters long), the transaction constructor throws right way.
+ * More complex errors (e.g. passing a non-existent GUID for parentGuid) only
+ * reveal once the transaction is executed.
+ *
+ * Executing Transactions (the |transact| method of transactions)
+ * --------------------------------------------------------------
+ * Once a transaction is created, you must call its |transact| method for it to
+ * be executed and take effect.  |transact| is an asynchronous method that takes
+ * no arguments, and returns a promise that resolves once the transaction is
+ * executed.  Executing one of the transactions for creating items (NewBookmark,
+ * NewFolder, NewSeparator or NewLivemark) resolve to the new item's GUID.
+ * There's no resolution value for other transactions.
+ * If a transaction fails to execute, |transact| rejects and the transactions
+ * history is not affected.
+ *
+ * |transact| throws if it's called more than once (successfully or not) on the
+ * same transaction object.
+ *
+ * Batches
+ * -------
+ * Sometimes it is useful to "batch" or "merge" transactions.  For example,
+ * something like "Bookmark All Tabs" may be implemented as one NewFolder
+ * transaction followed by numerous NewBookmark transactions - all to be undone
+ * or redone in a single undo or redo command.  Use |PlacesTransactions.batch|
+ * in such cases.  It can take either an array of transactions which will be
+ * executed in the given order and later be treated a a single entry in the
+ * transactions history, or a generator function that is passed to Task.spawn,
+ * that is to "contain" the batch: once the generator function is called a batch
+ * starts, and it lasts until the asynchronous generator iteration is complete
+ * All transactions executed by |transact| during this time are to be treated as
+ * a single entry in the transactions history.
  *
- * "Custom" transactions
- * ---------------------
- * In the legacy transactions API it was possible to pass-in transactions
- * implemented "externally".  For various reason this isn't allowed anymore:
- * transact throws right away if one attempts to pass a transaction that was not
- * created by this module.  However, it's almost always possible to achieve the
- * same functionality with the batching technique described above.
+ * In both modes, |PlacesTransactions.batch| returns a promise that is to be
+ * resolved when the batch ends.  In the array-input mode, there's no resolution
+ * value.  In the generator mode, the resolution value is whatever the generator
+ * function returned (the semantics are the same as in Task.spawn, basically).
+ *
+ * The array-input mode of |PlacesTransactions.batch| is useful for implementing
+ * a batch of mostly-independent transaction (for example, |paste| into a folder
+ * can be implemented as a batch of multiple NewBookmark transactions).
+ * The generator mode is useful when the resolution value of executing one
+ * transaction is the input of one more subsequent transaction.
+ *
+ * In the array-input mode, if any transactions fails to execute, the batch
+ * continues (exceptions are logged).  Only transactions that were executed
+ * successfully are added to the transactions history.
+ *
+ * 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
+ * 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
+ * -------------
+ * All |PlacesTransaction| operations are serialized.  That is, even though the
+ * implementation is asynchronous, the order in which PlacesTransactions methods
+ * is called does guarantee the order in which they are to be invoked.
+ *
+ * The only exception to this rule is |transact| calls done during a batch (see
+ * above).  |transact| calls are serialized with each other (and with undo, redo
+ * and clearTransactionsHistory), but they  are, of course, not serialized with
+ * batches.
  *
  * The transactions-history structure
  * ----------------------------------
  * The transactions-history is a two-dimensional stack of transactions: the
  * transactions are ordered in reverse to the order they were committed.
- * It's two-dimensional because the undo manager allows batching transactions
- * together for the purpose of undo or redo (batched transactions can never be
- * undone or redone partially).
+ * It's two-dimensional because PTM allows batching transactions together for
+ * the purpose of undo or redo (see Batches above).
  *
  * The undoPosition property is set to the index of the top entry. If there is
  * no entry at that index, there is nothing to undo.
  * Entries prior to undoPosition, if any, are redo entries, the first one being
  * the top redo entry.
  *
  * [ [2nd redo txn, 1st redo txn],  <= 2nd redo entry
  *   [2nd redo txn, 1st redo txn],  <= 1st redo entry
@@ -119,32 +181,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/devtools/Console.jsm");
 
-// Updates commands in the undo group of the active window commands.
-// Inactive windows commands will be updated on focus.
-function 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"); }
-}
+Components.utils.importGlobalProperties(["URL"]);
 
-// The internal object for managing the transactions history.
-// The public API is included in PlacesTransactions.
-// TODO bug 982099: extending the array "properly" makes it painful to implement
-// getters.  If/when ES6 gets proper array subclassing we can revise this.
 let 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,
   get undoPosition() this._undoPosition,
@@ -164,17 +212,19 @@ TransactionsHistory.__proto__ = {
   /**
    * Proxify a transaction object for consumers.
    * @param aRawTransaction
    *        the raw transaction object.
    * @return the proxified transaction object.
    * @see getRawTransaction for retrieving the raw transaction.
    */
   proxifyTransaction: function (aRawTransaction) {
-    let proxy = Object.freeze({});
+    let proxy = Object.freeze({
+      transact() TransactionsManager.transact(this)
+    });
     this.proxifiedToRaw.set(proxy, aRawTransaction);
     return proxy;
   },
 
   /**
    * Check if the given object is a the proxy object for some transaction.
    * @param aValue
    *        any JS value.
@@ -186,314 +236,143 @@ TransactionsHistory.__proto__ = {
 
   /**
    * 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: function (aProxy) this.proxifiedToRaw.get(aProxy),
-
-  /**
-   * Undo the top undo entry, if any, and update the undo position accordingly.
-   */
-  undo: function* () {
-    let entry = this.topUndoEntry;
-    if (!entry)
-      return;
-
-    for (let transaction of entry) {
-      try {
-        yield TransactionsHistory.getRawTransaction(transaction).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.");
-        this.clearUndoEntries();
-        return;
-      }
-    }
-    this._undoPosition++;
-    updateCommandsOnActiveWindow();
-  },
-
-  /**
-   * Redo the top redo entry, if any, and update the undo position accordingly.
-   */
-  redo: function* () {
-    let entry = this.topRedoEntry;
-    if (!entry)
-      return;
-
-    for (let i = entry.length - 1; i >= 0; i--) {
-      let transaction = TransactionsHistory.getRawTransaction(entry[i]);
-      try {
-        if (transaction.redo)
-          yield transaction.redo();
-        else
-          yield 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.");
-        this.clearRedoEntries();
-        return;
-      }
-    }
-    this._undoPosition--;
-    updateCommandsOnActiveWindow();
-  },
+  getRawTransaction(aProxy) this.proxifiedToRaw.get(aProxy),
 
   /**
    * 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: function (aProxifiedTransaction, aForceNewEntry = false) {
+  add(aProxifiedTransaction, aForceNewEntry = false) {
     if (!this.isProxifiedTransactionObject(aProxifiedTransaction))
       throw new Error("aProxifiedTransaction is not a proxified transaction");
 
     if (this.length == 0 || aForceNewEntry) {
       this.clearRedoEntries();
       this.unshift([aProxifiedTransaction]);
     }
     else {
       this[this.undoPosition].unshift(aProxifiedTransaction);
     }
-    updateCommandsOnActiveWindow();
   },
 
   /**
    * Clear all undo entries.
    */
-  clearUndoEntries: function () {
+  clearUndoEntries() {
     if (this.undoPosition < this.length)
       this.splice(this.undoPosition);
   },
 
   /**
    * Clear all redo entries.
    */
-  clearRedoEntries: function () {
+  clearRedoEntries() {
     if (this.undoPosition > 0) {
       this.splice(0, this.undoPosition);
       this._undoPosition = 0;
     }
   },
 
   /**
    * Clear all entries.
    */
-  clearAllEntries: function () {
+  clearAllEntries() {
     if (this.length > 0) {
       this.splice(0);
       this._undoPosition = 0;
     }
   }
 };
 
 
-// Our transaction manager is asynchronous in the sense that all of its methods
-// don't execute synchronously. However, all actions must be serialized.
-let currentTask = Promise.resolve();
-function Serialize(aTask) {
-  // Ignore failures.
-  return currentTask = currentTask.then( () => Task.spawn(aTask) )
-                                  .then(null, Components.utils.reportError);
-}
-
-// 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.
-let executedTransactions = new WeakMap(); // TODO: use WeakSet (bug 792439)
-executedTransactions.add = k => executedTransactions.set(k, null);
-
 let PlacesTransactions = {
   /**
-   * Asynchronously transact either a single transaction, or a sequence of
-   * transactions that would be treated as a single entry in the transactions
-   * history.
-   *
-   * @param aToTransact
-   *        Either a transaction object or an array of transaction objects, or a
-   *        generator function (ES6-style only) that yields transaction objects.
-   *
-   *        Generator mode how-to: when a transaction is yielded, it's executed.
-   *        Then, if it was executed successfully, the resolution of |execute|
-   *        is sent to the generator.  If |execute| threw or rejected, the
-   *        exception is propagated to the generator.
-   *        Any other value yielded by a generator function is handled the
-   *        same way as in a Task (see Task.jsm).
-   *
-   * @return {Promise}
-   * @resolves either to the resolution of |execute|, in single-transaction
-   * mode, or to the return value of the generator, in generator-mode. For an
-   * array of transactions, there's no resolution value.
-   *
-   * @rejects either if |execute| threw, in single-transaction mode, or if
-   * the generator function threw (or didn't handle) an exception, in generator
-   * mode.
-   * @throws if aTransactionOrGeneratorFunction is neither a transaction object
-   * created by this module, nor an array of such object, nor a generator
-   * function.
-   * @note If no transaction was executed successfully, the transactions history
-   * is not affected.
-   *
-   * @note All PlacesTransactions operations are serialized. This means that the
-   * transactions history state may change by the time the transaction/generator
-   * is processed.  It's guaranteed, however, that a generator function "blocks"
-   * the queue: that is, it is assured that no other operations are performed
-   * by or on PlacesTransactions until the generator returns.  Keep in mind you
-   * are not protected from consumers who use the raw places APIs directly.
+   * @see Batches in the module documentation.
    */
-  transact: function (aToTransact) {
-    if (Array.isArray(aToTransact)) {
-      if (aToTransact.some(
+  batch(aToBatch) {
+    let batchFunc;
+    if (Array.isArray(aToBatch)) {
+      if (aToBatch.length == 0)
+        throw new Error("aToBatch must not be an empty array");
+
+      if (aToBatch.some(
            o => !TransactionsHistory.isProxifiedTransactionObject(o))) {
-        throw new Error("aToTransact contains non-transaction element");
+        throw new Error("aToBatch contains non-transaction element");
       }
-      // Proceed as if a generator yielding the transactions was passed in.
-      return this.transact(function* () {
-        for (let t of aToTransact) {
-          yield t;
+      return TransactionsManager.batch(function* () {
+        for (let txn of aToBatch) {
+          try {
+            yield txn.transact();
+          }
+          catch(ex) {
+            console.error(ex);
+          }
         }
       });
     }
-
-    let isGeneratorObj =
-      o => Object.prototype.toString.call(o) ==  "[object Generator]";
-
-    let generator = null;
-    if (typeof(aToTransact) == "function") {
-      generator = aToTransact();
-      if (!isGeneratorObj(generator))
-        throw new Error("aToTransact is not a generator function");
-    }
-    else if (!TransactionsHistory.isProxifiedTransactionObject(aToTransact)) {
-      throw new Error("aToTransact is not a valid transaction object");
-    }
-    else if (executedTransactions.has(aToTransact)) {
-      throw new Error("Transactions objects may not be recycled.");
+    if (typeof(aToBatch) == "function") {
+      return TransactionsManager.batch(aToBatch);
     }
 
-    return Serialize(function* () {
-      // The entry in the transactions history is created once the first
-      // transaction is committed. This means that if |transact| is called
-      // in its "generator mode" and no transactions are committed by the
-      // generator, the transactions history is left unchanged.
-      // Bug 982115: Depending on how this API is actually used we may revise
-      // this decision and make it so |transact| always forces a new entry.
-      let forceNewEntry = true;
-      function* transactOneTransaction(aTransaction) {
-        let retval =
-          yield TransactionsHistory.getRawTransaction(aTransaction).execute();
-        executedTransactions.add(aTransaction);
-        TransactionsHistory.add(aTransaction, forceNewEntry);
-        forceNewEntry = false;
-        return retval;
-      }
-
-      function* transactBatch(aGenerator) {
-        let error = false;
-        let sendValue = undefined;
-        while (true) {
-          let next = error ?
-                     aGenerator.throw(sendValue) : aGenerator.next(sendValue);
-          sendValue = next.value;
-          if (isGeneratorObj(sendValue)) {
-            sendValue = yield transactBatch(sendValue);
-          }
-          else if (typeof(sendValue) == "object" && sendValue) {
-            if (TransactionsHistory.isProxifiedTransactionObject(sendValue)) {
-              if (executedTransactions.has(sendValue)) {
-                sendValue = new Error("Transactions may not be recycled.");
-                error = true;
-              }
-              else {
-                sendValue = yield transactOneTransaction(sendValue);
-              }
-            }
-            else if ("then" in sendValue) {
-              sendValue = yield sendValue;
-            }
-          }
-          if (next.done)
-            break;
-        }
-        return sendValue;
-      }
-
-      if (generator)
-        return yield transactBatch(generator);
-      else
-        return yield transactOneTransaction(aToTransact);
-    }.bind(this));
+    throw new Error("aToBatch must be 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.
    * @note All undo manager operations are queued. This means that transactions
    * history may change by the time your request is fulfilled.
    */
-  undo: function () Serialize(() => TransactionsHistory.undo()),
+  undo() TransactionsManager.undo(),
 
   /**
    * Asynchronously redo the transaction immediately before the current undo
    * position in the transactions history, if any, and adjusts the undo
    * position.
    *
    * @return {Promises).  The promise always resolves.
    * @note All undo manager operations are queued. This means that transactions
    * history may change by the time your request is fulfilled.
    */
-  redo: function () Serialize(() => TransactionsHistory.redo()),
+  redo() TransactionsManager.redo(),
 
   /**
    * Asynchronously clear the undo, redo, or all entries from the transactions
    * history.
    *
    * @param [optional] aUndoEntries
    *        Whether or not to clear undo entries.  Default: true.
    * @param [optional] aRedoEntries
    *        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:
-  function (aUndoEntries = true, aRedoEntries = true) {
-    return Serialize(function* () {
-      if (aUndoEntries && aRedoEntries)
-        TransactionsHistory.clearAllEntries();
-      else if (aUndoEntries)
-        TransactionsHistory.clearUndoEntries();
-      else if (aRedoEntries)
-        TransactionsHistory.clearRedoEntries();
-      else
-        throw new Error("either aUndoEntries or aRedoEntries should be true");
-    });
-  },
+  clearTransactionsHistory(aUndoEntries = true, aRedoEntries = true)
+    TransactionsManager.clearTransactionsHistory(aUndoEntries, aRedoEntries),
 
   /**
    * The numbers of entries in the transactions history.
    */
   get length() TransactionsHistory.length,
 
   /**
    * Get the transaction history entry at a given index.  Each entry consists
@@ -502,17 +381,17 @@ let PlacesTransactions = {
    * @param aIndex
    *        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: function (aIndex) {
+  entry(aIndex) {
     if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
       throw new Error("Invalid index");
 
     return TransactionsHistory[aIndex];
   },
 
   /**
    * The index of the top undo entry in the transactions history.
@@ -529,16 +408,226 @@ let PlacesTransactions = {
 
   /**
    * Shortcut for accessing the top redo entry in the transaction history.
    */
   get topRedoEntry() TransactionsHistory.topRedoEntry
 };
 
 /**
+ * Helper for serializing the calls to TransactionsManager methods. It allows
+ * us to guarantee that the order in which TransactionsManager asynchronous
+ * methods are called also enforces the order in which they're executed, and
+ * that they are never executed in parallel.
+ *
+ * In other words: Enqueuer.enqueue(aFunc1); Enqueuer.enqueue(aFunc2) is roughly
+ * the same as Task.spawn(aFunc1).then(Task.spawn(aFunc2)).
+ */
+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
+   *          @see Task.spawn.
+   * @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(Task.async(aFunc));
+
+    // 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
+   *        any promise.
+   */
+  alsoWaitFor(aPromise) {
+    // We don't care if aPromise resolves or rejects, but just that is not
+    // pending anymore.
+    let promise = aPromise.catch(console.error);
+    this._promise = Promise.all([this._promise, promise]);
+  },
+
+  /**
+   * The promise for this queue.
+   */
+  get promise() this._promise
+};
+
+
+let TransactionsManager = {
+  // See the documentation at the top of this file. |transact| calls are not
+  // serialized with |batch| calls.
+  _mainEnqueuer: new Enqueuer(),
+  _transactEnqueuer: new Enqueuer(),
+
+  // Is a batch in progress? set when we enter a batch function and unset when
+  // it's execution is done.
+  _batching: false,
+
+  // If a batch started, this indicates if we've already created an entry in the
+  // transactions history for the batch (i.e. if at least one transaction was
+  // 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);
+    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(function* () {
+      // Don't try to catch exceptions. If execute fails, we better not add the
+      // transaction to the undo stack.
+      let retval = yield rawTxn.execute();
+
+      let forceNewEntry = !this._batching || !this._createdBatchEntry;
+      TransactionsHistory.add(aTxnProxy, forceNewEntry);
+      if (this._batching)
+        this._createdBatchEntry = true;
+
+      this._updateCommandsOnActiveWindow();
+      return retval;
+    }.bind(this));
+    this._mainEnqueuer.alsoWaitFor(promise);
+    return promise;
+  },
+
+  batch(aTask) {
+    return this._mainEnqueuer.enqueue(function* () {
+      this._batching = true;
+      this._createdBatchEntry = false;
+      let rv;
+      try {
+        // We should return here, but bug 958949 makes that impossible.
+        rv = (yield Task.spawn(aTask));
+      }
+      finally {
+        this._batching = false;
+        this._createdBatchEntry = false;
+      }
+      return rv;
+    }.bind(this));
+  },
+
+  /**
+   * Undo the top undo entry, if any, and update the undo position accordingly.
+   */
+  undo() {
+    let promise = this._mainEnqueuer.enqueue(function* () {
+      let entry = TransactionsHistory.topUndoEntry;
+      if (!entry)
+        return;
+
+      for (let txnProxy of entry) {
+        try {
+          yield 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.");
+          TransactionsHistory.clearUndoEntries();
+          return;
+        }
+      }
+      TransactionsHistory._undoPosition++;
+      this._updateCommandsOnActiveWindow();
+    }.bind(this));
+    this._transactEnqueuer.alsoWaitFor(promise);
+    return promise;
+  },
+
+  /**
+   * Redo the top redo entry, if any, and update the undo position accordingly.
+   */
+  redo() {
+    let promise = this._mainEnqueuer.enqueue(function* () {
+      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)
+            yield transaction.redo();
+          else
+            yield 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.");
+          TransactionsHistory.clearRedoEntries();
+          return;
+        }
+      }
+      TransactionsHistory._undoPosition--;
+      this._updateCommandsOnActiveWindow();
+    }.bind(this));
+
+    this._transactEnqueuer.alsoWaitFor(promise);
+    return promise;
+  },
+
+  clearTransactionsHistory(aUndoEntries, aRedoEntries) {
+    let promise = this._mainEnqueuer.enqueue(function* () {
+      if (aUndoEntries && aRedoEntries)
+        TransactionsHistory.clearAllEntries();
+      else if (aUndoEntries)
+        TransactionsHistory.clearUndoEntries();
+      else if (aRedoEntries)
+        TransactionsHistory.clearRedoEntries();
+      else
+        throw new Error("either aUndoEntries or aRedoEntries should be true");
+    }.bind(this));
+
+    this._transactEnqueuer.alsoWaitFor(promise);
+    return promise;
+  },
+
+  // 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"); }
+  }
+};
+
+/**
  * 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
  * the input properties to the function arguments (required properties first,
  * then the optional properties).
  *
  * If this seems confusing, look at the consumers.
@@ -613,20 +702,23 @@ DefineTransaction.annotationObjectValida
     // Nothing else should be set
     let validKeys = ["name", "value", "flags", "expires"];
     if (Object.keys(obj).every( (k) => validKeys.indexOf(k) != -1 ))
       return obj;
   }
   throw new Error("Invalid annotation object");
 };
 
-DefineTransaction.uriValidate = function(uriOrSpec) {
-  if (uriOrSpec instanceof Components.interfaces.nsIURI)
-    return uriOrSpec;
-  return NetUtil.newURI(uriOrSpec);
+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);
 };
 
 DefineTransaction.inputProps = new Map();
 DefineTransaction.defineInputProps =
 function (aNames, aValidationFunction, aDefaultValue) {
   for (let name of aNames) {
     // Workaround bug 449811.
     let propName = name;
@@ -667,18 +759,18 @@ function (aName, aBasePropertyName) {
       if (!Array.isArray(aValue))
         throw new Error(`${aName} input property value must be an array`);
 
       // This also takes care of abandoning the global scope of the input
       // array (through Array.prototype).
       return [for (e of aValue) baseProp.validateValue(e)];
     },
 
-    // We allow setting either the array property itself (e.g. uris), or a
-    // single element of it (uri, in that example), that is then transformed
+    // 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: function (aInput, aRequired) {
       if (aName in aInput) {
         // 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`);
         }
@@ -758,31 +850,31 @@ function (aInput, aRequiredProps = [], a
     fixedInput[prop] = this.validatePropertyValue(prop, input, false);
   }
 
   return fixedInput;
 };
 
 // Update the documentation at the top of this module if you add or
 // remove properties.
-DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"],
-                                   DefineTransaction.uriValidate, null);
+DefineTransaction.defineInputProps(["url", "feedUrl", "siteUrl"],
+                                   DefineTransaction.urlValidate, null);
 DefineTransaction.defineInputProps(["guid", "parentGuid", "newParentGuid"],
                                    DefineTransaction.guidValidate);
 DefineTransaction.defineInputProps(["title"],
                                    DefineTransaction.strOrNullValidate, null);
 DefineTransaction.defineInputProps(["keyword", "postData", "tag",
                                     "excludingAnnotation"],
                                    DefineTransaction.strValidate, "");
 DefineTransaction.defineInputProps(["index", "newIndex"],
                                    DefineTransaction.indexValidate,
                                    PlacesUtils.bookmarks.DEFAULT_INDEX);
 DefineTransaction.defineInputProps(["annotation"],
                                    DefineTransaction.annotationObjectValidate);
-DefineTransaction.defineArrayInputProp("uris", "uri");
+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.
@@ -950,22 +1042,22 @@ function* createItemsFromBookmarksTree(a
  * are also documented there.
  *****************************************************************************/
 
 let PT = PlacesTransactions;
 
 /**
  * Transaction for creating a bookmark.
  *
- * Required Input Properties: uri, parentGuid.
+ * 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", "uri"],
+PT.NewBookmark = DefineTransaction(["parentGuid", "url"],
                                    ["index", "title", "keyword", "postData",
                                     "annotations", "tags"]);
 PT.NewBookmark.prototype = Object.seal({
   execute: function (aParentGuid, aURI, aIndex, aTitle,
                      aKeyword, aPostData, aAnnos, aTags) {
     return ExecuteCreateItem(this, aParentGuid,
       function (parentId, guidToRestore = "") {
         let itemId = PlacesUtils.bookmarks.insertBookmark(
@@ -1034,24 +1126,24 @@ PT.NewSeparator.prototype = Object.seal(
       });
   }
 });
 
 /**
  * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the
  * semantics).
  *
- * Required Input Properties: feedURI, title, parentGuid.
- * Optional Input Properties: siteURI, index, annotations.
+ * 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(["feedURI", "title", "parentGuid"],
-                                   ["siteURI", "index", "annotations"]);
+PT.NewLivemark = DefineTransaction(["feedUrl", "title", "parentGuid"],
+                                   ["siteUrl", "index", "annotations"]);
 PT.NewLivemark.prototype = Object.seal({
   execute: function* (aFeedURI, aTitle, aParentGuid, aSiteURI, aIndex, aAnnos) {
     let livemarkInfo = { title: aTitle
                        , feedURI: aFeedURI
                        , siteURI: aSiteURI
                        , index: aIndex };
     let createItem = function* () {
       livemarkInfo.parentId = yield PlacesUtils.promiseItemId(aParentGuid);
@@ -1127,20 +1219,20 @@ PT.EditTitle.prototype = Object.seal({
     PlacesUtils.bookmarks.setItemTitle(itemId, aTitle);
     this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); };
   }
 });
 
 /**
  * Transaction for setting the URI for an item.
  *
- * Required Input Properties: guid, uri.
+ * Required Input Properties: guid, url.
  */
-PT.EditURI = DefineTransaction(["guid", "uri"]);
-PT.EditURI.prototype = Object.seal({
+PT.EditUrl = DefineTransaction(["guid", "url"]);
+PT.EditUrl.prototype = Object.seal({
   execute: function* (aGuid, aURI) {
     let itemId = yield PlacesUtils.promiseItemId(aGuid),
         oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId),
         oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI),
         newURIAdditionalTags = null;
     PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI);
 
     // Move tags from old URI to new URI.
@@ -1310,19 +1402,19 @@ PT.Remove.prototype = {
     PlacesUtils.bookmarks.removeItem(yield PlacesUtils.promiseItemId(aGuid));
     this.undo = createItemsFromBookmarksTree.bind(null, itemInfo, true);
   }
 };
 
 /**
  * Transaction for tagging a URI.
  *
- * Required Input Properties: uris, tags.
+ * Required Input Properties: urls, tags.
  */
-PT.Tag = DefineTransaction(["uris", "tags"]);
+PT.Tag = DefineTransaction(["urls", "tags"]);
 PT.Tag.prototype = {
   execute: function* (aURIs, aTags) {
     let onUndo = [], onRedo = [];
     for (let uri of aURIs) {
       // Workaround bug 449811.
       let currentURI = uri;
 
       let promiseIsBookmarked = function* () {
@@ -1330,17 +1422,17 @@ PT.Tag.prototype = {
         PlacesUtils.asyncGetBookmarkIds(
           currentURI, ids => { deferred.resolve(ids.length > 0); });
         return deferred.promise;
       };
 
       if (yield promiseIsBookmarked(currentURI)) {
         // Tagging is only allowed for bookmarked URIs (but see 424160).
         let createTxn = TransactionsHistory.getRawTransaction(
-          PT.NewBookmark({ uri: currentURI
+          PT.NewBookmark({ url: currentURI
                          , tags: aTags
                          , parentGuid: PlacesUtils.bookmarks.unfiledGuid }));
         yield createTxn.execute();
         onUndo.unshift(createTxn.undo.bind(createTxn));
         onRedo.push(createTxn.redo.bind(createTxn));
       }
       else {
         let currentTags = PlacesUtils.tagging.getTagsForURI(currentURI);
@@ -1365,22 +1457,22 @@ PT.Tag.prototype = {
       }
     };
   }
 };
 
 /**
  * Transaction for removing tags from a URI.
  *
- * Required Input Properties: uris.
+ * Required Input Properties: urls.
  * Optional Input Properties: tags.
  *
- * If |tags| is not set, all tags set for |uri| are removed.
+ * If |tags| is not set, all tags set for |url| are removed.
  */
-PT.Untag = DefineTransaction(["uris"], ["tags"]);
+PT.Untag = DefineTransaction(["urls"], ["tags"]);
 PT.Untag.prototype = {
   execute: function* (aURIs, aTags) {
     let onUndo = [], onRedo = [];
     for (let uri of aURIs) {
       // Workaround bug 449811.
       let currentURI = uri;
       let tagsToRemove;
       let tagsSet = PlacesUtils.tagging.getTagsForURI(currentURI);
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -4,16 +4,18 @@
  * 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/. */
 
 const bmsvc   = PlacesUtils.bookmarks;
 const tagssvc = PlacesUtils.tagging;
 const annosvc = PlacesUtils.annotations;
 const PT      = PlacesTransactions;
 
+Components.utils.importGlobalProperties(["URL"]);
+
 // Create and add bookmarks observer.
 let observer = {
   __proto__: NavBookmarkObserver.prototype,
 
   tagRelatedGuids: new Set(),
 
   reset: function () {
     this.itemsAdded = new Map();
@@ -43,17 +45,17 @@ let observer = {
       return;
     }
 
     this.itemsAdded.set(aGuid, { itemId:         aItemId
                                , parentGuid:     aParentGuid
                                , index:          aIndex
                                , itemType:       aItemType
                                , title:          aTitle
-                               , uri:            aURI });
+                               , url:            aURI });
   },
 
   onItemRemoved:
   function (aItemId, aParentId, aIndex, aItemType, aURI, aGuid, aParentGuid) {
     if (this.tagRelatedGuids.has(aGuid))
       return;
 
     this.itemsRemoved.set(aGuid, { parentGuid: aParentGuid
@@ -169,18 +171,18 @@ function ensureItemsAdded(...items) {
   for (let item of items) {
     Assert.ok(observer.itemsAdded.has(item.guid));
     let info = observer.itemsAdded.get(item.guid);
     Assert.equal(info.parentGuid, item.parentGuid);
     for (let propName of ["title", "index", "itemType"]) {
       if (propName in item)
         Assert.equal(info[propName], item[propName]);
     }
-    if ("uri" in item)
-      Assert.ok(info.uri.equals(item.uri));
+    if ("url" in item)
+      Assert.ok(info.url.equals(item.url));
   }
 }
 
 function ensureItemsRemoved(...items) {
   Assert.equal(observer.itemsRemoved.size, items.length);
   for (let item of items) {
     // We accept both guids and full info object here.
     if (typeof(item) == "string") {
@@ -199,18 +201,18 @@ function ensureItemsRemoved(...items) {
 function ensureItemsChanged(...items) {
   for (let item of items) {
     do_check_true(observer.itemsChanged.has(item.guid));
     let changes = observer.itemsChanged.get(item.guid);
     do_check_true(changes.has(item.property));
     let info = changes.get(item.property);
     do_check_eq(info.isAnnoProperty, Boolean(item.isAnnoProperty));
     do_check_eq(info.newValue, item.newValue);
-    if ("uri" in item)
-      do_check_true(item.uri.equals(info.uri));
+    if ("url" in item)
+      do_check_true(item.url.equals(info.url));
   }
 }
 
 function ensureAnnotationsSet(aGuid, aAnnos) {
   do_check_true(observer.itemsChanged.has(aGuid));
   let changes = observer.itemsChanged.get(aGuid);
   for (let anno of aAnnos) {
     do_check_true(changes.has(anno.name));
@@ -320,99 +322,70 @@ function* ensureBookmarksTreeRestoredCor
 }
 
 function* ensureNonExistent(...aGuids) {
   for (let guid of aGuids) {
     Assert.strictEqual((yield PlacesUtils.promiseBookmarksTree(guid)), null);
   }
 }
 
-add_task(function* test_invalid_transact_calls() {
-  try {
-    PT.transact({ execute: () => {}, undo: () => {}, redo: () => {}});
-    do_throw("transact shouldn't accept 'external' transactions");
-    PT.transact(null);
-    do_throw("transact should throw for invalid arguments");
-  }
-  catch(ex) { }
-});
-
 add_task(function* test_recycled_transactions() {
   function ensureTransactThrowsFor(aTransaction) {
     let [txns, undoPosition] = getTransactionsHistoryState();
     try {
-      yield PT.transact(aTransaction);
+      yield aTransaction.transact();
       do_throw("Shouldn't be able to use the same transaction twice");
     }
     catch(ex) { }
     ensureUndoState(txns, undoPosition);
   }
 
   let txn_a = PT.NewFolder(yield createTestFolderInfo());
-  ensureTransactThrowsFor(txn_a);
-  yield PT.transact(txn_a);
+  yield txn_a.transact();
   ensureUndoState([[txn_a]], 0);
+  yield ensureTransactThrowsFor(txn_a);
 
   yield PT.undo();
   ensureUndoState([[txn_a]], 1);
   ensureTransactThrowsFor(txn_a);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
   ensureTransactThrowsFor(txn_a);
 
   let txn_b = PT.NewFolder(yield createTestFolderInfo());
-  yield PT.transact(function* () {
+  yield PT.batch(function* () {
     try {
-      yield txn_a;
+      yield txn_a.transact();
       do_throw("Shouldn't be able to use the same transaction twice");
     }
     catch(ex) { }
     ensureUndoState();
-    yield txn_b;
+    yield txn_b.transact();
   });
   ensureUndoState([[txn_b]], 0);
 
   yield PT.undo();
   ensureUndoState([[txn_b]], 1);
   ensureTransactThrowsFor(txn_a);
   ensureTransactThrowsFor(txn_b);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
   observer.reset();
 });
 
-add_task(function* test_nested_batches() {
-  let txn_a = PT.NewFolder(yield createTestFolderInfo()),
-      txn_b = PT.NewFolder(yield createTestFolderInfo());
-  yield PT.transact(function* () {
-    yield txn_a;
-    yield (function*() {
-      yield txn_b;
-    }());
-  });
-  ensureUndoState([[txn_b, txn_a]], 0);
-
-  yield PT.undo();
-  ensureUndoState([[txn_b, txn_a]], 1);
-
-  yield PT.clearTransactionsHistory();
-  ensureUndoState();
-  observer.reset();
-});
-
 add_task(function* test_new_folder_with_annotation() {
   const ANNO = { name: "TestAnno", value: "TestValue" };
   let folder_info = yield createTestFolderInfo();
   folder_info.index = bmStartIndex;
   folder_info.annotations = [ANNO];
   ensureUndoState();
   let txn = PT.NewFolder(folder_info);
-  folder_info.guid = yield PT.transact(txn);
+  folder_info.guid = yield txn.transact();
   let ensureDo = function* (aRedo = false) {
     ensureUndoState([[txn]], 0);
     yield ensureItemsAdded(folder_info);
     ensureAnnotationsSet(folder_info.guid, [ANNO]);
     if (aRedo)
       ensureTimestampsUpdated(folder_info.guid, true);
     observer.reset();
   };
@@ -433,23 +406,23 @@ add_task(function* test_new_folder_with_
   yield PT.undo();
   ensureUndo();
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_new_bookmark() {
   let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
-                , uri:        NetUtil.newURI("http://test_create_item.com")
+                , 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 = yield PT.transact(txn);
+  bm_info.guid = yield txn.transact();
 
   let ensureDo = function* (aRedo = false) {
     ensureUndoState([[txn]], 0);
     yield ensureItemsAdded(bm_info);
     if (aRedo)
       ensureTimestampsUpdated(bm_info.guid, true);
     observer.reset();
   };
@@ -470,26 +443,26 @@ add_task(function* test_new_bookmark() {
   ensureUndo();
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_merge_create_folder_and_item() {
   let folder_info = yield createTestFolderInfo();
-  let bm_info = { uri: NetUtil.newURI("http://test_create_item_to_folder.com")
+  let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com")
                 , title: "Test Bookmark"
                 , index: bmStartIndex };
 
-  let { folderTxn, bkmTxn } = yield PT.transact( function* () {
+  let { folderTxn, bkmTxn } = yield PT.batch(function* () {
     let folderTxn = PT.NewFolder(folder_info);
-    folder_info.guid = bm_info.parentGuid = yield folderTxn;
+    folder_info.guid = bm_info.parentGuid = yield folderTxn.transact();
     let bkmTxn = PT.NewBookmark(bm_info);
-    bm_info.guid = yield bkmTxn;;
-    return { folderTxn: folderTxn, bkmTxn: bkmTxn};
+    bm_info.guid = yield bkmTxn.transact();
+    return { folderTxn, bkmTxn };
   });
 
   let ensureDo = function* () {
     ensureUndoState([[bkmTxn, folderTxn]], 0);
     yield ensureItemsAdded(folder_info, bm_info);
     observer.reset();
   };
 
@@ -508,39 +481,39 @@ add_task(function* test_merge_create_fol
   ensureUndo();
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_move_items_to_folder() {
   let folder_a_info = yield createTestFolderInfo("Folder A");
-  let bkm_a_info = { uri: NetUtil.newURI("http://test_move_items.com")
+  let bkm_a_info = { url: new URL("http://test_move_items.com")
                    , title: "Bookmark A" };
-  let bkm_b_info = { uri: NetUtil.newURI("http://test_move_items.com")
+  let bkm_b_info = { url: NetUtil.newURI("http://test_move_items.com")
                    , title: "Bookmark B" };
 
   // Test moving items within the same folder.
-  let [folder_a_txn, bkm_a_txn, bkm_b_txn] = yield PT.transact(function* () {
+  let [folder_a_txn, bkm_a_txn, bkm_b_txn] = yield PT.batch(function* () {
     let folder_a_txn = PT.NewFolder(folder_a_info);
 
-    folder_a_info.guid =
-      bkm_a_info.parentGuid = bkm_b_info.parentGuid = yield folder_a_txn;
+    folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid =
+      yield folder_a_txn.transact();
     let bkm_a_txn = PT.NewBookmark(bkm_a_info);
-    bkm_a_info.guid = yield bkm_a_txn;
+    bkm_a_info.guid = yield bkm_a_txn.transact();
     let bkm_b_txn = PT.NewBookmark(bkm_b_info);
-    bkm_b_info.guid = yield bkm_b_txn;
+    bkm_b_info.guid = yield bkm_b_txn.transact();
     return [folder_a_txn, bkm_a_txn, bkm_b_txn];
   });
 
   ensureUndoState([[bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
 
   let moveTxn = PT.Move({ guid:          bkm_a_info.guid
                         , newParentGuid: folder_a_info.guid });
-  yield PT.transact(moveTxn);
+  yield moveTxn.transact();
 
   let ensureDo = () => {
     ensureUndoState([[moveTxn], [bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
     ensureItemsMoved({ guid:          bkm_a_info.guid
                      , oldParentGuid: folder_a_info.guid
                      , newParentGuid: folder_a_info.guid
                      , oldIndex:      0
                      , newIndex:      1 });
@@ -565,24 +538,24 @@ add_task(function* test_move_items_to_fo
   ensureUndo();
 
   yield PT.clearTransactionsHistory(false, true);
   ensureUndoState([[bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
 
   // Test moving items between folders.
   let folder_b_info = yield createTestFolderInfo("Folder B");
   let folder_b_txn = PT.NewFolder(folder_b_info);
-  folder_b_info.guid = yield PT.transact(folder_b_txn);
+  folder_b_info.guid = yield folder_b_txn.transact();
   ensureUndoState([ [folder_b_txn]
                   , [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 0);
 
   moveTxn = PT.Move({ guid:          bkm_a_info.guid
                     , newParentGuid: folder_b_info.guid
                     , newIndex:      bmsvc.DEFAULT_INDEX });
-  yield PT.transact(moveTxn);
+  yield moveTxn.transact();
 
   ensureDo = () => {
     ensureUndoState([ [moveTxn]
                     , [folder_b_txn]
                     , [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 0);
     ensureItemsMoved({ guid:          bkm_a_info.guid
                      , oldParentGuid: folder_a_info.guid
                      , newParentGuid: folder_b_info.guid
@@ -620,31 +593,31 @@ add_task(function* test_move_items_to_fo
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_remove_folder() {
   let folder_level_1_info = yield createTestFolderInfo("Folder Level 1");
   let folder_level_2_info = { title: "Folder Level 2" };
   let [folder_level_1_txn,
-       folder_level_2_txn] = yield PT.transact(function* () {
+       folder_level_2_txn] = yield PT.batch(function* () {
     let folder_level_1_txn  = PT.NewFolder(folder_level_1_info);
-    folder_level_1_info.guid = yield folder_level_1_txn;
+    folder_level_1_info.guid = yield 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 = yield folder_level_2_txn;
+    folder_level_2_info.guid = yield folder_level_2_txn.transact();
     return [folder_level_1_txn, folder_level_2_txn];
   });
 
   ensureUndoState([[folder_level_2_txn, folder_level_1_txn]]);
   yield ensureItemsAdded(folder_level_1_info, folder_level_2_info);
   observer.reset();
 
   let remove_folder_2_txn = PT.Remove(folder_level_2_info);
-  yield PT.transact(remove_folder_2_txn);
+  yield remove_folder_2_txn.transact();
 
   ensureUndoState([ [remove_folder_2_txn]
                   , [folder_level_2_txn, folder_level_1_txn] ]);
   yield ensureItemsRemoved(folder_level_2_info);
 
   // Undo Remove "Folder Level 2"
   yield PT.undo();
   ensureUndoState([ [remove_folder_2_txn]
@@ -711,35 +684,35 @@ add_task(function* test_remove_folder() 
 add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
   const testURI = NetUtil.newURI("http://add.remove.tag")
       , TAG_1 = "TestTag1", TAG_2 = "TestTag2"
       , KEYWORD = "test_keyword"
       , POST_DATA = "post_data"
       , ANNO = { name: "TestAnno", value: "TestAnnoValue" };
 
   let folder_info = yield createTestFolderInfo();
-  folder_info.guid = yield PT.transact(PT.NewFolder(folder_info));
+  folder_info.guid = yield 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, uri: testURI, tags: [TAG_1] };
-  b1_info.guid = yield PT.transact(PT.NewBookmark(b1_info));
+  let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
+  b1_info.guid = yield PT.NewBookmark(b1_info).transact();
   ensureTags([TAG_1]);
   yield PT.undo();
   ensureTags([]);
 
   observer.reset();
   yield PT.redo();
   ensureTimestampsUpdated(b1_info.guid, true);
   ensureTags([TAG_1]);
 
   // Check if the Remove transaction removes and restores tags of children
   // correctly.
-  yield PT.transact(PT.Remove(folder_info.guid));
+  yield PT.Remove(folder_info.guid).transact();
   ensureTags([]);
 
   observer.reset();
   yield PT.undo();
   ensureTimestampsUpdated(b1_info.guid, true);
   ensureTags([TAG_1]);
 
   yield PT.redo();
@@ -750,21 +723,21 @@ add_task(function* test_add_and_remove_b
   ensureTimestampsUpdated(b1_info.guid, true);
   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
-                , uri:         testURI, tags: [TAG_1, TAG_2]
+                , url:         testURI, tags: [TAG_1, TAG_2]
                 , keyword:     KEYWORD
                 , postData:    POST_DATA
                 , annotations: [ANNO] };
-  b2_info.guid = yield PT.transact(PT.NewBookmark(b2_info));
+  b2_info.guid = yield 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 },
@@ -783,19 +756,19 @@ add_task(function* test_add_and_remove_b
   // Check if Remove correctly restores keywords, tags and annotations.
   observer.reset();
   yield PT.redo();
   ensureItemsChanged(...b2_post_creation_changes);
   ensureTags([TAG_1, TAG_2]);
 
   // Test Remove for multiple items.
   observer.reset();
-  yield PT.transact(PT.Remove(b1_info.guid));
-  yield PT.transact(PT.Remove(b2_info.guid));
-  yield PT.transact(PT.Remove(folder_info.guid));
+  yield PT.Remove(b1_info.guid).transact();
+  yield PT.Remove(b2_info.guid).transact();
+  yield PT.Remove(folder_info.guid).transact();
   yield ensureItemsRemoved(b1_info, b2_info, folder_info);
   ensureTags([]);
 
   observer.reset();
   yield PT.undo();
   yield ensureItemsAdded(folder_info);
   ensureTags([]);
 
@@ -830,21 +803,21 @@ add_task(function* test_add_and_remove_b
 });
 
 add_task(function* test_creating_and_removing_a_separator() {
   let folder_info = yield createTestFolderInfo();
   let separator_info = {};
   let undoEntries = [];
 
   observer.reset();
-  let create_txns = yield PT.transact(function* () {
+  let create_txns = yield PT.batch(function* () {
     let folder_txn = PT.NewFolder(folder_info);
-    folder_info.guid = separator_info.parentGuid = yield folder_txn;
+    folder_info.guid = separator_info.parentGuid = yield folder_txn.transact();
     let separator_txn = PT.NewSeparator(separator_info);
-    separator_info.guid = yield separator_txn;
+    separator_info.guid = yield separator_txn.transact();
     return [separator_txn, folder_txn];
   });
   undoEntries.unshift(create_txns);
   ensureUndoState(undoEntries, 0);
   ensureItemsAdded(folder_info, separator_info);
 
   observer.reset();
   yield PT.undo();
@@ -853,17 +826,17 @@ add_task(function* test_creating_and_rem
 
   observer.reset();
   yield PT.redo();
   ensureUndoState(undoEntries, 0);
   ensureItemsAdded(folder_info, separator_info);
 
   observer.reset();
   let remove_sep_txn = PT.Remove(separator_info);
-  yield PT.transact(remove_sep_txn);
+  yield remove_sep_txn.transact();
   undoEntries.unshift([remove_sep_txn]);
   ensureUndoState(undoEntries, 0);
   ensureItemsRemoved(separator_info);
 
   observer.reset();
   yield PT.undo();
   ensureUndoState(undoEntries, 1);
   ensureItemsAdded(separator_info);
@@ -893,26 +866,26 @@ add_task(function* test_creating_and_rem
   ensureUndoState(undoEntries, 1);
   ensureItemsRemoved(folder_info, separator_info);
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_add_and_remove_livemark() {
   let createLivemarkTxn = PT.NewLivemark(
-    { feedURI: NetUtil.newURI("http://test.remove.livemark")
+    { feedUrl: NetUtil.newURI("http://test.remove.livemark")
     , parentGuid: yield PlacesUtils.promiseItemGuid(root)
     , title: "Test Remove Livemark" });
-  let guid = yield PlacesTransactions.transact(createLivemarkTxn);
+  let guid = yield createLivemarkTxn.transact();
   let originalInfo = yield PlacesUtils.promiseBookmarksTree(guid);
   Assert.ok(originalInfo);
   yield ensureLivemarkCreatedByAddLivemark(guid);
 
   let removeTxn = PT.Remove(guid);
-  yield PT.transact(removeTxn);
+  yield removeTxn.transact();
   yield ensureNonExistent(guid);
   function* undo() {
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
     yield PT.undo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
     yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
     yield PT.undo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
@@ -936,29 +909,29 @@ add_task(function* test_add_and_remove_l
   // Cleanup
   yield undo();
   observer.reset();
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_edit_title() {
   let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
-                , uri:        NetUtil.newURI("http://test_create_item.com")
+                , url:        NetUtil.newURI("http://test_create_item.com")
                 , title:      "Original Title" };
 
   function ensureTitleChange(aCurrentTitle) {
     ensureItemsChanged({ guid: bm_info.guid
                        , property: "title"
                        , newValue: aCurrentTitle});
   }
 
-  bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
+  bm_info.guid = yield PT.NewBookmark(bm_info).transact();
 
   observer.reset();
-  yield PT.transact(PT.EditTitle({ guid: bm_info.guid, title: "New Title" }));
+  yield PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact();
   ensureTitleChange("New Title");
 
   observer.reset();
   yield PT.undo();
   ensureTitleChange("Original Title");
 
   observer.reset();
   yield PT.redo();
@@ -974,56 +947,56 @@ add_task(function* test_edit_title() {
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_edit_url() {
   let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
   let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
   let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
-                , uri:        oldURI
+                , url:        oldURI
                 , tags:       ["TestTag"]};
 
   function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) {
     ensureItemsChanged({ guid: bm_info.guid
                        , property: "uri"
                        , newValue: aPostChangeURI.spec });
     ensureTagsForURI(aPostChangeURI, bm_info.tags);
     ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []);
   }
 
-  bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
+  bm_info.guid = yield PT.NewBookmark(bm_info).transact();
   ensureTagsForURI(oldURI, bm_info.tags);
 
   // When there's a single bookmark for the same url, tags should be moved.
   observer.reset();
-  yield PT.transact(PT.EditURI({ guid: bm_info.guid, uri: newURI }));
+  yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
   ensureURIAndTags(oldURI, newURI, false);
 
   observer.reset();
   yield PT.undo();
   ensureURIAndTags(newURI, oldURI, false);
 
   observer.reset();
   yield PT.redo();
   ensureURIAndTags(oldURI, newURI, false);
 
   observer.reset();
   yield PT.undo();
   ensureURIAndTags(newURI, oldURI, false);
 
   // When there're multiple bookmarks for the same url, tags should be copied.
   let bm2_info = Object.create(bm_info);
-  bm2_info.guid = yield PT.transact(PT.NewBookmark(bm2_info));
+  bm2_info.guid = yield PT.NewBookmark(bm2_info).transact();
   let bm3_info = Object.create(bm_info);
-  bm3_info.uri = newURI;
-  bm3_info.guid = yield PT.transact(PT.NewBookmark(bm3_info));
+  bm3_info.url = newURI;
+  bm3_info.guid = yield PT.NewBookmark(bm3_info).transact();
 
   observer.reset();
-  yield PT.transact(PT.EditURI({ guid: bm_info.guid, uri: newURI }));
+  yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
   ensureURIAndTags(oldURI, newURI, true);
 
   observer.reset();
   yield PT.undo();
   ensureURIAndTags(newURI, oldURI, true);
 
   observer.reset();
   yield PT.redo();
@@ -1039,29 +1012,29 @@ add_task(function* test_edit_url() {
   ensureItemsRemoved(bm3_info, bm2_info, bm_info);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_edit_keyword() {
   let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
-                , uri:        NetUtil.newURI("http://test.edit.keyword") };
+                , url:        NetUtil.newURI("http://test.edit.keyword") };
   const KEYWORD = "test_keyword";
-  bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
+  bm_info.guid = yield PT.NewBookmark(bm_info).transact();
   function ensureKeywordChange(aCurrentKeyword = "") {
     ensureItemsChanged({ guid: bm_info.guid
                        , property: "keyword"
                        , newValue: aCurrentKeyword });
   }
 
-  bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
+  bm_info.guid = yield PT.NewBookmark(bm_info).transact();
 
   observer.reset();
-  yield PT.transact(PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD }));
+  yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD }).transact();
   ensureKeywordChange(KEYWORD);
 
   observer.reset();
   yield PT.undo();
   ensureKeywordChange();
 
   observer.reset();
   yield PT.redo();
@@ -1075,212 +1048,212 @@ add_task(function* test_edit_keyword() {
   ensureItemsRemoved(bm_info);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_tag_uri() {
   // This also tests passing uri specs.
-  let bm_info_a = { uri: "http://bookmarked.uri"
+  let bm_info_a = { url: "http://bookmarked.uri"
                   , parentGuid: yield PlacesUtils.promiseItemGuid(root) };
-  let bm_info_b = { uri: NetUtil.newURI("http://bookmarked2.uri")
+  let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
                   , parentGuid: yield PlacesUtils.promiseItemGuid(root) };
   let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri");
 
   function* promiseIsBookmarked(aURI) {
     let deferred = Promise.defer();
     PlacesUtils.asyncGetBookmarkIds(aURI, ids => {
                                             deferred.resolve(ids.length > 0);
                                           });
     return deferred.promise;
   }
 
-  yield PT.transact(function* () {
-    bm_info_a.guid = yield PT.NewBookmark(bm_info_a);
-    bm_info_b.guid = yield PT.NewBookmark(bm_info_b);
+  yield PT.batch(function* () {
+    bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
+    bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
   });
 
   function* doTest(aInfo) {
-    let uris = "uri" in aInfo ? [aInfo.uri] : aInfo.uris;
+    let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
     let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
 
-    let ensureURI = uri => typeof(uri) == "string" ? NetUtil.newURI(uri) : uri;
-    uris = [for (uri of uris) ensureURI(uri)];
+    let ensureURI = url => typeof(url) == "string" ? NetUtil.newURI(url) : url;
+    urls = [for (url of urls) ensureURI(url)];
 
     let tagWillAlsoBookmark = new Set();
-    for (let uri of uris) {
-      if (!(yield promiseIsBookmarked(uri))) {
-        tagWillAlsoBookmark.add(uri);
+    for (let url of urls) {
+      if (!(yield promiseIsBookmarked(url))) {
+        tagWillAlsoBookmark.add(url);
       }
     }
 
     function* ensureTagsSet() {
-      for (let uri of uris) {
-        ensureTagsForURI(uri, tags);
-        Assert.ok(yield promiseIsBookmarked(uri));
+      for (let url of urls) {
+        ensureTagsForURI(url, tags);
+        Assert.ok(yield promiseIsBookmarked(url));
       }
     }
     function* ensureTagsUnset() {
-      for (let uri of uris) {
-        ensureTagsForURI(uri, []);
-        if (tagWillAlsoBookmark.has(uri))
-          Assert.ok(!(yield promiseIsBookmarked(uri)));
+      for (let url of urls) {
+        ensureTagsForURI(url, []);
+        if (tagWillAlsoBookmark.has(url))
+          Assert.ok(!(yield promiseIsBookmarked(url)));
         else
-          Assert.ok(yield promiseIsBookmarked(uri));
+          Assert.ok(yield promiseIsBookmarked(url));
       }
     }
 
-    yield PT.transact(PT.Tag(aInfo));
+    yield PT.Tag(aInfo).transact();
     yield ensureTagsSet();
     yield PT.undo();
     yield ensureTagsUnset();
     yield PT.redo();
     yield ensureTagsSet();
     yield PT.undo();
     yield ensureTagsUnset();
   }
 
-  yield doTest({ uri: bm_info_a.uri, tags: ["MyTag"] });
-  yield doTest({ uris: [bm_info_a.uri], tag: "MyTag" });
-  yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tags: ["A, B"] });
-  yield doTest({ uris: [bm_info_a.uri, unbookmarked_uri], tag: "C" });
+  yield doTest({ url: bm_info_a.url, tags: ["MyTag"] });
+  yield doTest({ urls: [bm_info_a.url], tag: "MyTag" });
+  yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] });
+  yield doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" });
 
   // Cleanup
   observer.reset();
   yield PT.undo();
   ensureItemsRemoved(bm_info_a, bm_info_b);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_untag_uri() {
-  let bm_info_a = { uri: NetUtil.newURI("http://bookmarked.uri")
+  let bm_info_a = { url: NetUtil.newURI("http://bookmarked.uri")
                   , parentGuid: yield PlacesUtils.promiseItemGuid(root)
                   , tags: ["A", "B"] };
-  let bm_info_b = { uri: NetUtil.newURI("http://bookmarked2.uri")
+  let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
                   , parentGuid: yield PlacesUtils.promiseItemGuid(root)
                   , tag: "B" };
 
-  yield PT.transact(function* () {
-    bm_info_a.guid = yield PT.NewBookmark(bm_info_a);
-    ensureTagsForURI(bm_info_a.uri, bm_info_a.tags);
-    bm_info_b.guid = yield PT.NewBookmark(bm_info_b);
-    ensureTagsForURI(bm_info_b.uri, [bm_info_b.tag]);
+  yield PT.batch(function* () {
+    bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
+    ensureTagsForURI(bm_info_a.url, bm_info_a.tags);
+    bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
+    ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]);
   });
 
   function* doTest(aInfo) {
-    let uris, tagsToRemove;
+    let urls, tagsToRemove;
     if (aInfo instanceof Ci.nsIURI) {
-      uris = [aInfo];
+      urls = [aInfo];
       tagsRemoved = [];
     }
     else if (Array.isArray(aInfo)) {
-      uris = aInfo;
+      urls = aInfo;
       tagsRemoved = [];
     }
     else {
-      uris = "uri" in aInfo ? [aInfo.uri] : aInfo.uris;
+      urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
       tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
     }
 
     let preRemovalTags = new Map();
-    for (let uri of uris) {
-      preRemovalTags.set(uri, tagssvc.getTagsForURI(uri));
+    for (let url of urls) {
+      preRemovalTags.set(url, tagssvc.getTagsForURI(url));
     }
 
     function ensureTagsSet() {
-      for (let uri of uris) {
-        ensureTagsForURI(uri, preRemovalTags.get(uri));
+      for (let url of urls) {
+        ensureTagsForURI(url, preRemovalTags.get(url));
       }
     }
     function ensureTagsUnset() {
-      for (let uri of uris) {
+      for (let url of urls) {
         let expectedTags = tagsRemoved.length == 0 ?
-           [] : [for (tag of preRemovalTags.get(uri))
+           [] : [for (tag of preRemovalTags.get(url))
                  if (tagsRemoved.indexOf(tag) == -1) tag];
-        ensureTagsForURI(uri, expectedTags);
+        ensureTagsForURI(url, expectedTags);
       }
     }
 
-    yield PT.transact(PT.Untag(aInfo));
+    yield PT.Untag(aInfo).transact();
     yield ensureTagsUnset();
     yield PT.undo();
     yield ensureTagsSet();
     yield PT.redo();
     yield ensureTagsUnset();
     yield PT.undo();
     yield ensureTagsSet();
   }
 
   yield doTest(bm_info_a);
   yield doTest(bm_info_b);
-  yield doTest(bm_info_a.uri);
-  yield doTest(bm_info_b.uri);
-  yield doTest([bm_info_a.uri, bm_info_b.uri]);
-  yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tags: ["A", "B"] });
-  yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tag: "B" });
-  yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tag: "C" });
-  yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tags: ["C"] });
+  yield doTest(bm_info_a.url);
+  yield doTest(bm_info_b.url);
+  yield doTest([bm_info_a.url, bm_info_b.url]);
+  yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] });
+  yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" });
+  yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" });
+  yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] });
 
   // Cleanup
   observer.reset();
   yield PT.undo();
   ensureItemsRemoved(bm_info_a, bm_info_b);
 
   yield PT.clearTransactionsHistory();
   ensureUndoState();
 });
 
 add_task(function* test_annotate() {
-  let bm_info = { uri: NetUtil.newURI("http://test.item.annotation")
+  let bm_info = { url: NetUtil.newURI("http://test.item.annotation")
                 , parentGuid: yield PlacesUtils.promiseItemGuid(root) };
   let anno_info = { name: "TestAnno", value: "TestValue" };
   function ensureAnnoState(aSet) {
     ensureAnnotationsSet(bm_info.guid,
                          [{ name: anno_info.name
                           , value: aSet ? anno_info.value : null }]);
   }
 
-  bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
+  bm_info.guid = yield PT.NewBookmark(bm_info).transact();
 
   observer.reset();
-  yield PT.transact(PT.Annotate({ guid: bm_info.guid, annotation: anno_info }));
+  yield PT.Annotate({ guid: bm_info.guid, annotation: anno_info }).transact();
   ensureAnnoState(true);
 
   observer.reset();
   yield PT.undo();
   ensureAnnoState(false);
 
   observer.reset();
   yield PT.redo();
   ensureAnnoState(true);
 
   // Test removing the annotation by not passing the |value| property.
   observer.reset();
-  yield PT.transact(PT.Annotate({ guid: bm_info.guid,
-                                  annotation: { name: anno_info.name }}));
+  yield PT.Annotate({ guid: bm_info.guid,
+                      annotation: { name: anno_info.name }}).transact();
   ensureAnnoState(false);
 
   observer.reset();
   yield PT.undo();
   ensureAnnoState(true);
 
   observer.reset();
   yield PT.redo();
   ensureAnnoState(false);
 
   // Cleanup
   yield PT.undo();
   observer.reset();
 });
 
 add_task(function* test_annotate_multiple() {
-  let guid = yield PT.transact(PT.NewFolder(yield createTestFolderInfo()));
+  let guid = yield PT.NewFolder(yield createTestFolderInfo()).transact();
   let itemId = yield PlacesUtils.promiseItemId(guid);
 
   function AnnoObj(aName, aValue) {
     this.name = aName;
     this.value = aValue;
     this.flags = 0;
     this.expires = Ci.nsIAnnotationService.EXPIRE_NEVER;
   }
@@ -1293,29 +1266,29 @@ add_task(function* test_annotate_multipl
     if (a !== null)
       expectedAnnos.push(new AnnoObj("A", a));
     if (b !== null)
       expectedAnnos.push(new AnnoObj("B", b));
 
     Assert.deepEqual(currentAnnos, expectedAnnos);
   }
 
-  yield PT.transact(PT.Annotate({ guid: guid, annotations: annos(1, 2) }));
+  yield PT.Annotate({ guid: guid, annotations: annos(1, 2) }).transact();
   verifyAnnoValues(1, 2);
   yield PT.undo();
   verifyAnnoValues();
   yield PT.redo();
   verifyAnnoValues(1, 2);
 
-  yield PT.transact(PT.Annotate({ guid: guid
-                                , annotation: { name: "A" } }));
+  yield PT.Annotate({ guid: guid
+                    , annotation: { name: "A" } }).transact();
   verifyAnnoValues(null, 2);
 
-  yield PT.transact(PT.Annotate({ guid: guid
-                                , annotation: { name: "B", value: 0 } }));
+  yield PT.Annotate({ guid: guid
+                    , annotation: { name: "B", value: 0 } }).transact();
   verifyAnnoValues(null, 0);
   yield PT.undo();
   verifyAnnoValues(null, 2);
   yield PT.redo();
   verifyAnnoValues(null, 0);
   yield PT.undo();
   verifyAnnoValues(null, 2);
   yield PT.undo();
@@ -1326,112 +1299,113 @@ add_task(function* test_annotate_multipl
   // Cleanup
   yield PT.undo();
   observer.reset();
 });
 
 add_task(function* test_sort_folder_by_name() {
   let folder_info = yield createTestFolderInfo();
 
-  let uri = NetUtil.newURI("http://sort.by.name/");
-  let preSep =  [{ title: i, uri: uri } for (i of ["3","2","1"])];
+  let url = NetUtil.newURI("http://sort.by.name/");
+  let preSep =  [{ title: i, url } for (i of ["3","2","1"])];
   let sep = {};
-  let postSep = [{ title: l, uri: uri } for (l of ["c","b","a"])];
+  let postSep = [{ title: l, url } for (l of ["c","b","a"])];
   let originalOrder = [...preSep, sep, ...postSep];
   let sortedOrder = [...preSep.slice(0).reverse(),
                      sep,
                      ...postSep.slice(0).reverse()];
-  yield PT.transact(function* () {
-    folder_info.guid = yield PT.NewFolder(folder_info);
+  yield PT.batch(function* () {
+    folder_info.guid = yield PT.NewFolder(folder_info).transact();
     for (let info of originalOrder) {
       info.parentGuid = folder_info.guid;
       info.guid = yield info == sep ?
-                  PT.NewSeparator(info) : PT.NewBookmark(info);
+                    PT.NewSeparator(info).transact() :
+                    PT.NewBookmark(info).transact();
     }
   });
 
   let folderId = yield PlacesUtils.promiseItemId(folder_info.guid);
   let folderContainer = PlacesUtils.getFolderContents(folderId).root;
   function ensureOrder(aOrder) {
     for (let i = 0; i < folderContainer.childCount; i++) {
       do_check_eq(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid);
     }
   }
 
   ensureOrder(originalOrder);
-  yield PT.transact(PT.SortByName(folder_info.guid));
+  yield PT.SortByName(folder_info.guid).transact();
   ensureOrder(sortedOrder);
   yield PT.undo();
   ensureOrder(originalOrder);
   yield PT.redo();
   ensureOrder(sortedOrder);
 
   // Cleanup
   observer.reset();
   yield PT.undo();
   ensureOrder(originalOrder);
   yield PT.undo();
   ensureItemsRemoved(...originalOrder, folder_info);
 });
 
 add_task(function* test_livemark_txns() {
   let livemark_info =
-    { feedURI: NetUtil.newURI("http://test.feed.uri")
+    { feedUrl: NetUtil.newURI("http://test.feed.uri")
     , parentGuid: yield PlacesUtils.promiseItemGuid(root)
     , title: "Test Livemark" };
   function ensureLivemarkAdded() {
     ensureItemsAdded({ guid:       livemark_info.guid
                      , title:      livemark_info.title
                      , parentGuid: livemark_info.parentGuid
                      , itemType:   bmsvc.TYPE_FOLDER });
     let annos = [{ name:  PlacesUtils.LMANNO_FEEDURI
-                 , value: livemark_info.feedURI.spec }];
-    if ("siteURI" in livemark_info) {
+                 , value: livemark_info.feedUrl.spec }];
+    if ("siteUrl" in livemark_info) {
       annos.push({ name: PlacesUtils.LMANNO_SITEURI
-                 , value: livemark_info.siteURI.spec });
+                 , value: livemark_info.siteUrl.spec });
     }
     ensureAnnotationsSet(livemark_info.guid, annos);
   }
   function ensureLivemarkRemoved() {
     ensureItemsRemoved({ guid:       livemark_info.guid
                        , parentGuid: livemark_info.parentGuid });
   }
 
   function* _testDoUndoRedoUndo() {
     observer.reset();
-    livemark_info.guid = yield PT.transact(PT.NewLivemark(livemark_info));
+    livemark_info.guid = yield PT.NewLivemark(livemark_info).transact();
     ensureLivemarkAdded();
 
     observer.reset();
     yield PT.undo();
     ensureLivemarkRemoved();
 
     observer.reset();
     yield PT.redo();
     ensureLivemarkAdded();
 
     yield PT.undo();
     ensureLivemarkRemoved();
   }
 
   yield* _testDoUndoRedoUndo()
-  livemark_info.siteURI = NetUtil.newURI("http://feed.site.uri");
+  livemark_info.siteUrl = NetUtil.newURI("http://feed.site.uri");
   yield* _testDoUndoRedoUndo();
 
   // Cleanup
   observer.reset();
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_copy() {
   let rootGuid = yield PlacesUtils.promiseItemGuid(root);
 
   function* duplicate_and_test(aOriginalGuid) {
-    yield duplicateGuid = yield PT.transact(
-      PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }));
+    yield duplicateGuid =
+      yield PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }).transact();
     let originalInfo = yield PlacesUtils.promiseBookmarksTree(aOriginalGuid);
     let duplicateInfo = yield PlacesUtils.promiseBookmarksTree(duplicateGuid);
     yield ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false);
 
     function* redo() {
       yield PT.redo();
       yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
       yield PT.redo();
@@ -1451,61 +1425,63 @@ add_task(function* test_copy() {
 
     // Cleanup. This also remove the original item.
     yield PT.undo();
     observer.reset();
     yield PT.clearTransactionsHistory();
   }
 
   // Test duplicating leafs (bookmark, separator, empty folder)
-  let bmTxn = PT.NewBookmark({ uri: NetUtil.newURI("http://test.item.duplicate")
+  let bmTxn = PT.NewBookmark({ url: new URL("http://test.item.duplicate")
                              , parentGuid: rootGuid
                              , annos: [{ name: "Anno", value: "AnnoValue"}] });
   let sepTxn = PT.NewSeparator({ parentGuid: rootGuid, index: 1 });
   let livemarkTxn = PT.NewLivemark(
-    { feedURI: NetUtil.newURI("http://test.feed.uri")
+    { feedUrl: new URL("http://test.feed.uri")
     , parentGuid: yield PlacesUtils.promiseItemGuid(root)
     , title: "Test Livemark", index: 1 });
   let emptyFolderTxn = PT.NewFolder(yield createTestFolderInfo());
   for (let txn of [livemarkTxn, sepTxn, emptyFolderTxn]) {
-    let guid = yield PT.transact(txn);
+    let guid = yield txn.transact();
     yield duplicate_and_test(guid);
   }
 
   // Test duplicating a folder having some contents.
-  let filledFolderGuid = yield PT.transact(function *() {
-    let folderGuid = yield PT.NewFolder(yield createTestFolderInfo());
-    let nestedFolderGuid = yield PT.NewFolder({ parentGuid: folderGuid
-                                              , title: "Nested Folder" });
+  let filledFolderGuid = yield PT.batch(function *() {
+    let folderGuid =
+      yield PT.NewFolder(yield createTestFolderInfo()).transact();
+    let nestedFolderGuid =
+      yield PT.NewFolder({ parentGuid: folderGuid
+                         , title: "Nested Folder" }).transact();
     // Insert a bookmark under the nested folder.
-    yield PT.NewBookmark({ uri: NetUtil.newURI("http://nested.nested.bookmark")
-                         , parentGuid: nestedFolderGuid });
+    yield PT.NewBookmark({ url: new URL("http://nested.nested.bookmark")
+                         , parentGuid: nestedFolderGuid }).transact();
     // Insert a separator below the nested folder
-    yield PT.NewSeparator({ parentGuid: folderGuid });
+    yield PT.NewSeparator({ parentGuid: folderGuid }).transact();
     // And another bookmark.
-    yield PT.NewBookmark({ uri: NetUtil.newURI("http://nested.bookmark")
-                         , parentGuid: folderGuid });
+    yield PT.NewBookmark({ url: new URL("http://nested.bookmark")
+                         , parentGuid: folderGuid }).transact();
     return folderGuid;
   });
 
   yield duplicate_and_test(filledFolderGuid);
 
   // Cleanup
   yield PT.clearTransactionsHistory();
 });
 
-add_task(function* test_array_input_for_transact() {
+add_task(function* test_array_input_for_batch() {
   let rootGuid = yield PlacesUtils.promiseItemGuid(root);
 
   let folderTxn = PT.NewFolder(yield createTestFolderInfo());
-  let folderGuid = yield PT.transact(folderTxn);
+  let folderGuid = yield folderTxn.transact();
 
   let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid });
   let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid });
-  yield PT.transact([sep1_txn, sep2_txn]);
+  yield PT.batch([sep1_txn, sep2_txn]);
   ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0);
 
   let ensureChildCount = function* (count) {
     let tree = yield PlacesUtils.promiseBookmarksTree(folderGuid);
     if (count == 0)
       Assert.ok(!("children" in tree));
     else
       Assert.equal(tree.children.length, count);
@@ -1527,51 +1503,51 @@ add_task(function* test_array_input_for_
 });
 
 add_task(function* test_copy_excluding_annotations() {
   let rootGuid = yield PlacesUtils.promiseItemGuid(root);
 
   let folderInfo = yield createTestFolderInfo();
   let anno = n => { return { name: n, value: 1 } };
   folderInfo.annotations = [anno("a"), anno("b"), anno("c")];
-  let folderGuid = yield PT.transact(PT.NewFolder(folderInfo));
+  let folderGuid = yield PT.NewFolder(folderInfo).transact();
 
   let ensureAnnosSet = function* (guid, ...expectedAnnoNames) {
     let tree = yield PlacesUtils.promiseBookmarksTree(guid);
     let annoNames = "annos" in tree ?
                       [for (a of tree.annos) a.name].sort() : [];
     Assert.deepEqual(annoNames, expectedAnnoNames);
   };
 
   yield ensureAnnosSet(folderGuid, "a", "b", "c");
 
   let excluding_a_dupeGuid =
-    yield PT.transact(PT.Copy({ guid: folderGuid
-                              , newParentGuid: rootGuid
-                              , excludingAnnotation: "a" }));
+    yield PT.Copy({ guid: folderGuid
+                  , newParentGuid: rootGuid
+                  , excludingAnnotation: "a" }).transact();
   yield ensureAnnosSet(excluding_a_dupeGuid,  "b", "c");
 
   let excluding_ac_dupeGuid =
-    yield PT.transact(PT.Copy({ guid: folderGuid
-                              , newParentGuid: rootGuid
-                              , excludingAnnotations: ["a", "c"] }));
+    yield PT.Copy({ guid: folderGuid
+                  , newParentGuid: rootGuid
+                  , excludingAnnotations: ["a", "c"] }).transact();
   yield ensureAnnosSet(excluding_ac_dupeGuid,  "b");
 
   // Cleanup
   yield PT.undo();
   yield PT.undo();
   yield PT.undo();
   yield PT.clearTransactionsHistory();
 });
 
 add_task(function* test_invalid_uri_spec_throws() {
   let rootGuid = yield PlacesUtils.promiseItemGuid(root);
   Assert.throws(() =>
     PT.NewBookmark({ parentGuid: rootGuid
-                   , uri:        "invalid uri spec"
+                   , url:        "invalid uri spec"
                    , title:      "test bookmark" }));
   Assert.throws(() =>
     PT.Tag({ tag: "TheTag"
-           , uris: ["invalid uri spec"] }));
+           , urls: ["invalid uri spec"] }));
   Assert.throws(() =>
     PT.Tag({ tag: "TheTag"
-           , uris: ["about:blank", "invalid uri spec"] }));
+           , urls: ["about:blank", "invalid uri spec"] }));
 });
--- a/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
+++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
@@ -161,29 +161,29 @@ function* compareToNode(aItem, aNode, aI
     do_check_eq(aItem.itemsCount, nodesCount);
 
   return nodesCount;
 }
 
 let itemsCount = 0;
 function* new_bookmark(aInfo) {
   let currentItem = ++itemsCount;
-  if (!("uri" in aInfo))
-    aInfo.uri = uri("http://test.item." + itemsCount);
+  if (!("url" in aInfo))
+    aInfo.url = uri("http://test.item." + itemsCount);
 
   if (!("title" in aInfo))
     aInfo.title = "Test Item (bookmark) " + itemsCount;
 
-  yield PlacesTransactions.transact(PlacesTransactions.NewBookmark(aInfo));
+  yield PlacesTransactions.NewBookmark(aInfo).transact();
 }
 
 function* new_folder(aInfo) {
   if (!("title" in aInfo))
     aInfo.title = "Test Item (folder) " + itemsCount;
-  return yield PlacesTransactions.transact(PlacesTransactions.NewFolder(aInfo));
+  return yield PlacesTransactions.NewFolder(aInfo).transact();
 }
 
 // Walks a result nodes tree and test promiseBookmarksTree for each node.
 // DO NOT COPY THIS LOGIC:  It is done here to accomplish a more comprehensive
 // test of the API (the entire hierarchy data is available in the very test).
 function* test_promiseBookmarksTreeForEachNode(aNode, aOptions, aExcludedGuids) {
   do_check_true(aNode.bookmarkGuid && aNode.bookmarkGuid.length > 0);
   let item = yield PlacesUtils.promiseBookmarksTree(aNode.bookmarkGuid, aOptions);
@@ -209,26 +209,26 @@ function* test_promiseBookmarksTreeAgain
 }
 
 add_task(function* () {
   // Add some bookmarks to cover various use cases.
   yield new_bookmark({ parentGuid: PlacesUtils.bookmarks.toolbarGuid });
   yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid
                    , annotations: [{ name: "TestAnnoA", value: "TestVal"
                                    , name: "TestAnnoB", value: 0 }]});
-  yield PlacesTransactions.transact(
-    PlacesTransactions.NewSeparator({ parentGuid: PlacesUtils.bookmarks.menuGuid }));
+  let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid };
+  yield PlacesTransactions.NewSeparator(sepInfo).transact();
   let folderGuid = yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid });
   yield new_bookmark({ title: null
                      , parentGuid: folderGuid
                      , keyword: "test_keyword"
                      , tags: ["TestTagA", "TestTagB"]
                      , annotations: [{ name: "TestAnnoA", value: "TestVal2"}]});
   let urlWithCharsetAndFavicon = uri("http://charset.and.favicon");
-  yield new_bookmark({ parentGuid: folderGuid, uri: urlWithCharsetAndFavicon });
+  yield new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon });
   yield PlacesUtils.setCharsetForURI(urlWithCharsetAndFavicon, "UTF-8");
   yield promiseSetIconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI);
   // Test the default places root without specifying it.
   yield test_promiseBookmarksTreeAgainstResult();
 
   // Do specify it
   yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid);