Bug 1426245 - Replace OnItemAdded with bookmark-item-added r=mak
authorDoug Thayer <dothayer@mozilla.com>
Tue, 09 Oct 2018 14:47:27 +0000
changeset 496013 50ca67245a715b0e37014f2066102ad9feec9f1f
parent 496012 01f7327625291933b8886befa8fb561ced401b3d
child 496014 85a806b69f15dcf8d4ebf1a1a847be5641323013
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1426245
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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 Depends on D4605 Differential Revision: https://phabricator.services.mozilla.com/D4606
browser/base/content/browser-places.js
browser/components/extensions/parent/ext-bookmarks.js
browser/components/newtab/lib/PlacesFeed.jsm
browser/components/places/content/editBookmark.js
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
tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1426,16 +1426,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() {
@@ -1471,16 +1472,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;
        });
@@ -1705,30 +1708,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) {
+    // Only need to update the UI if it wasn't marked as starred before:
+    if (this._itemGuids.size == 0) {
+      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);
+            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/newtab/lib/PlacesFeed.jsm
+++ b/browser/components/newtab/lib/PlacesFeed.jsm
@@ -75,56 +75,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
@@ -153,32 +113,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
@@ -209,16 +209,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/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -1022,17 +1022,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",
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
@@ -200,16 +200,18 @@ EXPORTS.mozilla.dom += [
     'MimeType.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',
     'ProcessMessageManager.h',
     'ResponsiveImageSelector.h',
     'SameProcessMessageQueue.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,88 @@ 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.
+   * NOTE: this will go away with bug 424160.
+   */
+  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
@@ -1284,23 +1284,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");
@@ -1358,25 +1361,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
@@ -274,34 +274,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 ]);
         }
       }
@@ -549,55 +561,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
@@ -2671,36 +2671,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
@@ -4614,24 +4614,29 @@ class BookmarkObserverRecorder {
       SELECT EXISTS(SELECT 1 FROM itemsAdded WHERE keywordChanged) OR
              EXISTS(SELECT 1 FROM itemsChanged WHERE keywordChanged)
              AS keywordsChanged`);
     this.shouldInvalidateKeywords =
       !!keywordsChangedRows[0].getResultByName("keywordsChanged");
   }
 
   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,
@@ -4698,22 +4703,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");
     }
   }
 
   notifyObserver(observer, notification, args = []) {
     try {
       observer[notification](...args);
     } catch (ex) {
--- 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.
@@ -220,17 +221,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
@@ -564,21 +565,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);
@@ -776,21 +794,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
@@ -2691,23 +2691,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 &&
@@ -3404,23 +3403,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();
@@ -3854,31 +3852,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;
 }
 
 
@@ -3966,40 +3962,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");
@@ -4023,16 +4021,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);
   }
@@ -4063,16 +4064,19 @@ nsNavHistoryResult::AddBookmarkFolderObs
 {
   if (!mIsBookmarkFolderObserver && !mIsAllBookmarksObserver) {
     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);
     mIsBookmarkFolderObserver = 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.
   FolderObserverList* list = BookmarkFolderObserversForId(aFolder, true);
   if (list->IndexOf(aNode) == FolderObserverList::NoIndex) {
     list->AppendElement(aNode);
@@ -4327,47 +4331,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)
@@ -4588,33 +4561,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.
@@ -288,16 +291,17 @@ TaggingService.prototype = {
 
     return this.__tagFolders;
   },
 
   // 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
@@ -334,27 +338,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.
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js
@@ -48,16 +48,18 @@ module.exports = {
     // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/InternalError
     "InternalError": true,
     "KeyEvent": false,
     "MatchGlob": false,
     "MatchPattern": false,
     "MatchPatternSet": false,
     "MenuBoxObject": false,
     // Specific to Firefox (Chrome code only).
+    "PlacesBookmarkAddition": false,
+    "PlacesEvent": false,
     "PlacesObservers": false,
     "PlacesWeakCallbackWrapper": false,
     "PrioEncoder": false,
     // Specific to Firefox (Chrome code only).
     "SharedArrayBuffer": false,
     "SimpleGestureEvent": false,
     // Note: StopIteration will likely be removed as part of removing legacy
     // generators, see bug 968038.