Bug 1305563 - Add `mozIAsyncLivemarks.invalidateCachedLivemarks`. r?mak draft
authorKit Cambridge <kit@yakshaving.ninja>
Fri, 08 Sep 2017 15:27:32 -0700
changeset 680373 70b3df565bd478ac0a6d7ac4ae9437e059daf0c0
parent 680372 f4cc89ccd2b28b42962647ff8cb8ee015c1fc2c0
child 680374 a6b1ac0d624b2cfa6de06f84d3fbbc6037f7c0d8
push id84483
push userbmo:kit@mozilla.com
push dateFri, 13 Oct 2017 22:55:10 +0000
reviewersmak
bugs1305563
milestone58.0a1
Bug 1305563 - Add `mozIAsyncLivemarks.invalidateCachedLivemarks`. r?mak The Sync bookmark buffer updates the database in a single transaction, so we can't use `mozIAsyncLivemarks.{add, remove}Livemark` because they start their own transactions. To simplify merging, the buffer sets the livemark feed and site URL annos in the transaction, then calls `invalidateCachedLivemarks` once merging finishes to clear the livemarks cache. The next `{add, remove, get}Livemark` call will repopulate the cache, with the new livemarks. This patch uses a promise queue to serialize reads and writes to the livemarks cache. MozReview-Commit-ID: 8R7N6ORxrtV
toolkit/components/places/mozIAsyncLivemarks.idl
toolkit/components/places/nsLivemarkService.js
toolkit/components/places/tests/unit/test_async_transactions.js
toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
--- a/toolkit/components/places/mozIAsyncLivemarks.idl
+++ b/toolkit/components/places/mozIAsyncLivemarks.idl
@@ -57,16 +57,18 @@ interface mozIAsyncLivemarks : nsISuppor
    *
    * @param [optional]aForceUpdate
    *        If set to true forces a reload even if contents are still valid.
    *
    * @note The update process is asynchronous, observers registered through
    *       registerForUpdates will be notified of updated contents.
    */
   void reloadLivemarks([optional]in boolean aForceUpdate);
+
+  jsval invalidateCachedLivemarks();
 };
 
 [scriptable, uuid(3a3c5e8f-ec4a-4086-ae0a-d16420d30c9f)]
 interface mozILivemarkInfo : nsISupports
 {
   /**
    * Id of the bookmarks folder representing this livemark.
    *
--- a/toolkit/components/places/nsLivemarkService.js
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -46,44 +46,16 @@ XPCOMUtils.defineLazyGetter(this, "CACHE
           FROM moz_bookmarks b
           JOIN moz_bookmarks p ON b.parent = p.id
           JOIN moz_items_annos a ON a.item_id = b.id
           JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
           WHERE b.type = :folder_type
             AND n.name = :feedURI_anno`;
 });
 
-XPCOMUtils.defineLazyGetter(this, "gLivemarksCachePromised",
-  async function() {
-    let livemarksMap = new Map();
-    let conn = await PlacesUtils.promiseDBConnection();
-    let rows = await conn.executeCached(CACHE_SQL,
-      { folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
-        feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
-        siteURI_anno: PlacesUtils.LMANNO_SITEURI });
-    for (let row of rows) {
-      let siteURI = row.getResultByName("siteURI");
-      let livemark = new Livemark({
-        id: row.getResultByName("id"),
-        guid: row.getResultByName("guid"),
-        title: row.getResultByName("title"),
-        parentId: row.getResultByName("parentId"),
-        parentGuid: row.getResultByName("parentGuid"),
-        index: row.getResultByName("index"),
-        dateAdded: row.getResultByName("dateAdded"),
-        lastModified: row.getResultByName("lastModified"),
-        feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
-        siteURI: siteURI ? NetUtil.newURI(siteURI) : null
-      });
-      livemarksMap.set(livemark.guid, livemark);
-    }
-    return livemarksMap;
-  }
-);
-
 /**
  * Convert a Date object to a PRTime (microseconds).
  *
  * @param date
  *        the Date object to convert.
  * @return microseconds from the epoch.
  */
 function toPRTime(date) {
@@ -104,21 +76,53 @@ function toDate(time) {
 // LivemarkService
 
 function LivemarkService() {
   // Cleanup on shutdown.
   Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
 
   // Observe bookmarks but don't init the service just for that.
   PlacesUtils.bookmarks.addObserver(this, true);
+
+  this._livemarksMap = null;
+  this._promiseLivemarksMapReady = Promise.resolve();
 }
 
 LivemarkService.prototype = {
-  // This is just an helper for code readability.
-  _promiseLivemarksMap: () => gLivemarksCachePromised,
+  _withLivemarksMap(func) {
+    let promise = this._promiseLivemarksMapReady.then(async () => {
+      if (!this._livemarksMap) {
+        this._livemarksMap = new Map();
+        let conn = await PlacesUtils.promiseDBConnection();
+        let rows = await conn.executeCached(CACHE_SQL,
+          { folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
+            feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
+            siteURI_anno: PlacesUtils.LMANNO_SITEURI });
+        for (let row of rows) {
+          let siteURI = row.getResultByName("siteURI");
+          let livemark = new Livemark({
+            id: row.getResultByName("id"),
+            guid: row.getResultByName("guid"),
+            title: row.getResultByName("title"),
+            parentId: row.getResultByName("parentId"),
+            parentGuid: row.getResultByName("parentGuid"),
+            index: row.getResultByName("index"),
+            dateAdded: row.getResultByName("dateAdded"),
+            lastModified: row.getResultByName("lastModified"),
+            feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
+            siteURI: siteURI ? NetUtil.newURI(siteURI) : null
+          });
+          this._livemarksMap.set(livemark.guid, livemark);
+        }
+      }
+      return func(this._livemarksMap);
+    });
+    this._promiseLivemarksMapReady = promise.then(() => {}, Cu.reportError);
+    return promise;
+  },
 
   _reloading: false,
   _startReloadTimer(livemarksMap, forceUpdate, reloaded) {
     if (this._reloadTimer) {
       this._reloadTimer.cancel();
     } else {
       this._reloadTimer = Cc["@mozilla.org/timer;1"]
                             .createInstance(Ci.nsITimer);
@@ -139,28 +143,17 @@ LivemarkService.prototype = {
       this._reloading = false;
     }, RELOAD_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
   },
 
   // nsIObserver
 
   observe(aSubject, aTopic, aData) {
     if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) {
-      if (this._reloadTimer) {
-        this._reloading = false;
-        this._reloadTimer.cancel();
-        delete this._reloadTimer;
-      }
-
-      // Stop any ongoing network fetch.
-      this._promiseLivemarksMap().then(livemarksMap => {
-        for (let livemark of livemarksMap.values()) {
-          livemark.terminate();
-        }
-      });
+      this.invalidateCachedLivemarks().catch(Cu.reportError);
     }
   },
 
   // mozIAsyncLivemarks
 
   addLivemark(aLivemarkInfo) {
     if (!aLivemarkInfo) {
       throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
@@ -174,22 +167,20 @@ LivemarkService.prototype = {
         (hasParentGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.parentGuid)) ||
         (hasIndex && aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX) ||
         !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) ||
         (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) ||
         (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) {
       throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
     }
 
-    return (async () => {
+    return this._withLivemarksMap(async livemarksMap => {
       if (!aLivemarkInfo.parentGuid)
         aLivemarkInfo.parentGuid = await PlacesUtils.promiseItemGuid(aLivemarkInfo.parentId);
 
-      let livemarksMap = await this._promiseLivemarksMap();
-
       // Disallow adding a livemark inside another livemark.
       if (livemarksMap.has(aLivemarkInfo.parentGuid)) {
         throw new Components.Exception("Cannot create a livemark inside a livemark", Cr.NS_ERROR_INVALID_ARG);
       }
 
       // Create a new livemark.
       let folder = await PlacesUtils.bookmarks.insert({
         type: PlacesUtils.bookmarks.TYPE_FOLDER,
@@ -227,56 +218,55 @@ LivemarkService.prototype = {
                                              lastModified: toDate(aLivemarkInfo.lastModified),
                                              source: aLivemarkInfo.source });
         livemark.lastModified = aLivemarkInfo.lastModified;
       }
 
       livemarksMap.set(folder.guid, livemark);
 
       return livemark;
-    })();
+    });
   },
 
   removeLivemark(aLivemarkInfo) {
     if (!aLivemarkInfo) {
       throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
     }
     // Accept either a guid or an id.
     let hasGuid = "guid" in aLivemarkInfo;
     let hasId = "id" in aLivemarkInfo;
     if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
         (hasId && aLivemarkInfo.id < 1) ||
         (!hasId && !hasGuid)) {
       throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
     }
 
-    return (async () => {
+    return this._withLivemarksMap(async livemarksMap => {
       if (!aLivemarkInfo.guid)
         aLivemarkInfo.guid = await PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
 
-      let livemarksMap = await this._promiseLivemarksMap();
       if (!livemarksMap.has(aLivemarkInfo.guid))
         throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
 
       await PlacesUtils.bookmarks.remove(aLivemarkInfo.guid,
                                          { source: aLivemarkInfo.source });
-    })();
+    });
   },
 
   reloadLivemarks(aForceUpdate) {
     // Check if there's a currently running reload, to save some useless work.
     let notWorthRestarting =
       this._forceUpdate || // We're already forceUpdating.
       !aForceUpdate;       // The caller didn't request a forced update.
     if (this._reloading && notWorthRestarting) {
       // Ignore this call.
       return;
     }
 
-    this._promiseLivemarksMap().then(livemarksMap => {
+    this._withLivemarksMap(livemarksMap => {
       this._forceUpdate = !!aForceUpdate;
       // Livemarks reloads happen on a timer for performance reasons.
       this._startReloadTimer(livemarksMap, this._forceUpdate, new Set());
     });
   },
 
   getLivemark(aLivemarkInfo) {
     if (!aLivemarkInfo) {
@@ -286,103 +276,127 @@ LivemarkService.prototype = {
     let hasGuid = "guid" in aLivemarkInfo;
     let hasId = "id" in aLivemarkInfo;
     if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
         (hasId && aLivemarkInfo.id < 1) ||
         (!hasId && !hasGuid)) {
       throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
     }
 
-    return (async () => {
+    return this._withLivemarksMap(async livemarksMap => {
       if (!aLivemarkInfo.guid)
         aLivemarkInfo.guid = await PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
 
-      let livemarksMap = await this._promiseLivemarksMap();
       if (!livemarksMap.has(aLivemarkInfo.guid))
         throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
 
       return livemarksMap.get(aLivemarkInfo.guid);
-    })();
+    });
+  },
+
+  invalidateCachedLivemarks() {
+    if (this._reloadTimer) {
+      this._reloading = false;
+      this._reloadTimer.cancel();
+      delete this._reloadTimer;
+    }
+    let promise = this._promiseLivemarksMapReady.then(() => {
+      let livemarksMap = this._livemarksMap;
+      this._livemarksMap = null;
+      if (livemarksMap) {
+        // Stop any ongoing network fetch.
+        for (let livemark of livemarksMap.values()) {
+          livemark.terminate();
+        }
+      }
+    });
+    this._promiseLivemarksMapReady = promise.catch(Cu.reportError);
+    return promise;
   },
 
   // nsINavBookmarkObserver
 
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
   onItemVisited() {},
   onItemAdded() {},
 
   onItemChanged(id, property, isAnno, value, lastModified, itemType, parentId,
                 guid, parentGuid) {
     if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
       return;
 
-    this._promiseLivemarksMap().then(livemarksMap => {
+    if (isAnno && (property == PlacesUtils.LMANNO_FEEDURI ||
+                   property == PlacesUtils.LMANNO_SITEURI)) {
+      return;
+    }
+
+    this._withLivemarksMap(livemarksMap => {
       if (livemarksMap.has(guid)) {
         let livemark = livemarksMap.get(guid);
         if (property == "title") {
           livemark.title = value;
         }
         livemark.lastModified = lastModified;
       }
     });
   },
 
   onItemMoved(id, parentId, oldIndex, newParentId, newIndex, itemType, guid,
               oldParentGuid, newParentGuid) {
     if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
       return;
 
-    this._promiseLivemarksMap().then(livemarksMap => {
+    this._withLivemarksMap(livemarksMap => {
       if (livemarksMap.has(guid)) {
         let livemark = livemarksMap.get(guid);
         livemark.parentId = newParentId;
         livemark.parentGuid = newParentGuid;
         livemark.index = newIndex;
       }
     });
   },
 
   onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
     if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
       return;
 
-    this._promiseLivemarksMap().then(livemarksMap => {
+    this._withLivemarksMap(livemarksMap => {
       if (livemarksMap.has(guid)) {
         let livemark = livemarksMap.get(guid);
         livemark.terminate();
         livemarksMap.delete(guid);
       }
     });
   },
 
   // nsINavHistoryObserver
 
   onPageChanged() {},
   onTitleChanged() {},
   onDeleteVisits() {},
 
   onClearHistory() {
-    this._promiseLivemarksMap().then(livemarksMap => {
+    this._withLivemarksMap(livemarksMap => {
       for (let livemark of livemarksMap.values()) {
         livemark.updateURIVisitedStatus(null, false);
       }
     });
   },
 
   onDeleteURI(aURI) {
-    this._promiseLivemarksMap().then(livemarksMap => {
+    this._withLivemarksMap(livemarksMap => {
       for (let livemark of livemarksMap.values()) {
         livemark.updateURIVisitedStatus(aURI, false);
       }
     });
   },
 
   onVisit(aURI) {
-    this._promiseLivemarksMap().then(livemarksMap => {
+    this._withLivemarksMap(livemarksMap => {
       for (let livemark of livemarksMap.values()) {
         livemark.updateURIVisitedStatus(aURI, true);
       }
     });
   },
 
   // nsISupports
 
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -329,17 +329,18 @@ async function ensureEqualBookmarksTrees
 
   for (let property of Object.keys(aOriginal)) {
     if (property == "children") {
       Assert.equal(aOriginal.children.length, aNew.children.length);
       for (let i = 0; i < aOriginal.children.length; i++) {
         await ensureEqualBookmarksTrees(aOriginal.children[i],
                                         aNew.children[i],
                                         false,
-                                        true);
+                                        true,
+                                        aIgnoreAllDates);
       }
     } else if (property == "guid") {
       // guid shouldn't be copied if the item was not restored.
       Assert.notEqual(aOriginal.guid, aNew.guid);
     } else if (property == "dateAdded") {
       // dateAdded shouldn't be copied if the item was not restored.
       Assert.ok(is_time_ordered(aOriginal.dateAdded, aNew.dateAdded));
     } else if (property == "lastModified") {
@@ -982,26 +983,26 @@ add_task(async function test_add_and_rem
   let removeTxn = PT.Remove(guid);
   await removeTxn.transact();
   await ensureNonExistent(guid);
 
   async function undo() {
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
     await PT.undo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
-    await ensureBookmarksTreeRestoredCorrectly(originalInfo);
+    await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo);
     await PT.undo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
     await ensureNonExistent(guid);
   }
   async function redo() {
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
     await PT.redo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
-    await ensureBookmarksTreeRestoredCorrectly(originalInfo);
+    await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo);
     await PT.redo();
     ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
     await ensureNonExistent(guid);
   }
 
   await undo();
   await redo();
   await undo();
--- a/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
+++ b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
@@ -184,17 +184,17 @@ add_task(async function test_addLivemark
   do_check_true(livemark.id > 0);
   do_check_valid_places_guid(livemark.guid);
   do_check_eq(livemark.title, "test");
   do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
   do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   do_check_true(livemark.feedURI.equals(FEED_URI));
   do_check_eq(livemark.siteURI, null);
   do_check_true(livemark.lastModified > 0);
-  do_check_true(is_time_ordered(livemark.dateAdded, livemark.lastModified));
+  do_check_eq(livemark.dateAdded, livemark.lastModified);
 
   let bookmark = await PlacesUtils.bookmarks.fetch(livemark.guid);
   do_check_eq(livemark.index, bookmark.index);
   do_check_eq(livemark.dateAdded, bookmark.dateAdded * 1000);
 });
 
 add_task(async function test_addLivemark_succeeds() {
   let livemark = await PlacesUtils.livemarks.addLivemark(