/* 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/. */ import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs"; import { Changeset, Store, SyncEngine, Tracker, } from "resource://services-sync/engines.sys.mjs"; import { CryptoWrapper } from "resource://services-sync/record.sys.mjs"; import { Svc, Utils } from "resource://services-sync/util.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { Async: "resource://services-common/async.sys.mjs", Observers: "resource://services-common/observers.sys.mjs", PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs", PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs", PlacesSyncUtils: "resource://gre/modules/PlacesSyncUtils.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", Resource: "resource://services-sync/resource.sys.mjs", SyncedBookmarksMirror: "resource://gre/modules/SyncedBookmarksMirror.sys.mjs", }); const PLACES_MAINTENANCE_INTERVAL_SECONDS = 4 * 60 * 60; // 4 hours. const FOLDER_SORTINDEX = 1000000; // Roots that should be deleted from the server, instead of applied locally. // This matches `AndroidBrowserBookmarksRepositorySession::forbiddenGUID`, // but allows tags because we don't want to reparent tag folders or tag items // to "unfiled". const FORBIDDEN_INCOMING_IDS = ["pinned", "places", "readinglist"]; // Items with these parents should be deleted from the server. We allow // children of the Places root, to avoid orphaning left pane queries and other // descendants of custom roots. const FORBIDDEN_INCOMING_PARENT_IDS = ["pinned", "readinglist"]; // The tracker ignores changes made by import and restore, to avoid bumping the // score and triggering syncs during the process, as well as changes made by // Sync. ChromeUtils.defineLazyGetter(lazy, "IGNORED_SOURCES", () => [ lazy.PlacesUtils.bookmarks.SOURCES.SYNC, lazy.PlacesUtils.bookmarks.SOURCES.IMPORT, lazy.PlacesUtils.bookmarks.SOURCES.RESTORE, lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP, lazy.PlacesUtils.bookmarks.SOURCES.SYNC_REPARENT_REMOVED_FOLDER_CHILDREN, ]); // The validation telemetry version for the engine. Version 1 is collected // by `bookmark_validator.js`, and checks value as well as structure // differences. Version 2 is collected by the engine as part of building the // remote tree, and checks structure differences only. const BOOKMARK_VALIDATOR_VERSION = 2; // The maximum time that the engine should wait before aborting a bookmark // merge. const BOOKMARK_APPLY_TIMEOUT_MS = 5 * 60 * 60 * 1000; // 5 minutes // The default frecency value to use when not known. const FRECENCY_UNKNOWN = -1; // Returns the constructor for a bookmark record type. function getTypeObject(type) { switch (type) { case "bookmark": return Bookmark; case "query": return BookmarkQuery; case "folder": return BookmarkFolder; case "livemark": return Livemark; case "separator": return BookmarkSeparator; case "item": return PlacesItem; } return null; } export function PlacesItem(collection, id, type) { CryptoWrapper.call(this, collection, id); this.type = type || "item"; } PlacesItem.prototype = { async decrypt(keyBundle) { // Do the normal CryptoWrapper decrypt, but change types before returning let clear = await CryptoWrapper.prototype.decrypt.call(this, keyBundle); // Convert the abstract places item to the actual object type if (!this.deleted) { Object.setPrototypeOf(this, this.getTypeObject(this.type).prototype); } return clear; }, getTypeObject: function PlacesItem_getTypeObject(type) { let recordObj = getTypeObject(type); if (!recordObj) { throw new Error("Unknown places item object type: " + type); } return recordObj; }, _logName: "Sync.Record.PlacesItem", // Converts the record to a Sync bookmark object that can be passed to // `PlacesSyncUtils.bookmarks.{insert, update}`. toSyncBookmark() { let result = { kind: this.type, recordId: this.id, parentRecordId: this.parentid, }; let dateAdded = lazy.PlacesSyncUtils.bookmarks.ratchetTimestampBackwards( this.dateAdded, +this.modified * 1000 ); if (dateAdded > 0) { result.dateAdded = dateAdded; } return result; }, // Populates the record from a Sync bookmark object returned from // `PlacesSyncUtils.bookmarks.fetch`. fromSyncBookmark(item) { this.parentid = item.parentRecordId; this.parentName = item.parentTitle; if (item.dateAdded) { this.dateAdded = item.dateAdded; } }, }; Object.setPrototypeOf(PlacesItem.prototype, CryptoWrapper.prototype); Utils.deferGetSet(PlacesItem, "cleartext", [ "hasDupe", "parentid", "parentName", "type", "dateAdded", ]); export function Bookmark(collection, id, type) { PlacesItem.call(this, collection, id, type || "bookmark"); } Bookmark.prototype = { _logName: "Sync.Record.Bookmark", toSyncBookmark() { let info = PlacesItem.prototype.toSyncBookmark.call(this); info.title = this.title; info.url = this.bmkUri; info.description = this.description; info.tags = this.tags; info.keyword = this.keyword; return info; }, fromSyncBookmark(item) { PlacesItem.prototype.fromSyncBookmark.call(this, item); this.title = item.title; this.bmkUri = item.url.href; this.description = item.description; this.tags = item.tags; this.keyword = item.keyword; }, }; Object.setPrototypeOf(Bookmark.prototype, PlacesItem.prototype); Utils.deferGetSet(Bookmark, "cleartext", [ "title", "bmkUri", "description", "tags", "keyword", ]); export function BookmarkQuery(collection, id) { Bookmark.call(this, collection, id, "query"); } BookmarkQuery.prototype = { _logName: "Sync.Record.BookmarkQuery", toSyncBookmark() { let info = Bookmark.prototype.toSyncBookmark.call(this); info.folder = this.folderName || undefined; // empty string -> undefined info.query = this.queryId; return info; }, fromSyncBookmark(item) { Bookmark.prototype.fromSyncBookmark.call(this, item); this.folderName = item.folder || undefined; // empty string -> undefined this.queryId = item.query; }, }; Object.setPrototypeOf(BookmarkQuery.prototype, Bookmark.prototype); Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName", "queryId"]); export function BookmarkFolder(collection, id, type) { PlacesItem.call(this, collection, id, type || "folder"); } BookmarkFolder.prototype = { _logName: "Sync.Record.Folder", toSyncBookmark() { let info = PlacesItem.prototype.toSyncBookmark.call(this); info.description = this.description; info.title = this.title; return info; }, fromSyncBookmark(item) { PlacesItem.prototype.fromSyncBookmark.call(this, item); this.title = item.title; this.description = item.description; this.children = item.childRecordIds; }, }; Object.setPrototypeOf(BookmarkFolder.prototype, PlacesItem.prototype); Utils.deferGetSet(BookmarkFolder, "cleartext", [ "description", "title", "children", ]); export function Livemark(collection, id) { BookmarkFolder.call(this, collection, id, "livemark"); } Livemark.prototype = { _logName: "Sync.Record.Livemark", toSyncBookmark() { let info = BookmarkFolder.prototype.toSyncBookmark.call(this); info.feed = this.feedUri; info.site = this.siteUri; return info; }, fromSyncBookmark(item) { BookmarkFolder.prototype.fromSyncBookmark.call(this, item); this.feedUri = item.feed.href; if (item.site) { this.siteUri = item.site.href; } }, }; Object.setPrototypeOf(Livemark.prototype, BookmarkFolder.prototype); Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]); export function BookmarkSeparator(collection, id) { PlacesItem.call(this, collection, id, "separator"); } BookmarkSeparator.prototype = { _logName: "Sync.Record.Separator", fromSyncBookmark(item) { PlacesItem.prototype.fromSyncBookmark.call(this, item); this.pos = item.index; }, }; Object.setPrototypeOf(BookmarkSeparator.prototype, PlacesItem.prototype); Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos"); /** * The bookmarks engine uses a different store that stages downloaded bookmarks * in a separate database, instead of writing directly to Places. The buffer * handles reconciliation, so we stub out `_reconcile`, and wait to pull changes * until we're ready to upload. */ export function BookmarksEngine(service) { SyncEngine.call(this, "Bookmarks", service); } BookmarksEngine.prototype = { _recordObj: PlacesItem, _trackerObj: BookmarksTracker, _storeObj: BookmarksStore, version: 2, // Used to override the engine name in telemetry, so that we can distinguish // this engine from the old, now removed non-buffered engine. overrideTelemetryName: "bookmarks-buffered", // Needed to ensure we don't miss items when resuming a sync that failed or // aborted early. _defaultSort: "oldest", syncPriority: 4, allowSkippedRecord: false, async _ensureCurrentSyncID(newSyncID) { await lazy.PlacesSyncUtils.bookmarks.ensureCurrentSyncId(newSyncID); let buf = await this._store.ensureOpenMirror(); await buf.ensureCurrentSyncId(newSyncID); }, async ensureCurrentSyncID(newSyncID) { let shouldWipeRemote = await lazy.PlacesSyncUtils.bookmarks.shouldWipeRemote(); if (!shouldWipeRemote) { this._log.debug( "Checking if server sync ID ${newSyncID} matches existing", { newSyncID } ); await this._ensureCurrentSyncID(newSyncID); return newSyncID; } // We didn't take the new sync ID because we need to wipe the server // and other clients after a restore. Send the command, wipe the // server, and reset our sync ID to reupload everything. this._log.debug( "Ignoring server sync ID ${newSyncID} after restore; " + "wiping server and resetting sync ID", { newSyncID } ); await this.service.clientsEngine.sendCommand( "wipeEngine", [this.name], null, { reason: "bookmark-restore" } ); let assignedSyncID = await this.resetSyncID(); return assignedSyncID; }, async getSyncID() { return lazy.PlacesSyncUtils.bookmarks.getSyncId(); }, async resetSyncID() { await this._deleteServerCollection(); return this.resetLocalSyncID(); }, async resetLocalSyncID() { let newSyncID = await lazy.PlacesSyncUtils.bookmarks.resetSyncId(); this._log.debug("Assigned new sync ID ${newSyncID}", { newSyncID }); let buf = await this._store.ensureOpenMirror(); await buf.ensureCurrentSyncId(newSyncID); return newSyncID; }, async getLastSync() { let mirror = await this._store.ensureOpenMirror(); return mirror.getCollectionHighWaterMark(); }, async setLastSync(lastSync) { let mirror = await this._store.ensureOpenMirror(); await mirror.setCollectionLastModified(lastSync); // Update the last sync time in Places so that reverting to the original // bookmarks engine doesn't download records we've already applied. await lazy.PlacesSyncUtils.bookmarks.setLastSync(lastSync); }, async _syncStartup() { await super._syncStartup(); try { // For first syncs, back up the user's bookmarks. let lastSync = await this.getLastSync(); if (!lastSync) { this._log.debug("Bookmarks backup starting"); await lazy.PlacesBackups.create(null, true); this._log.debug("Bookmarks backup done"); } } catch (ex) { // Failure to create a backup is somewhat bad, but probably not bad // enough to prevent syncing of bookmarks - so just log the error and // continue. this._log.warn( "Error while backing up bookmarks, but continuing with sync", ex ); } }, async _sync() { try { await super._sync(); if (this._ranMaintenanceOnLastSync) { // If the last sync failed, we ran maintenance, and this sync succeeded, // maintenance likely fixed the issue. this._ranMaintenanceOnLastSync = false; this.service.recordTelemetryEvent("maintenance", "fix", "bookmarks"); } } catch (ex) { if ( lazy.Async.isShutdownException(ex) || ex.status > 0 || ex.name == "InterruptedError" ) { // Don't run maintenance on shutdown or HTTP errors, or if we aborted // the sync because the user changed their bookmarks during merging. throw ex; } if (ex.name == "MergeConflictError") { this._log.warn( "Bookmark syncing ran into a merge conflict error...will retry later" ); return; } // Run Places maintenance periodically to try to recover from corruption // that might have caused the sync to fail. We cap the interval because // persistent failures likely indicate a problem that won't be fixed by // running maintenance after every failed sync. let elapsedSinceMaintenance = Date.now() / 1000 - Services.prefs.getIntPref("places.database.lastMaintenance", 0); if (elapsedSinceMaintenance >= PLACES_MAINTENANCE_INTERVAL_SECONDS) { this._log.error( "Bookmark sync failed, ${elapsedSinceMaintenance}s " + "elapsed since last run; running Places maintenance", { elapsedSinceMaintenance } ); await lazy.PlacesDBUtils.maintenanceOnIdle(); this._ranMaintenanceOnLastSync = true; this.service.recordTelemetryEvent("maintenance", "run", "bookmarks"); } else { this._ranMaintenanceOnLastSync = false; } throw ex; } }, async _syncFinish() { await SyncEngine.prototype._syncFinish.call(this); await lazy.PlacesSyncUtils.bookmarks.ensureMobileQuery(); }, async pullAllChanges() { return this.pullNewChanges(); }, async trackRemainingChanges() { let changes = this._modified.changes; await lazy.PlacesSyncUtils.bookmarks.pushChanges(changes); }, _deleteId(id) { this._noteDeletedId(id); }, // The bookmarks engine rarely calls this method directly, except in tests or // when handling a `reset{All, Engine}` command from another client. We // usually reset local Sync metadata on a sync ID mismatch, which both engines // override with logic that lives in Places and the mirror. async _resetClient() { await super._resetClient(); await lazy.PlacesSyncUtils.bookmarks.reset(); let buf = await this._store.ensureOpenMirror(); await buf.reset(); }, // Cleans up the Places root, reading list items (ignored in bug 762118, // removed in bug 1155684), and pinned sites. _shouldDeleteRemotely(incomingItem) { return ( FORBIDDEN_INCOMING_IDS.includes(incomingItem.id) || FORBIDDEN_INCOMING_PARENT_IDS.includes(incomingItem.parentid) ); }, emptyChangeset() { return new BookmarksChangeset(); }, async _apply() { let buf = await this._store.ensureOpenMirror(); let watchdog = this._newWatchdog(); watchdog.start(BOOKMARK_APPLY_TIMEOUT_MS); try { let recordsToUpload = await buf.apply({ remoteTimeSeconds: lazy.Resource.serverTime, signal: watchdog.signal, }); this._modified.replace(recordsToUpload); } finally { watchdog.stop(); if (watchdog.abortReason) { this._log.warn(`Aborting bookmark merge: ${watchdog.abortReason}`); } } }, async _processIncoming(newitems) { await super._processIncoming(newitems); await this._apply(); }, async _reconcile(item) { return true; }, async _createRecord(id) { let record = await this._doCreateRecord(id); if (!record.deleted) { // Set hasDupe on all (non-deleted) records since we don't use it and we // want to minimize the risk of older clients corrupting records. Note // that the SyncedBookmarksMirror sets it for all records that it created, // but we would like to ensure that weakly uploaded records are marked as // hasDupe as well. record.hasDupe = true; } return record; }, async _doCreateRecord(id) { let change = this._modified.changes[id]; if (!change) { this._log.error( "Creating record for item ${id} not in strong changeset", { id } ); throw new TypeError("Can't create record for unchanged item"); } let record = this._recordFromCleartext(id, change.cleartext); record.sortindex = await this._store._calculateIndex(record); return record; }, _recordFromCleartext(id, cleartext) { let recordObj = getTypeObject(cleartext.type); if (!recordObj) { this._log.warn( "Creating record for item ${id} with unknown type ${type}", { id, type: cleartext.type } ); recordObj = PlacesItem; } let record = new recordObj(this.name, id); record.cleartext = cleartext; return record; }, async pullChanges() { return {}; }, /** * Writes successfully uploaded records back to the mirror, so that the * mirror matches the server. We update the mirror before updating Places, * which has implications for interrupted syncs. * * 1. Sync interrupted during upload; server doesn't support atomic uploads. * We'll download and reapply everything that we uploaded before the * interruption. All locally changed items retain their change counters. * 2. Sync interrupted during upload; atomic uploads enabled. The server * discards the batch. All changed local items retain their change * counters, so the next sync resumes cleanly. * 3. Sync interrupted during upload; outgoing records can't fit in a single * batch. We'll download and reapply all records through the most recent * committed batch. This is a variation of (1). * 4. Sync interrupted after we update the mirror, but before cleanup. The * mirror matches the server, but locally changed items retain their change * counters. Reuploading them on the next sync should be idempotent, though * unnecessary. If another client makes a conflicting remote change before * we sync again, we may incorrectly prefer the local state. * 5. Sync completes successfully. We'll update the mirror, and reset the * change counters for all items. */ async _onRecordsWritten(succeeded, failed, serverModifiedTime) { let records = []; for (let id of succeeded) { let change = this._modified.changes[id]; if (!change) { // TODO (Bug 1433178): Write weakly uploaded records back to the mirror. this._log.info("Uploaded record not in strong changeset", id); continue; } if (!change.synced) { this._log.info("Record in strong changeset not uploaded", id); continue; } let cleartext = change.cleartext; if (!cleartext) { this._log.error( "Missing Sync record cleartext for ${id} in ${change}", { id, change } ); throw new TypeError("Missing cleartext for uploaded Sync record"); } let record = this._recordFromCleartext(id, cleartext); record.modified = serverModifiedTime; records.push(record); } let buf = await this._store.ensureOpenMirror(); await buf.store(records, { needsMerge: false }); }, async finalize() { await super.finalize(); await this._store.finalize(); }, }; Object.setPrototypeOf(BookmarksEngine.prototype, SyncEngine.prototype); /** * The bookmarks store delegates to the mirror for staging and applying * records. Most `Store` methods intentionally remain abstract, so you can't use * this store to create or update bookmarks in Places. All changes must go * through the mirror, which takes care of merging and producing a valid tree. */ function BookmarksStore(name, engine) { Store.call(this, name, engine); } BookmarksStore.prototype = { _openMirrorPromise: null, // For tests. _batchChunkSize: 500, // Create a record starting from the weave id (places guid) async createRecord(id, collection) { let item = await lazy.PlacesSyncUtils.bookmarks.fetch(id); if (!item) { // deleted item let record = new PlacesItem(collection, id); record.deleted = true; return record; } let recordObj = getTypeObject(item.kind); if (!recordObj) { this._log.warn("Unknown item type, cannot serialize: " + item.kind); recordObj = PlacesItem; } let record = new recordObj(collection, id); record.fromSyncBookmark(item); record.sortindex = await this._calculateIndex(record); return record; }, async _calculateIndex(record) { // Ensure folders have a very high sort index so they're not synced last. if (record.type == "folder") { return FOLDER_SORTINDEX; } // For anything directly under the toolbar, give it a boost of more than an // unvisited bookmark let index = 0; if (record.parentid == "toolbar") { index += 150; } // Add in the bookmark's frecency if we have something. if (record.bmkUri != null) { let frecency = FRECENCY_UNKNOWN; try { frecency = await lazy.PlacesSyncUtils.history.fetchURLFrecency( record.bmkUri ); } catch (ex) { this._log.warn( `Failed to fetch frecency for ${record.id}; assuming default`, ex ); this._log.trace("Record {id} has invalid URL ${bmkUri}", record); } if (frecency != FRECENCY_UNKNOWN) { index += frecency; } } return index; }, async wipe() { // Save a backup before clearing out all bookmarks. await lazy.PlacesBackups.create(null, true); await lazy.PlacesSyncUtils.bookmarks.wipe(); }, ensureOpenMirror() { if (!this._openMirrorPromise) { this._openMirrorPromise = this._openMirror().catch(err => { // We may have failed to open the mirror temporarily; for example, if // the database is locked. Clear the promise so that subsequent // `ensureOpenMirror` calls can try to open the mirror again. this._openMirrorPromise = null; throw err; }); } return this._openMirrorPromise; }, async _openMirror() { let mirrorPath = PathUtils.join( PathUtils.profileDir, "weave", "bookmarks.sqlite" ); await IOUtils.makeDirectory(PathUtils.parent(mirrorPath), { createAncestors: true, }); return lazy.SyncedBookmarksMirror.open({ path: mirrorPath, recordStepTelemetry: (name, took, counts) => { lazy.Observers.notify( "weave:engine:sync:step", { name, took, counts, }, this.name ); }, recordValidationTelemetry: (took, checked, problems) => { lazy.Observers.notify( "weave:engine:validate:finish", { version: BOOKMARK_VALIDATOR_VERSION, took, checked, problems, }, this.name ); }, }); }, async applyIncomingBatch(records, countTelemetry) { let buf = await this.ensureOpenMirror(); for (let chunk of lazy.PlacesUtils.chunkArray( records, this._batchChunkSize )) { await buf.store(chunk); } // Array of failed records. return []; }, async applyIncoming(record) { let buf = await this.ensureOpenMirror(); await buf.store([record]); }, async finalize() { if (!this._openMirrorPromise) { return; } let buf = await this._openMirrorPromise; await buf.finalize(); }, }; Object.setPrototypeOf(BookmarksStore.prototype, Store.prototype); // The bookmarks tracker is a special flower. Instead of listening for changes // via observer notifications, it queries Places for the set of items that have // changed since the last sync. Because it's a "pull-based" tracker, it ignores // all concepts of "add a changed ID." However, it still registers an observer // to bump the score, so that changed bookmarks are synced immediately. function BookmarksTracker(name, engine) { Tracker.call(this, name, engine); } BookmarksTracker.prototype = { onStart() { this._placesListener = new PlacesWeakCallbackWrapper( this.handlePlacesEvents.bind(this) ); lazy.PlacesUtils.observers.addListener( [ "bookmark-added", "bookmark-removed", "bookmark-moved", "bookmark-guid-changed", "bookmark-keyword-changed", "bookmark-tags-changed", "bookmark-time-changed", "bookmark-title-changed", "bookmark-url-changed", ], this._placesListener ); Svc.Obs.add("bookmarks-restore-begin", this); Svc.Obs.add("bookmarks-restore-success", this); Svc.Obs.add("bookmarks-restore-failed", this); }, onStop() { lazy.PlacesUtils.observers.removeListener( [ "bookmark-added", "bookmark-removed", "bookmark-moved", "bookmark-guid-changed", "bookmark-keyword-changed", "bookmark-tags-changed", "bookmark-time-changed", "bookmark-title-changed", "bookmark-url-changed", ], this._placesListener ); Svc.Obs.remove("bookmarks-restore-begin", this); Svc.Obs.remove("bookmarks-restore-success", this); Svc.Obs.remove("bookmarks-restore-failed", this); }, async getChangedIDs() { return lazy.PlacesSyncUtils.bookmarks.pullChanges(); }, observe(subject, topic, data) { switch (topic) { case "bookmarks-restore-begin": this._log.debug("Ignoring changes from importing bookmarks."); break; case "bookmarks-restore-success": this._log.debug("Tracking all items on successful import."); if (data == "json") { this._log.debug( "Restore succeeded: wiping server and other clients." ); // Trigger an immediate sync. `ensureCurrentSyncID` will notice we // restored, wipe the server and other clients, reset the sync ID, and // upload the restored tree. this.score += SCORE_INCREMENT_XLARGE; } else { // "html", "html-initial", or "json-append" this._log.debug("Import succeeded."); } break; case "bookmarks-restore-failed": this._log.debug("Tracking all items on failed import."); break; } }, QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]), /* Every add/remove/change will trigger a sync for MULTI_DEVICE */ _upScore: function BMT__upScore() { this.score += SCORE_INCREMENT_XLARGE; }, handlePlacesEvents(events) { for (let event of events) { switch (event.type) { case "bookmark-added": case "bookmark-removed": case "bookmark-moved": case "bookmark-keyword-changed": case "bookmark-tags-changed": case "bookmark-time-changed": case "bookmark-title-changed": case "bookmark-url-changed": if (lazy.IGNORED_SOURCES.includes(event.source)) { continue; } this._log.trace(`'${event.type}': ${event.id}`); this._upScore(); break; case "bookmark-guid-changed": if (event.source !== lazy.PlacesUtils.bookmarks.SOURCES.SYNC) { this._log.warn( "The source of bookmark-guid-changed event shoud be sync." ); continue; } this._log.trace(`'${event.type}': ${event.id}`); this._upScore(); break; case "purge-caches": this._log.trace("purge-caches"); this._upScore(); break; } } }, }; Object.setPrototypeOf(BookmarksTracker.prototype, Tracker.prototype); /** * A changeset that stores extra metadata in a change record for each ID. The * engine updates this metadata when uploading Sync records, and writes it back * to Places in `BookmarksEngine#trackRemainingChanges`. * * The `synced` property on a change record means its corresponding item has * been uploaded, and we should pretend it doesn't exist in the changeset. */ class BookmarksChangeset extends Changeset { // Only `_reconcile` calls `getModifiedTimestamp` and `has`, and the engine // does its own reconciliation. getModifiedTimestamp(id) { throw new Error("Don't use timestamps to resolve bookmark conflicts"); } has(id) { throw new Error("Don't use the changeset to resolve bookmark conflicts"); } delete(id) { let change = this.changes[id]; if (change) { // Mark the change as synced without removing it from the set. We do this // so that we can update Places in `trackRemainingChanges`. change.synced = true; } } ids() { let results = new Set(); for (let id in this.changes) { if (!this.changes[id].synced) { results.add(id); } } return [...results]; } }