Bug 1426245 - Replace OnItemAdded with bookmark-item-added r?mak draft
authorDoug Thayer <dothayer@mozilla.com>
Fri, 06 Jul 2018 09:29:01 -0700
changeset 819553 77a6b95bdb8d7f2976966e79c01986b62b16de10
parent 819446 cf1c15b4456d6420ea8f7f63a6eb828441b05119
child 819554 4f39261a050dcdc9c2541722e6526007d9594cc2
push id116579
push userbmo:dothayer@mozilla.com
push dateWed, 18 Jul 2018 04:18:30 +0000
reviewersmak
bugs1426245
milestone63.0a1
Bug 1426245 - Replace OnItemAdded with bookmark-item-added r?mak See https://docs.google.com/document/d/1G45vfd6RXFXwNz7i4FV40lDCU0ao-JX_bZdgJV4tLjk/edit# for further info. This essentially follows the same philosophy as the onVisits migration. MozReview-Commit-ID: I4bOvFH0ZQR
browser/base/content/browser-places.js
browser/components/extensions/parent/ext-bookmarks.js
browser/components/places/content/editBookmark.js
browser/extensions/activity-stream/lib/PlacesFeed.jsm
browser/modules/SavantShieldStudy.jsm
dom/base/PlacesBookmark.h
dom/base/PlacesBookmarkAddition.h
dom/base/PlacesEvent.h
dom/base/moz.build
dom/chrome-webidl/PlacesEvent.webidl
services/sync/modules/engines/bookmarks.js
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/SyncedBookmarksMirror.jsm
toolkit/components/places/nsINavBookmarksService.idl
toolkit/components/places/nsLivemarkService.js
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavHistoryResult.cpp
toolkit/components/places/nsNavHistoryResult.h
toolkit/components/places/nsTaggingService.js
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1443,16 +1443,17 @@ var BookmarkingUI = {
     CustomizableUI.removeListener(this);
 
     this.star.removeEventListener("mouseover", this);
 
     this._uninitView();
 
     if (this._hasBookmarksObserver) {
       PlacesUtils.bookmarks.removeObserver(this);
+      PlacesUtils.observers.removeListener(["bookmark-added"], this.handlePlacesEvents);
     }
 
     if (this._pendingUpdate) {
       delete this._pendingUpdate;
     }
   },
 
   onLocationChange: function BUI_onLocationChange() {
@@ -1488,16 +1489,18 @@ var BookmarkingUI = {
          }
 
          this._updateStar();
 
          // Start observing bookmarks if needed.
          if (!this._hasBookmarksObserver) {
            try {
              PlacesUtils.bookmarks.addObserver(this);
+            this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
+            PlacesUtils.observers.addListener(["bookmark-added"], this.handlePlacesEvents);
              this._hasBookmarksObserver = true;
            } catch (ex) {
              Cu.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
            }
          }
 
          delete this._pendingUpdate;
        });
@@ -1683,30 +1686,32 @@ var BookmarkingUI = {
     } else {
       // Move it back to the palette.
       CustomizableUI.removeWidgetFromArea(this.BOOKMARK_BUTTON_ID);
     }
     triggerNode.setAttribute("checked", !placement);
     updateToggleControlLabel(triggerNode);
   },
 
-  // nsINavBookmarkObserver
-  onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGuid) {
-    if (aURI && aURI.equals(this._uri)) {
-      // If a new bookmark has been added to the tracked uri, register it.
-      if (!this._itemGuids.has(aGuid)) {
-        this._itemGuids.add(aGuid);
-        // Only need to update the UI if it wasn't marked as starred before:
-        if (this._itemGuids.size == 1) {
-          this._updateStar();
+  handlePlacesEvents(aEvents) {
+    for (let {url, guid} of aEvents) {
+      if (url && url == this._uri.spec) {
+        // If a new bookmark has been added to the tracked uri, register it.
+        if (!this._itemGuids.has(guid)) {
+          this._itemGuids.add(guid);
+          // Only need to update the UI if it wasn't marked as starred before:
+          if (this._itemGuids.size == 1) {
+            this._updateStar();
+          }
         }
       }
     }
   },
 
+  // nsINavBookmarkObserver
   onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGuid) {
     // If one of the tracked bookmarks has been removed, unregister it.
     if (this._itemGuids.has(aGuid)) {
       this._itemGuids.delete(aGuid);
       // Only need to update the UI if the page is no longer starred
       if (this._itemGuids.size == 0) {
         this._updateStar();
       }
--- a/browser/components/extensions/parent/ext-bookmarks.js
+++ b/browser/components/extensions/parent/ext-bookmarks.js
@@ -102,37 +102,44 @@ const convertBookmarks = result => {
 };
 
 let observer = new class extends EventEmitter {
   constructor() {
     super();
 
     this.skipTags = true;
     this.skipDescendantsOnItemRemoval = true;
+
+    this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
   }
 
   onBeginUpdateBatch() {}
   onEndUpdateBatch() {}
 
-  onItemAdded(id, parentId, index, itemType, uri, title, dateAdded, guid, parentGuid, source) {
-    let bookmark = {
-      id: guid,
-      parentId: parentGuid,
-      index,
-      title,
-      dateAdded: dateAdded / 1000,
-      type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(itemType),
-      url: getUrl(itemType, uri && uri.spec),
-    };
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      if (event.isTagging) {
+        continue;
+      }
+      let bookmark = {
+        id: event.guid,
+        parentId: event.parentGuid,
+        index: event.index,
+        title: event.title,
+        dateAdded: event.dateAdded,
+        type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType),
+        url: getUrl(event.itemType, event.url),
+      };
 
-    if (itemType == TYPE_FOLDER) {
-      bookmark.dateGroupModified = bookmark.dateAdded;
+      if (event.itemType == TYPE_FOLDER) {
+        bookmark.dateGroupModified = bookmark.dateAdded;
+      }
+
+      this.emit("created", bookmark);
     }
-
-    this.emit("created", bookmark);
   }
 
   onItemVisited() {}
 
   onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, itemType, guid, oldParentGuid, newParentGuid, source) {
     let info = {
       parentId: newParentGuid,
       index: newIndex,
@@ -168,23 +175,25 @@ let observer = new class extends EventEm
     this.emit("changed", {guid, info});
   }
 }();
 
 const decrementListeners = () => {
   listenerCount -= 1;
   if (!listenerCount) {
     PlacesUtils.bookmarks.removeObserver(observer);
+    PlacesUtils.observers.removeListener(["bookmark-added"], observer.handlePlacesEvents);
   }
 };
 
 const incrementListeners = () => {
   listenerCount++;
   if (listenerCount == 1) {
     PlacesUtils.bookmarks.addObserver(observer);
+    PlacesUtils.observers.addListener(["bookmark-added"], observer.handlePlacesEvents);
   }
 };
 
 this.bookmarks = class extends ExtensionAPI {
   getAPI(context) {
     return {
       bookmarks: {
         async get(idOrIdList) {
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -999,17 +999,16 @@ var gEditItemOverlay = {
     // Just setting selectItem _does not_ trigger oncommand, so we don't
     // recurse.
     PlacesUtils.bookmarks.fetch(newParentGuid).then(bm => {
       this._folderMenuList.selectedItem = this._getFolderMenuItem(newParentGuid,
                                                                   bm.title);
     });
   },
 
-  onItemAdded() {},
   onItemRemoved() { },
   onBeginUpdateBatch() { },
   onEndUpdateBatch() { },
   onItemVisited() { },
 };
 
 for (let elt of ["folderMenuList", "folderTree", "namePicker",
                  "locationField", "keywordField",
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -76,56 +76,16 @@ class HistoryObserver extends Observer {
  */
 class BookmarksObserver extends Observer {
   constructor(dispatch) {
     super(dispatch, Ci.nsINavBookmarkObserver);
     this.skipTags = true;
   }
 
   /**
-   * onItemAdded - Called when a bookmark is added
-   *
-   * @param  {str} id
-   * @param  {str} folderId
-   * @param  {int} index
-   * @param  {int} type       Indicates if the bookmark is an actual bookmark,
-   *                          a folder, or a separator.
-   * @param  {str} uri
-   * @param  {str} title
-   * @param  {int} dateAdded
-   * @param  {str} guid      The unique id of the bookmark
-   * @param  {str} parent guid
-   * @param  {int} source    Used to distinguish bookmarks made by different
-   *                         actions: sync, bookmarks import, other.
-   */
-  onItemAdded(id, folderId, index, type, uri, bookmarkTitle, dateAdded, bookmarkGuid, parentGuid, source) { // eslint-disable-line max-params
-    // Skips items that are not bookmarks (like folders), about:* pages or
-    // default bookmarks, added when the profile is created.
-    if (type !== PlacesUtils.bookmarks.TYPE_BOOKMARK ||
-        source === PlacesUtils.bookmarks.SOURCES.IMPORT ||
-        source === PlacesUtils.bookmarks.SOURCES.RESTORE ||
-        source === PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP ||
-        source === PlacesUtils.bookmarks.SOURCES.SYNC ||
-        (uri.scheme !== "http" && uri.scheme !== "https")) {
-      return;
-    }
-
-    this.dispatch({type: at.PLACES_LINKS_CHANGED});
-    this.dispatch({
-      type: at.PLACES_BOOKMARK_ADDED,
-      data: {
-        bookmarkGuid,
-        bookmarkTitle,
-        dateAdded,
-        url: uri.spec
-      }
-    });
-  }
-
-  /**
    * onItemRemoved - Called when a bookmark is removed
    *
    * @param  {str} id
    * @param  {str} folderId
    * @param  {int} index
    * @param  {int} type       Indicates if the bookmark is an actual bookmark,
    *                          a folder, or a separator.
    * @param  {str} uri
@@ -154,32 +114,72 @@ class BookmarksObserver extends Observer
 
   onItemMoved() {}
 
   // Disabled due to performance cost, see Issue 3203 /
   // https://bugzilla.mozilla.org/show_bug.cgi?id=1392267.
   onItemChanged() {}
 }
 
+/**
+ * PlacesObserver - observes events from PlacesUtils.observers
+ */
+class PlacesObserver extends Observer {
+  constructor(dispatch) {
+    super(dispatch, Ci.nsINavBookmarkObserver);
+    this.handlePlacesEvent = this.handlePlacesEvent.bind(this);
+  }
+
+  handlePlacesEvent(events) {
+    for (let {itemType, source, dateAdded, guid, title, url, isTagging} of events) {
+      // Skips items that are not bookmarks (like folders), about:* pages or
+      // default bookmarks, added when the profile is created.
+      if (isTagging ||
+          itemType !== PlacesUtils.bookmarks.TYPE_BOOKMARK ||
+          source === PlacesUtils.bookmarks.SOURCES.IMPORT ||
+          source === PlacesUtils.bookmarks.SOURCES.RESTORE ||
+          source === PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP ||
+          source === PlacesUtils.bookmarks.SOURCES.SYNC ||
+          (!url.startsWith("http://") && !url.startsWith("https://"))) {
+        return;
+      }
+
+      this.dispatch({type: at.PLACES_LINKS_CHANGED});
+      this.dispatch({
+        type: at.PLACES_BOOKMARK_ADDED,
+        data: {
+          bookmarkGuid: guid,
+          bookmarkTitle: title,
+          dateAdded: dateAdded * 1000,
+          url
+        }
+      });
+    }
+  }
+}
+
 class PlacesFeed {
   constructor() {
     this.placesChangedTimer = null;
     this.customDispatch = this.customDispatch.bind(this);
     this.historyObserver = new HistoryObserver(this.customDispatch);
     this.bookmarksObserver = new BookmarksObserver(this.customDispatch);
+    this.placesObserver = new PlacesObserver(this.customDispatch);
   }
 
   addObservers() {
     // NB: Directly get services without importing the *BIG* PlacesUtils module
     Cc["@mozilla.org/browser/nav-history-service;1"]
       .getService(Ci.nsINavHistoryService)
       .addObserver(this.historyObserver, true);
     Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
       .getService(Ci.nsINavBookmarksService)
       .addObserver(this.bookmarksObserver, true);
+    PlacesUtils.observers.addListener(["bookmark-added"],
+                                      this.placesObserver.handlePlacesEvent);
 
     Services.obs.addObserver(this, LINK_BLOCKED_EVENT);
   }
 
   /**
    * setTimeout - A custom function that creates an nsITimer that can be cancelled
    *
    * @param {func} callback       A function to be executed after the timer expires
@@ -210,16 +210,18 @@ class PlacesFeed {
 
   removeObservers() {
     if (this.placesChangedTimer) {
       this.placesChangedTimer.cancel();
       this.placesChangedTimer = null;
     }
     PlacesUtils.history.removeObserver(this.historyObserver);
     PlacesUtils.bookmarks.removeObserver(this.bookmarksObserver);
+    PlacesUtils.observers.removeListener(["bookmark-added"],
+                                         this.placesObserver.handlePlacesEvent);
     Services.obs.removeObserver(this, LINK_BLOCKED_EVENT);
   }
 
   /**
    * observe - An observer for the LINK_BLOCKED_EVENT.
    *           Called when a link is blocked.
    *
    * @param  {null} subject
--- a/browser/modules/SavantShieldStudy.jsm
+++ b/browser/modules/SavantShieldStudy.jsm
@@ -271,28 +271,37 @@ class BookmarkObserver {
     // there are two probes: bookmark and follow_bookmark
     this.METHOD_1 = "bookmark";
     this.EXTRA_SUBCATEGORY_1 = "feature";
     this.METHOD_2 = "follow_bookmark";
     this.EXTRA_SUBCATEGORY_2 = "navigation";
     this.TYPE_BOOKMARK = Ci.nsINavBookmarksService.TYPE_BOOKMARK;
     // Ignore "fake" bookmarks created for bookmark tags
     this.skipTags = true;
+    this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
   }
 
   init() {
     this.addObservers();
   }
 
   addObservers() {
     PlacesUtils.bookmarks.addObserver(this);
+    PlacesUtils.observers.addListener(["bookmark-added"], this.handlePlacesEvents);
   }
 
-  onItemAdded(itemID, parentID, index, itemType, uri, title, dateAdded, guid, parentGUID, source) {
-    this.handleItemAddRemove(itemType, uri, source, "save");
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      if (event.itemType == this.TYPE_BOOKMARK) {
+        this.handleItemAddRemove(this.TYPE_BOOKMARK,
+                                 Services.io.newURI(event.url),
+                                 event.source,
+                                 "save");
+      }
+    }
   }
 
   onItemRemoved(itemID, parentID, index, itemType, uri, guid, parentGUID, source) {
     this.handleItemAddRemove(itemType, uri, source, "remove");
   }
 
   handleItemAddRemove(itemType, uri, source, event) {
     /*
@@ -318,16 +327,17 @@ class BookmarkObserver {
     Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY, method, event, null,
                                   {
                                     subcategory
                                   });
   }
 
   removeObservers() {
     PlacesUtils.bookmarks.removeObserver(this);
+    PlacesUtils.observers.removeListener(["bookmark-added"], this.handlePlacesEvents);
   }
 
   uninit() {
     if (SavantShieldStudy.shouldRemoveListeners) {
       this.removeObservers();
     }
   }
 }
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesBookmark.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesBookmark_h
+#define mozilla_dom_PlacesBookmark_h
+
+#include "mozilla/dom/PlacesEvent.h"
+
+namespace mozilla {
+namespace dom {
+
+class PlacesBookmark : public PlacesEvent
+{
+public:
+  explicit PlacesBookmark(PlacesEventType aEventType) : PlacesEvent(aEventType) {}
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return PlacesBookmark_Binding::Wrap(aCx, this, aGivenProto);
+  }
+
+  const PlacesBookmark* AsPlacesBookmark() const override { return this; }
+
+  unsigned short ItemType() { return mItemType; }
+  int64_t Id() { return mId; }
+  int64_t ParentId() { return mParentId; }
+  void GetUrl(nsString& aUrl) { aUrl = mUrl; }
+  void GetGuid(nsCString& aGuid) { aGuid = mGuid; }
+  void GetParentGuid(nsCString& aParentGuid) { aParentGuid = mParentGuid; }
+  uint16_t Source() { return mSource; }
+  bool IsTagging() { return mIsTagging; }
+
+  unsigned short mItemType;
+  int64_t mId;
+  int64_t mParentId;
+  nsString mUrl;
+  nsCString mGuid;
+  nsCString mParentGuid;
+  uint16_t mSource;
+  bool mIsTagging;
+
+protected:
+  virtual ~PlacesBookmark() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesBookmark_h
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesBookmarkAddition.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesBookmarkAddition_h
+#define mozilla_dom_PlacesBookmarkAddition_h
+
+#include "mozilla/dom/PlacesBookmark.h"
+
+namespace mozilla {
+namespace dom {
+
+class PlacesBookmarkAddition final : public PlacesBookmark
+{
+public:
+  explicit PlacesBookmarkAddition() : PlacesBookmark(PlacesEventType::Bookmark_added) {}
+
+  static already_AddRefed<PlacesBookmarkAddition>
+  Constructor(const GlobalObject& aGlobal,
+              const PlacesBookmarkAdditionInit& aInitDict,
+              ErrorResult& aRv) {
+    RefPtr<PlacesBookmarkAddition> event = new PlacesBookmarkAddition();
+    event->mItemType = aInitDict.mItemType;
+    event->mId = aInitDict.mId;
+    event->mParentId = aInitDict.mParentId;
+    event->mIndex = aInitDict.mIndex;
+    event->mUrl = aInitDict.mUrl;
+    event->mTitle = aInitDict.mTitle;
+    event->mDateAdded = aInitDict.mDateAdded;
+    event->mGuid = aInitDict.mGuid;
+    event->mParentGuid = aInitDict.mParentGuid;
+    event->mSource = aInitDict.mSource;
+    event->mIsTagging = aInitDict.mIsTagging;
+    return event.forget();
+  }
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return PlacesBookmarkAddition_Binding::Wrap(aCx, this, aGivenProto);
+  }
+
+  const PlacesBookmarkAddition* AsPlacesBookmarkAddition() const override { return this; }
+
+  int32_t Index() { return mIndex; }
+  void GetTitle(nsString& aTitle) { aTitle = mTitle; }
+  uint64_t DateAdded() { return mDateAdded; }
+
+  int32_t mIndex;
+  nsString mTitle;
+  uint64_t mDateAdded;
+
+private:
+  ~PlacesBookmarkAddition() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesBookmarkAddition_h
--- a/dom/base/PlacesEvent.h
+++ b/dom/base/PlacesEvent.h
@@ -32,16 +32,18 @@ public:
   nsISupports* GetParentObject() const;
 
   JSObject*
   WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   PlacesEventType Type() const { return mType; }
 
   virtual const PlacesVisit* AsPlacesVisit() const { return nullptr; }
+  virtual const PlacesBookmark* AsPlacesBookmark() const { return nullptr; }
+  virtual const PlacesBookmarkAddition* AsPlacesBookmarkAddition() const { return nullptr; }
 protected:
   virtual ~PlacesEvent() = default;
   PlacesEventType mType;
 };
 
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -193,16 +193,18 @@ EXPORTS.mozilla.dom += [
     'MessageSender.h',
     'MozQueryInterface.h',
     'NameSpaceConstants.h',
     'Navigator.h',
     'NodeInfo.h',
     'NodeInfoInlines.h',
     'NodeIterator.h',
     'ParentProcessMessageManager.h',
+    'PlacesBookmark.h',
+    'PlacesBookmarkAddition.h',
     'PlacesEvent.h',
     'PlacesObservers.h',
     'PlacesVisit.h',
     'PlacesWeakCallbackWrapper.h',
     'Pose.h',
     'ProcessGlobal.h',
     'ProcessMessageManager.h',
     'ResponsiveImageSelector.h',
--- a/dom/chrome-webidl/PlacesEvent.webidl
+++ b/dom/chrome-webidl/PlacesEvent.webidl
@@ -1,15 +1,20 @@
 enum PlacesEventType {
   "none",
 
   /**
    * data: PlacesVisit. Fired whenever a page is visited.
    */
   "page-visited",
+  /**
+   * data: PlacesBookmarkAddition. Fired whenever a bookmark
+   * (or a bookmark folder/separator) is created.
+   */
+  "bookmark-added",
 };
 
 [ChromeOnly, Exposed=(Window,System)]
 interface PlacesEvent {
   readonly attribute PlacesEventType type;
 };
 
 [ChromeOnly, Exposed=(Window,System)]
@@ -62,8 +67,87 @@ interface PlacesVisit : PlacesEvent {
   readonly attribute unsigned long typedCount;
 
   /**
    * The last known title of the page. Might not be from the current visit,
    * and might be null if it is not known.
    */
   readonly attribute DOMString? lastKnownTitle;
 };
+
+/**
+ * Base class for properties that are common to all bookmark events.
+ */
+[ChromeOnly, Exposed=(Window,System)]
+interface PlacesBookmark : PlacesEvent {
+  /**
+   * The id of the item.
+   */
+  readonly attribute long long id;
+
+  /**
+   * The id of the folder to which the item belongs.
+   */
+  readonly attribute long long parentId;
+
+  /**
+   * The type of the added item (see TYPE_* constants in nsINavBooksService.idl).
+   */
+  readonly attribute unsigned short itemType;
+
+  /**
+   * The URI of the added item if it was TYPE_BOOKMARK, "" otherwise.
+   */
+  readonly attribute DOMString url;
+
+  /**
+   * The unique ID associated with the item.
+   */
+  readonly attribute ByteString guid;
+
+  /**
+   * The unique ID associated with the item's parent.
+   */
+  readonly attribute ByteString parentGuid;
+
+  /**
+   * A change source constant from nsINavBookmarksService::SOURCE_*,
+   * passed to the method that notifies the observer.
+   */
+  readonly attribute unsigned short source;
+
+  /**
+   * True if the item is a tag or a tag folder
+   */
+  readonly attribute boolean isTagging;
+};
+
+dictionary PlacesBookmarkAdditionInit {
+  required long long id;
+  required long long parentId;
+  required unsigned short itemType;
+  required DOMString url;
+  required ByteString guid;
+  required ByteString parentGuid;
+  required unsigned short source;
+  required long index;
+  required DOMString title;
+  required unsigned long long dateAdded;
+  required boolean isTagging;
+};
+
+[ChromeOnly, Exposed=(Window,System), Constructor(PlacesBookmarkAdditionInit initDict)]
+interface PlacesBookmarkAddition : PlacesBookmark {
+  /**
+   * The item's index in the folder.
+   */
+  readonly attribute long index;
+
+  /**
+   * The title of the added item.
+   */
+  readonly attribute DOMString title;
+
+  /**
+   * The time that the item was added, in milliseconds from the epoch.
+   */
+  readonly attribute unsigned long long dateAdded;
+};
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -1277,23 +1277,26 @@ BookmarksTracker.prototype = {
   set ignoreAll(value) {},
 
   // We never want to persist changed IDs, as the changes are already stored
   // in Places.
   persistChangedIDs: false,
 
   onStart() {
     PlacesUtils.bookmarks.addObserver(this, true);
+    this._placesListener = new PlacesWeakCallbackWrapper(this.handlePlacesEvents.bind(this));
+    PlacesUtils.observers.addListener(["bookmark-added"], this._placesListener);
     Svc.Obs.add("bookmarks-restore-begin", this);
     Svc.Obs.add("bookmarks-restore-success", this);
     Svc.Obs.add("bookmarks-restore-failed", this);
   },
 
   onStop() {
     PlacesUtils.bookmarks.removeObserver(this);
+    PlacesUtils.observers.removeListener(["bookmark-added"], this._placesListener);
     Svc.Obs.remove("bookmarks-restore-begin", this);
     Svc.Obs.remove("bookmarks-restore-success", this);
     Svc.Obs.remove("bookmarks-restore-failed", this);
   },
 
   // Ensure we aren't accidentally using the base persistence.
   addChangedID(id, when) {
     throw new Error("Don't add IDs to the bookmarks tracker");
@@ -1351,25 +1354,25 @@ BookmarksTracker.prototype = {
   _upScore: function BMT__upScore() {
     if (this._batchDepth == 0) {
       this.score += SCORE_INCREMENT_XLARGE;
     } else {
       this._batchSawScoreIncrement = true;
     }
   },
 
-  onItemAdded: function BMT_onItemAdded(itemId, folder, index,
-                                        itemType, uri, title, dateAdded,
-                                        guid, parentGuid, source) {
-    if (IGNORED_SOURCES.includes(source)) {
-      return;
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      if (IGNORED_SOURCES.includes(event.source)) {
+        continue;
+      }
+
+      this._log.trace("'bookmark-added': " + event.id);
+      this._upScore();
     }
-
-    this._log.trace("onItemAdded: " + itemId);
-    this._upScore();
   },
 
   onItemRemoved(itemId, parentId, index, type, uri,
                            guid, parentGuid, source) {
     if (IGNORED_SOURCES.includes(source)) {
       return;
     }
 
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -268,34 +268,46 @@ var Bookmarks = Object.freeze({
       // Set index in the appending case.
       if (insertInfo.index == this.DEFAULT_INDEX ||
           insertInfo.index > parent._childCount) {
         insertInfo.index = parent._childCount;
       }
 
       let item = await insertBookmark(insertInfo, parent);
 
-      // Notify onItemAdded to listeners.
-      let observers = PlacesUtils.bookmarks.getObservers();
       // We need the itemId to notify, though once the switch to guids is
       // complete we may stop using it.
-      let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
       let itemId = await PlacesUtils.promiseItemId(item.guid);
 
       // Pass tagging information for the observers to skip over these notifications when needed.
       let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
       let isTagsFolder = parent._id == PlacesUtils.tagsFolderId;
-      notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
-                                         item.type, uri, item.title,
-                                         PlacesUtils.toPRTime(item.dateAdded), item.guid,
-                                         item.parentGuid, item.source ],
-                                       { isTagging: isTagging || isTagsFolder });
+      let url = "";
+      if (item.type == Bookmarks.TYPE_BOOKMARK) {
+        url = item.url.href;
+      }
+
+      let notification = new PlacesBookmarkAddition({
+        id: itemId,
+        url,
+        itemType: item.type,
+        parentId: parent._id,
+        index: item.index,
+        title: item.title,
+        dateAdded: item.dateAdded,
+        guid: item.guid,
+        parentGuid: item.parentGuid,
+        source: item.source,
+        isTagging: isTagging || isTagsFolder,
+      });
+      PlacesObservers.notifyListeners([notification]);
 
       // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
       if (isTagging) {
+        let observers = PlacesUtils.bookmarks.getObservers();
         for (let entry of (await fetchBookmarksByURL(item, true))) {
           notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
                                                PlacesUtils.toPRTime(entry.lastModified),
                                                entry.type, entry._parentId,
                                                entry.guid, entry.parentGuid,
                                                "", item.source ]);
         }
       }
@@ -543,55 +555,71 @@ var Bookmarks = Object.freeze({
       for (let insertInfo of insertInfos) {
         if (insertInfo.parentGuid == tree.guid) {
           insertInfo.index += rootIndex++;
         }
       }
       // We need the itemIds to notify, though once the switch to guids is
       // complete we may stop using them.
       let itemIdMap = await PlacesUtils.promiseManyItemIds(insertInfos.map(info => info.guid));
-      // Notify onItemAdded to listeners.
-      let observers = PlacesUtils.bookmarks.getObservers();
+
+      let notifications = [];
       for (let i = 0; i < insertInfos.length; i++) {
         let item = insertInfos[i];
         let itemId = itemIdMap.get(item.guid);
-        let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
         // For sub-folders, we need to make sure their children have the correct parent ids.
         let parentId;
         if (item.parentGuid === treeParent.guid) {
           // This is a direct child of the tree parent, so we can use the
           // existing parent's id.
           parentId = treeParent._id;
         } else {
           // This is a parent folder that's been updated, so we need to
           // use the new item id.
           parentId = itemIdMap.get(item.parentGuid);
         }
 
-        notify(observers, "onItemAdded", [ itemId, parentId, item.index,
-                                           item.type, uri, item.title,
-                                           PlacesUtils.toPRTime(item.dateAdded), item.guid,
-                                           item.parentGuid, item.source ],
-                                         { isTagging: false });
+        let url = "";
+        if (item.type == Bookmarks.TYPE_BOOKMARK) {
+          url = (item.url instanceof URL) ? item.url.href : item.url;
+        }
+
+        notifications.push(new PlacesBookmarkAddition({
+          id: itemId,
+          url,
+          itemType: item.type,
+          parentId,
+          index: item.index,
+          title: item.title,
+          dateAdded: item.dateAdded,
+          guid: item.guid,
+          parentGuid: item.parentGuid,
+          source: item.source,
+          isTagging: false,
+        }));
+
         // Note, annotations for livemark data are deleted from insertInfo
         // within appendInsertionInfoForInfoArray, so we won't be duplicating
         // the insertions here.
         try {
           await handleBookmarkItemSpecialData(itemId, item);
         } catch (ex) {
           // This is not critical, regardless the bookmark has been created
           // and we should continue notifying the next ones.
           Cu.reportError(`An error occured while handling special bookmark data: ${ex}`);
         }
 
         // Remove non-enumerable properties.
         delete item.source;
 
         insertInfos[i] = Object.assign({}, item);
       }
+
+      PlacesObservers.notifyListeners(notifications);
+
       return insertInfos;
     })();
   },
 
   /**
    * Updates a bookmark-item.
    *
    * Only set the properties which should be changed (undefined properties
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -2704,36 +2704,39 @@ var GuidHelper = {
     if (!("observer" in this)) {
       /**
        * This observers serves two purposes:
        * (1) Invalidate cached id<->GUID paris on when items are removed.
        * (2) Cache GUIDs given us free of charge by onItemAdded/onItemRemoved.
       *      So, for exmaple, when the NewBookmark needs the new GUID, we already
       *      have it cached.
       */
+      let listener = events => {
+        for (let event of events) {
+          this.updateCache(event.id, event.guid);
+          this.updateCache(event.parentId, event.parentGuid);
+        }
+      };
       this.observer = {
-        onItemAdded: (aItemId, aParentId, aIndex, aItemType, aURI, aTitle,
-                      aDateAdded, aGuid, aParentGuid) => {
-          this.updateCache(aItemId, aGuid);
-          this.updateCache(aParentId, aParentGuid);
-        },
         onItemRemoved:
         (aItemId, aParentId, aIndex, aItemTyep, aURI, aGuid, aParentGuid) => {
           this.guidsForIds.delete(aItemId);
           this.idsForGuids.delete(aGuid);
           this.updateCache(aParentId, aParentGuid);
         },
 
         QueryInterface: ChromeUtils.generateQI([Ci.nsINavBookmarkObserver]),
 
         onBeginUpdateBatch() {},
         onEndUpdateBatch() {},
         onItemChanged() {},
         onItemVisited() {},
         onItemMoved() {},
       };
       PlacesUtils.bookmarks.addObserver(this.observer);
+      PlacesUtils.observers.addListener(["bookmark-added"], listener);
       PlacesUtils.registerShutdownFunction(() => {
         PlacesUtils.bookmarks.removeObserver(this.observer);
+        PlacesUtils.observers.removeListener(["bookmark-added"], listener);
       });
     }
   }
 };
--- a/toolkit/components/places/SyncedBookmarksMirror.jsm
+++ b/toolkit/components/places/SyncedBookmarksMirror.jsm
@@ -4558,24 +4558,29 @@ class BookmarkObserverRecorder {
         WHERE frecency < 0
         ORDER BY frecency ASC
         LIMIT :limit
       )`,
       { limit: this.maxFrecenciesToRecalculate });
   }
 
   noteItemAdded(info) {
-    let uri = info.urlHref ? Services.io.newURI(info.urlHref) : null;
-    this.bookmarkObserverNotifications.push({
-      name: "onItemAdded",
+    this.bookmarkObserverNotifications.push(new PlacesBookmarkAddition({
+      id: info.id,
+      parentId: info.parentId,
+      index: info.position,
+      url: info.urlHref || "",
+      title: info.title,
+      dateAdded: info.dateAdded,
+      guid: info.guid,
+      parentGuid: info.parentGuid,
+      source: PlacesUtils.bookmarks.SOURCES.SYNC,
+      itemType: info.type,
       isTagging: info.isTagging,
-      args: [info.id, info.parentId, info.position, info.type, uri, info.title,
-        info.dateAdded, info.guid, info.parentGuid,
-        PlacesUtils.bookmarks.SOURCES.SYNC],
-    });
+    }));
   }
 
   noteGuidChanged(info) {
     PlacesUtils.invalidateCachedGuidFor(info.id);
     this.bookmarkObserverNotifications.push({
       name: "onItemChanged",
       isTagging: false,
       args: [info.id, "guid", /* isAnnotationProperty */ false, info.newGuid,
@@ -4654,22 +4659,30 @@ class BookmarkObserverRecorder {
     });
   }
 
   async notifyBookmarkObservers() {
     MirrorLog.trace("Notifying bookmark observers");
     let observers = PlacesUtils.bookmarks.getObservers();
     for (let observer of observers) {
       this.notifyObserver(observer, "onBeginUpdateBatch");
-      for await (let info of yieldingIterator(this.bookmarkObserverNotifications)) {
-        if (info.isTagging && observer.skipTags) {
-          continue;
+    }
+    for await (let info of yieldingIterator(this.bookmarkObserverNotifications)) {
+      if (info instanceof PlacesEvent) {
+        PlacesObservers.notifyListeners([info]);
+      } else {
+        for (let observer of observers) {
+          if (info.isTagging && observer.skipTags) {
+            continue;
+          }
+          this.notifyObserver(observer, info.name, info.args);
         }
-        this.notifyObserver(observer, info.name, info.args);
       }
+    }
+    for (let observer of observers) {
       this.notifyObserver(observer, "onEndUpdateBatch");
     }
   }
 
   async notifyAnnoObservers() {
     MirrorLog.trace("Notifying anno observers");
     let observers = PlacesUtils.annotations.getObservers();
     for (let observer of observers) {
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -38,56 +38,16 @@ interface nsINavBookmarkObserver : nsISu
   void onBeginUpdateBatch();
 
   /**
    * Notifies that a batch transaction has ended.
    */
   void onEndUpdateBatch();
 
   /**
-   * Notifies that an item (any type) was added.  Called after the actual
-   * addition took place.
-   * When a new item is created, all the items following it in the same folder
-   * will have their index shifted down, but no additional notifications will
-   * be sent.
-   *
-   * @param aItemId
-   *        The id of the item that was added.
-   * @param aParentId
-   *        The id of the folder to which the item was added.
-   * @param aIndex
-   *        The item's index in the folder.
-   * @param aItemType
-   *        The type of the added item (see TYPE_* constants below).
-   * @param aURI
-   *        The URI of the added item if it was TYPE_BOOKMARK, null otherwise.
-   * @param aTitle
-   *        The title of the added item.
-   * @param aDateAdded
-   *        The stored date added value, in microseconds from the epoch.
-   * @param aGuid
-   *        The unique ID associated with the item.
-   * @param aParentGuid
-   *        The unique ID associated with the item's parent.
-   * @param aSource
-   *        A change source constant from nsINavBookmarksService::SOURCE_*,
-   *        passed to the method that notifies the observer.
-   */
-  void onItemAdded(in long long aItemId,
-                   in long long aParentId,
-                   in long aIndex,
-                   in unsigned short aItemType,
-                   in nsIURI aURI,
-                   in AUTF8String aTitle,
-                   in PRTime aDateAdded,
-                   in ACString aGuid,
-                   in ACString aParentGuid,
-                   in unsigned short aSource);
-
-  /**
    * Notifies that an item was removed.  Called after the actual remove took
    * place.
    * When an item is removed, all the items following it in the same folder
    * will have their index shifted down, but no additional notifications will
    * be sent.
    *
    * @param aItemId
    *        The id of the item that was removed.
--- a/toolkit/components/places/nsLivemarkService.js
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -356,17 +356,16 @@ LivemarkService.prototype = {
     });
   },
 
   // nsINavBookmarkObserver
 
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
   onItemVisited() {},
-  onItemAdded() {},
 
   onItemChanged(id, property, isAnno, value, lastModified, itemType, parentId,
                 guid, parentGuid) {
     if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
       return;
 
     this._withLivemarksMap(livemarksMap => {
       if (livemarksMap.has(guid)) {
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -12,16 +12,17 @@
 
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsNetUtil.h"
 #include "nsUnicharUtils.h"
 #include "nsPrintfCString.h"
 #include "nsQueryObject.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/storage.h"
+#include "mozilla/dom/PlacesBookmarkAddition.h"
 #include "mozilla/dom/PlacesObservers.h"
 #include "mozilla/dom/PlacesVisit.h"
 
 #include "GeckoProfiler.h"
 
 using namespace mozilla;
 
 // These columns sit to the right of the kGetInfoIndex_* columns.
@@ -210,17 +211,17 @@ nsNavBookmarks::Init()
 
   // Allows us to notify on title changes. MUST BE LAST so it is impossible
   // to fail after this call, or the history service will have a reference to
   // us and we won't go away.
   nsNavHistory* history = nsNavHistory::GetHistoryService();
   NS_ENSURE_STATE(history);
   history->AddObserver(this, true);
   AutoTArray<PlacesEventType, 1> events;
-  events.AppendElement(PlacesEventType::Page_visited);
+  events.AppendElement(PlacesEventType::Page_visited, fallible);
   PlacesObservers::AddListener(events, this);
 
   // DO NOT PUT STUFF HERE that can fail. See observer comment above.
 
   return NS_OK;
 }
 
 nsresult
@@ -566,21 +567,38 @@ nsNavBookmarks::InsertBookmark(int64_t a
   if (grandParentId != tagsRootId) {
     rv = history->UpdateFrecency(placeId);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mObservers,
-                             SKIP_TAGS(grandParentId == mDB->GetTagsFolderId()),
-                             OnItemAdded(*aNewBookmarkId, aFolder, index,
-                                         TYPE_BOOKMARK, aURI, title, dateAdded,
-                                         guid, folderGuid, aSource));
+  if (mCanNotify) {
+    Sequence<OwningNonNull<PlacesEvent>> events;
+    nsAutoCString utf8spec;
+    aURI->GetSpec(utf8spec);
+
+    RefPtr<PlacesBookmarkAddition> bookmark = new PlacesBookmarkAddition();
+    bookmark->mItemType = TYPE_BOOKMARK;
+    bookmark->mId = *aNewBookmarkId;
+    bookmark->mParentId = aFolder;
+    bookmark->mIndex = index;
+    bookmark->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
+    bookmark->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
+    bookmark->mDateAdded = dateAdded / 1000;
+    bookmark->mGuid.Assign(guid);
+    bookmark->mParentGuid.Assign(folderGuid);
+    bookmark->mSource = aSource;
+    bookmark->mIsTagging = grandParentId == mDB->GetTagsFolderId();
+    bool success = !!events.AppendElement(bookmark.forget(), fallible);
+    MOZ_RELEASE_ASSERT(success);
+
+    PlacesObservers::NotifyListeners(events);
+  }
 
   // If the bookmark has been added to a tag container, notify all
   // bookmark-folder result nodes which contain a bookmark for the new
   // bookmark's url.
   if (grandParentId == tagsRootId) {
     // Notify a tags change to all bookmarks for this URI.
     nsTArray<BookmarkData> bookmarks;
     rv = GetBookmarksForURI(aURI, bookmarks);
@@ -780,21 +798,34 @@ nsNavBookmarks::CreateFolder(int64_t aPa
                           nullptr, aSource, aNewFolderId, guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   int64_t tagsRootId = TagsRootId();
 
-  NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mObservers,
-                             SKIP_TAGS(aParent == tagsRootId),
-                             OnItemAdded(*aNewFolderId, aParent, index, FOLDER,
-                                         nullptr, title, dateAdded, guid,
-                                         folderGuid, aSource));
+  if (mCanNotify) {
+    Sequence<OwningNonNull<PlacesEvent>> events;
+    RefPtr<PlacesBookmarkAddition> folder = new PlacesBookmarkAddition();
+    folder->mItemType = TYPE_FOLDER;
+    folder->mId = *aNewFolderId;
+    folder->mParentId = aParent;
+    folder->mIndex = index;
+    folder->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
+    folder->mDateAdded = dateAdded / 1000;
+    folder->mGuid.Assign(guid);
+    folder->mParentGuid.Assign(folderGuid);
+    folder->mSource = aSource;
+    folder->mIsTagging = aParent == tagsRootId;
+    bool success = !!events.AppendElement(folder.forget(), fallible);
+    MOZ_RELEASE_ASSERT(success);
+
+    PlacesObservers::NotifyListeners(events);
+  }
 
   return NS_OK;
 }
 
 bool nsNavBookmarks::IsLivemark(int64_t aFolderId)
 {
   nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
   NS_ENSURE_TRUE(annosvc, false);
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -2699,23 +2699,22 @@ nsNavHistoryQueryResultNode::NotifyIfTag
   return NS_OK;
 }
 
 /**
  * These are the bookmark observer functions for query nodes.  They listen
  * for bookmark events and refresh the results if we have any dependence on
  * the bookmark system.
  */
-NS_IMETHODIMP
+nsresult
 nsNavHistoryQueryResultNode::OnItemAdded(int64_t aItemId,
                                          int64_t aParentId,
                                          int32_t aIndex,
                                          uint16_t aItemType,
                                          nsIURI* aURI,
-                                         const nsACString& aTitle,
                                          PRTime aDateAdded,
                                          const nsACString& aGUID,
                                          const nsACString& aParentGUID,
                                          uint16_t aSource)
 {
   if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
       mLiveUpdate != QUERYUPDATE_SIMPLE &&
       mLiveUpdate != QUERYUPDATE_TIME &&
@@ -3412,23 +3411,22 @@ nsNavHistoryFolderResultNode::OnBeginUpd
 
 NS_IMETHODIMP
 nsNavHistoryFolderResultNode::OnEndUpdateBatch()
 {
   return NS_OK;
 }
 
 
-NS_IMETHODIMP
+nsresult
 nsNavHistoryFolderResultNode::OnItemAdded(int64_t aItemId,
                                           int64_t aParentFolder,
                                           int32_t aIndex,
                                           uint16_t aItemType,
                                           nsIURI* aURI,
-                                          const nsACString& aTitle,
                                           PRTime aDateAdded,
                                           const nsACString& aGUID,
                                           const nsACString& aParentGUID,
                                           uint16_t aSource)
 {
   MOZ_ASSERT(aParentFolder == mTargetFolderItemId, "Got wrong bookmark update");
 
   RESTART_AND_RETURN_IF_ASYNC_PENDING();
@@ -3862,31 +3860,29 @@ nsNavHistoryFolderResultNode::OnItemMove
     node->mBookmarkIndex = aNewIndex;
 
     // adjust position
     EnsureItemPosition(index);
     return NS_OK;
   } else {
     // moving between two different folders, just do a remove and an add
     nsCOMPtr<nsIURI> itemURI;
-    nsAutoCString itemTitle;
     if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) {
       nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
       NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
       nsresult rv = bookmarks->GetBookmarkURI(aItemId, getter_AddRefs(itemURI));
       NS_ENSURE_SUCCESS(rv, rv);
-      rv = bookmarks->GetItemTitle(aItemId, itemTitle);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     if (aOldParent == mTargetFolderItemId) {
       OnItemRemoved(aItemId, aOldParent, aOldIndex, aItemType, itemURI,
                     aGUID, aOldParentGUID, aSource);
     }
     if (aNewParent == mTargetFolderItemId) {
-      OnItemAdded(aItemId, aNewParent, aNewIndex, aItemType, itemURI, itemTitle,
+      OnItemAdded(aItemId, aNewParent, aNewIndex, aItemType, itemURI,
                   RoundedPRNow(), // This is a dummy dateAdded, not the real value.
                   aGUID, aNewParentGUID, aSource);
     }
   }
   return NS_OK;
 }
 
 
@@ -3974,40 +3970,42 @@ nsNavHistoryResult::~nsNavHistoryResult(
     delete it.Data();
     it.Remove();
   }
 }
 
 void
 nsNavHistoryResult::StopObserving()
 {
+  AutoTArray<PlacesEventType, 2> events;
   if (mIsBookmarkFolderObserver || mIsAllBookmarksObserver) {
     nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
     if (bookmarks) {
       bookmarks->RemoveObserver(this);
       mIsBookmarkFolderObserver = false;
       mIsAllBookmarksObserver = false;
     }
+    events.AppendElement(PlacesEventType::Bookmark_added);
   }
   if (mIsMobilePrefObserver) {
     Preferences::UnregisterCallback(OnMobilePrefChangedCallback,
                                     MOBILE_BOOKMARKS_PREF,
                                     this);
     mIsMobilePrefObserver = false;
   }
   if (mIsHistoryObserver) {
     nsNavHistory* history = nsNavHistory::GetHistoryService();
     if (history) {
       history->RemoveObserver(this);
-      AutoTArray<PlacesEventType, 1> events;
       events.AppendElement(PlacesEventType::Page_visited);
-      PlacesObservers::RemoveListener(events, this);
       mIsHistoryObserver = false;
     }
   }
+
+  PlacesObservers::RemoveListener(events, this);
 }
 
 void
 nsNavHistoryResult::AddHistoryObserver(nsNavHistoryQueryResultNode* aNode)
 {
   if (!mIsHistoryObserver) {
       nsNavHistory* history = nsNavHistory::GetHistoryService();
       NS_ASSERTION(history, "Can't create history service");
@@ -4031,16 +4029,19 @@ nsNavHistoryResult::AddAllBookmarksObser
 {
   if (!mIsAllBookmarksObserver && !mIsBookmarkFolderObserver) {
     nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
     if (!bookmarks) {
       MOZ_ASSERT_UNREACHABLE("Can't create bookmark service");
       return;
     }
     bookmarks->AddObserver(this, true);
+    AutoTArray<PlacesEventType, 1> events;
+    events.AppendElement(PlacesEventType::Bookmark_added);
+    PlacesObservers::AddListener(events, this);
     mIsAllBookmarksObserver = true;
   }
   // Don't add duplicate observers.  In some case we don't unregister when
   // children are cleared (see ClearChildren) and the next FillChildren call
   // will try to add the observer again.
   if (mAllBookmarksObservers.IndexOf(aNode) == QueryObserverList::NoIndex) {
     mAllBookmarksObservers.AppendElement(aNode);
   }
@@ -4335,47 +4336,16 @@ nsNavHistoryResult::OnEndUpdateBatch()
     NOTIFY_RESULT_OBSERVERS(this, Batching(false));
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
-nsNavHistoryResult::OnItemAdded(int64_t aItemId,
-                                int64_t aParentId,
-                                int32_t aIndex,
-                                uint16_t aItemType,
-                                nsIURI* aURI,
-                                const nsACString& aTitle,
-                                PRTime aDateAdded,
-                                const nsACString& aGUID,
-                                const nsACString& aParentGUID,
-                                uint16_t aSource)
-{
-  NS_ENSURE_ARG(aItemType != nsINavBookmarksService::TYPE_BOOKMARK ||
-                aURI);
-
-  ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(aParentId,
-    OnItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
-                aGUID, aParentGUID, aSource)
-  );
-  ENUMERATE_HISTORY_OBSERVERS(
-    OnItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
-                aGUID, aParentGUID, aSource)
-  );
-  ENUMERATE_ALL_BOOKMARKS_OBSERVERS(
-    OnItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
-                aGUID, aParentGUID, aSource)
-  );
-  return NS_OK;
-}
-
-
-NS_IMETHODIMP
 nsNavHistoryResult::OnItemRemoved(int64_t aItemId,
                                   int64_t aParentId,
                                   int32_t aIndex,
                                   uint16_t aItemType,
                                   nsIURI* aURI,
                                   const nsACString& aGUID,
                                   const nsACString& aParentGUID,
                                   uint16_t aSource)
@@ -4596,33 +4566,68 @@ nsNavHistoryResult::OnVisit(nsIURI* aURI
 
   return NS_OK;
 }
 
 
 void
 nsNavHistoryResult::HandlePlacesEvent(const PlacesEventSequence& aEvents) {
   for (const auto& event : aEvents) {
-    if (NS_WARN_IF(event->Type() != PlacesEventType::Page_visited)) {
-      continue;
-    }
-
-    const dom::PlacesVisit* visit = event->AsPlacesVisit();
-    if (NS_WARN_IF(!visit)) {
-      continue;
+    switch (event->Type()) {
+      case PlacesEventType::Page_visited: {
+        const dom::PlacesVisit* visit = event->AsPlacesVisit();
+        if (NS_WARN_IF(!visit)) {
+          continue;
+        }
+
+        nsCOMPtr<nsIURI> uri;
+        MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), visit->mUrl));
+        if (!uri) {
+          continue;
+        }
+        OnVisit(uri, visit->mVisitId, visit->mVisitTime * 1000,
+                visit->mTransitionType, visit->mPageGuid,
+                visit->mHidden, visit->mVisitCount, visit->mLastKnownTitle);
+        break;
+      }
+      case PlacesEventType::Bookmark_added: {
+        const dom::PlacesBookmarkAddition* item = event->AsPlacesBookmarkAddition();
+        if (NS_WARN_IF(!item)) {
+          continue;
+        }
+
+        nsCOMPtr<nsIURI> uri;
+        if (item->mItemType == nsINavBookmarksService::TYPE_BOOKMARK) {
+          MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), item->mUrl));
+          if (!uri) {
+            continue;
+          }
+        }
+
+        ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(item->mParentId,
+          OnItemAdded(item->mId, item->mParentId, item->mIndex,
+                      item->mItemType, uri, item->mDateAdded * 1000,
+                      item->mGuid, item->mParentGuid, item->mSource)
+        );
+        ENUMERATE_HISTORY_OBSERVERS(
+          OnItemAdded(item->mId, item->mParentId,item->mIndex,
+                      item->mItemType, uri, item->mDateAdded * 1000,
+                      item->mGuid, item->mParentGuid, item->mSource)
+        );
+        ENUMERATE_ALL_BOOKMARKS_OBSERVERS(
+          OnItemAdded(item->mId, item->mParentId,item->mIndex,
+                      item->mItemType, uri, item->mDateAdded * 1000,
+                      item->mGuid, item->mParentGuid, item->mSource)
+        );
+        break;
+      }
+      default: {
+        MOZ_ASSERT_UNREACHABLE("Receive notification of a type not subscribed to.");
+      }
     }
-
-    nsCOMPtr<nsIURI> uri;
-    MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), visit->mUrl));
-    if (!uri) {
-      return;
-    }
-    OnVisit(uri, visit->mVisitId, visit->mVisitTime * 1000,
-            visit->mTransitionType, visit->mPageGuid,
-            visit->mHidden, visit->mVisitCount, visit->mLastKnownTitle);
   }
 }
 
 
 NS_IMETHODIMP
 nsNavHistoryResult::OnTitleChanged(nsIURI* aURI,
                                    const nsAString& aPageTitle,
                                    const nsACString& aGUID)
--- a/toolkit/components/places/nsNavHistoryResult.h
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -623,16 +623,25 @@ public:
 
   bool CanExpand();
   bool IsContainersQuery();
 
   virtual nsresult OpenContainer() override;
 
   NS_DECL_BOOKMARK_HISTORY_OBSERVER_INTERNAL
 
+  nsresult OnItemAdded(int64_t aItemId,
+                       int64_t aParentId,
+                       int32_t aIndex,
+                       uint16_t aItemType,
+                       nsIURI* aURI,
+                       PRTime aDateAdded,
+                       const nsACString& aGUID,
+                       const nsACString& aParentGUID,
+                       uint16_t aSource);
   // The internal version has an output aAdded parameter, it is incremented by
   // query nodes when the visited uri belongs to them. If no such query exists,
   // the history result creates a new query node dynamically.
   nsresult OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
                    uint32_t aTransitionType, bool aHidden,
                    uint32_t* aAdded);
   virtual void OnRemoving() override;
 
@@ -701,16 +710,26 @@ public:
 
   virtual nsresult OpenContainerAsync() override;
   NS_DECL_ASYNCSTATEMENTCALLBACK
 
   // This object implements a bookmark observer interface. This is called from the
   // result's actual observer and it knows all observers are FolderResultNodes
   NS_DECL_NSINAVBOOKMARKOBSERVER
 
+  nsresult OnItemAdded(int64_t aItemId,
+                       int64_t aParentId,
+                       int32_t aIndex,
+                       uint16_t aItemType,
+                       nsIURI* aURI,
+                       PRTime aDateAdded,
+                       const nsACString& aGUID,
+                       const nsACString& aParentGUID,
+                       uint16_t aSource);
+
   virtual void OnRemoving() override;
 
   // this indicates whether the folder contents are valid, they don't go away
   // after the container is closed until a notification comes in
   bool mContentsValid;
 
   // If the node is generated from a place:folder=X query, this is the target
   // folder id and GUID.  For regular folder nodes, they are set to the same
--- a/toolkit/components/places/nsTaggingService.js
+++ b/toolkit/components/places/nsTaggingService.js
@@ -8,18 +8,21 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
 
 const TOPIC_SHUTDOWN = "places-shutdown";
 
 /**
  * The Places Tagging Service
  */
 function TaggingService() {
+  this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
+
   // Observe bookmarks changes.
   PlacesUtils.bookmarks.addObserver(this);
+  PlacesUtils.observers.addListener(["bookmark-added"], this.handlePlacesEvents);
 
   // Cleanup on shutdown.
   Services.obs.addObserver(this, TOPIC_SHUTDOWN);
 }
 
 TaggingService.prototype = {
   /**
    * Creates a tag container under the tags-root with the given name.
@@ -341,16 +344,17 @@ TaggingService.prototype = {
   get hasTags() {
     return this._tagFolders.length > 0;
   },
 
   // nsIObserver
   observe: function TS_observe(aSubject, aTopic, aData) {
     if (aTopic == TOPIC_SHUTDOWN) {
       PlacesUtils.bookmarks.removeObserver(this);
+      PlacesUtils.observers.removeListener(["bookmark-added"], this.handlePlacesEvents);
       Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
     }
   },
 
   /**
    * If the only bookmark items associated with aURI are contained in tag
    * folders, returns the IDs of those items.  This can be the case if
    * the URI was bookmarked and tagged at some point, but the bookmark was
@@ -387,27 +391,28 @@ TaggingService.prototype = {
       }
     } finally {
       stmt.finalize();
     }
 
     return isBookmarked ? [] : itemIds;
   },
 
-  // nsINavBookmarkObserver
-  onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
-                                       aURI, aTitle) {
-    // Nothing to do if this is not a tag.
-    if (aFolderId != PlacesUtils.tagsFolderId ||
-        aItemType != PlacesUtils.bookmarks.TYPE_FOLDER)
-      return;
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      if (!event.isTagging ||
+          event.itemType != PlacesUtils.bookmarks.TYPE_FOLDER) {
+        continue;
+      }
 
-    this._tagFolders[aItemId] = aTitle;
+      this._tagFolders[event.id] = event.title;
+    }
   },
 
+  // nsINavBookmarkObserver
   onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex,
                                            aItemType, aURI, aGuid, aParentGuid,
                                            aSource) {
     // Item is a tag folder.
     if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) {
       delete this._tagFolders[aItemId];
     } else if (aURI && !this._tagFolders[aFolderId]) {
       // Item is a bookmark that was removed from a non-tag folder.