Duplicates the encryption and payload formatting logic, but this seems fine and clear. Differential Revision: https://phabricator.services.mozilla.com/D311813
713 lines
24 KiB
JavaScript
713 lines
24 KiB
JavaScript
/* 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/. */
|
|
|
|
const STORAGE_VERSION = 1; // This needs to be kept in-sync with the rust storage version
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { SyncEngine, Tracker } from "resource://services-sync/engines.sys.mjs";
|
|
import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
|
|
import { Log } from "resource://gre/modules/Log.sys.mjs";
|
|
import {
|
|
SCORE_INCREMENT_SMALL,
|
|
STATUS_OK,
|
|
URI_LENGTH_MAX,
|
|
} from "resource://services-sync/constants.sys.mjs";
|
|
import { CommonUtils } from "resource://services-common/utils.sys.mjs";
|
|
import { Async } from "resource://services-common/async.sys.mjs";
|
|
import { Observers } from "resource://services-common/observers.sys.mjs";
|
|
import { Collection } from "resource://services-sync/record.sys.mjs";
|
|
import {
|
|
SyncRecord,
|
|
SyncTelemetry,
|
|
} from "resource://services-sync/telemetry.sys.mjs";
|
|
import { BridgedEngine } from "resource://services-sync/bridged_engine.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs",
|
|
getTabsStore: "resource://services-sync/TabsStore.sys.mjs",
|
|
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
|
|
RemoteTabRecord:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs",
|
|
LocalTabsInfo:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs",
|
|
TabGroup:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs",
|
|
Window:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs",
|
|
WindowType:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustTabs.sys.mjs",
|
|
setupLoggerForTarget: "resource://gre/modules/AppServicesTracing.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"TABS_FILTERED_SCHEMES",
|
|
"services.sync.engine.tabs.filteredSchemes",
|
|
"",
|
|
null,
|
|
val => {
|
|
return new Set(val.split("|"));
|
|
}
|
|
);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"SYNC_AFTER_DELAY_MS",
|
|
"services.sync.syncedTabs.syncDelayAfterTabChange",
|
|
0
|
|
);
|
|
|
|
// A "bridged engine" to our tabs component.
|
|
export function TabEngine(service) {
|
|
BridgedEngine.call(this, "Tabs", service);
|
|
}
|
|
|
|
TabEngine.prototype = {
|
|
_trackerObj: TabTracker,
|
|
syncPriority: 3,
|
|
|
|
async prepareTheBridge() {
|
|
let clientsEngine = this.service.clientsEngine;
|
|
// Tell the bridged engine about clients.
|
|
// This is the same shape as ClientData in app-services.
|
|
// schema: https://github.com/mozilla/application-services/blob/a1168751231ed4e88c44d85f6dccc09c3b412bd2/components/sync15/src/client_types.rs#L14
|
|
let clientData = {
|
|
local_client_id: clientsEngine.localID,
|
|
recent_clients: {},
|
|
};
|
|
|
|
// We shouldn't upload tabs past what the server will accept
|
|
const maxPayloadSize = this.service.getMaxRecordPayloadSize();
|
|
let tabsInfo = await TabProvider.getLocalTabsInfo(maxPayloadSize);
|
|
|
|
await this._rustStore.setLocalTabsInfo(tabsInfo);
|
|
|
|
for (let remoteClient of clientsEngine.remoteClients) {
|
|
let id = remoteClient.id;
|
|
if (!id) {
|
|
throw new Error("Remote client somehow did not have an id");
|
|
}
|
|
let client = {
|
|
fxa_device_id: remoteClient.fxaDeviceId,
|
|
// device_name and device_type are soft-deprecated - every client
|
|
// prefers what's in the FxA record. But fill them correctly anyway.
|
|
device_name: clientsEngine.getClientName(id) ?? "",
|
|
device_type: clientsEngine.getClientType(id),
|
|
};
|
|
clientData.recent_clients[id] = client;
|
|
}
|
|
|
|
// put ourself in there too so we record the correct device info in our sync record.
|
|
clientData.recent_clients[clientsEngine.localID] = {
|
|
fxa_device_id: await clientsEngine.fxAccounts.device.getLocalId(),
|
|
device_name: clientsEngine.localName,
|
|
device_type: clientsEngine.localType,
|
|
};
|
|
|
|
// Recover from an older pre-154-ish build that crashed with lastSync set to
|
|
// FAR_FUTURE: reset it so incoming tabs are downloaded again. Can be removed once
|
|
// ESRs don't cover that.
|
|
const FAR_FUTURE = 4102405200000; // 2100/01/01, used by old versions.
|
|
if ((await this.getLastSync()) === FAR_FUTURE) {
|
|
await this._bridge.setLastSync(0);
|
|
}
|
|
await this._bridge.prepareForSync(JSON.stringify(clientData));
|
|
},
|
|
|
|
async _syncStartup() {
|
|
await super._syncStartup();
|
|
await this.prepareTheBridge();
|
|
},
|
|
|
|
async initialize() {
|
|
await SyncEngine.prototype.initialize.call(this);
|
|
|
|
lazy.setupLoggerForTarget("tabs", "Sync.Engine.Tabs");
|
|
// highlights problems with simple logs - we get everyone's sql-support
|
|
lazy.setupLoggerForTarget("sql_support", "Sync.Engine.Tabs");
|
|
this._rustStore = await lazy.getTabsStore();
|
|
this._bridge = await this._rustStore.bridgedEngine();
|
|
|
|
// Uniffi doesn't currently only support async methods, so we'll need to hardcode
|
|
// these values for now (which is fine for now as these hardly ever change)
|
|
this._bridge.storageVersion = STORAGE_VERSION;
|
|
this._bridge.allowSkippedRecord = true;
|
|
|
|
this._log.info("Got a bridged engine!");
|
|
this._tracker.modified = true;
|
|
},
|
|
|
|
async getChangedIDs() {
|
|
// No need for a proper timestamp (no conflict resolution needed).
|
|
let changedIDs = {};
|
|
if (this._tracker.modified) {
|
|
changedIDs[this.service.clientsEngine.localID] = 0;
|
|
}
|
|
return changedIDs;
|
|
},
|
|
|
|
// API for use by Sync UI code to give user choices of tabs to open.
|
|
async getAllClients() {
|
|
let remoteTabs = await this._rustStore.getAll();
|
|
let remoteClientTabs = [];
|
|
for (let remoteClient of this.service.clientsEngine.remoteClients) {
|
|
// We get the some client info from the rust tabs engine and some from
|
|
// the clients engine.
|
|
let rustClient = remoteTabs.find(
|
|
x => x.clientId === remoteClient.fxaDeviceId
|
|
);
|
|
if (!rustClient) {
|
|
continue;
|
|
}
|
|
let client = {
|
|
// rust gives us ms but js uses seconds, so fix them up.
|
|
tabs: rustClient.remoteTabs.map(tab => {
|
|
tab.lastUsed = tab.lastUsed / 1000;
|
|
return tab;
|
|
}),
|
|
lastModified: rustClient.lastModified / 1000,
|
|
...remoteClient,
|
|
};
|
|
remoteClientTabs.push(client);
|
|
}
|
|
return remoteClientTabs;
|
|
},
|
|
|
|
async removeClientData() {
|
|
let url = this.engineURL + "/" + this.service.clientsEngine.localID;
|
|
await this.service.resource(url).delete();
|
|
},
|
|
|
|
async trackRemainingChanges() {
|
|
if (this._modified.count() > 0) {
|
|
this._tracker.modified = true;
|
|
}
|
|
},
|
|
|
|
// Support for "quick writes"
|
|
_engineLock: Utils.lock,
|
|
_engineLocked: false,
|
|
|
|
// Tabs has a special lock to help support its "quick write"
|
|
get locked() {
|
|
return this._engineLocked;
|
|
},
|
|
lock() {
|
|
if (this._engineLocked) {
|
|
return false;
|
|
}
|
|
this._engineLocked = true;
|
|
return true;
|
|
},
|
|
unlock() {
|
|
this._engineLocked = false;
|
|
},
|
|
|
|
// Quickly do a POST of our current tabs if possible.
|
|
// This does things that would be dangerous for other engines - eg, posting
|
|
// without checking what's on the server could cause data-loss for other
|
|
// engines, but because each device exclusively owns exactly 1 tabs record
|
|
// with a known ID, it's safe here.
|
|
// Returns true if we successfully synced, false otherwise (either on error
|
|
// or because we declined to sync for any reason.) The return value is
|
|
// primarily for tests.
|
|
async quickWrite() {
|
|
if (!this.enabled) {
|
|
// this should be very rare, and only if tabs are disabled after the
|
|
// timer is created.
|
|
this._log.info("Can't do a quick-sync as tabs is disabled");
|
|
return false;
|
|
}
|
|
// This quick-sync doesn't drive the login state correctly, so just
|
|
// decline to sync if out status is bad
|
|
if (this.service.status.checkSetup() != STATUS_OK) {
|
|
this._log.info(
|
|
"Can't do a quick-sync due to the service status",
|
|
this.service.status.toString()
|
|
);
|
|
return false;
|
|
}
|
|
if (!this.service.serverConfiguration) {
|
|
this._log.info("Can't do a quick sync before the first full sync");
|
|
return false;
|
|
}
|
|
try {
|
|
return await this._engineLock("tabs.js: quickWrite", async () => {
|
|
return this._doQuickWrite();
|
|
})();
|
|
} catch (ex) {
|
|
if (!Utils.isLockException(ex)) {
|
|
throw ex;
|
|
}
|
|
this._log.info(
|
|
"Can't do a quick-write as another tab sync is in progress"
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// The guts of the quick-write sync, after we've taken the lock, checked
|
|
// the service status etc.
|
|
async _doQuickWrite() {
|
|
// We need to track telemetry for these syncs too!
|
|
const name = "tabs";
|
|
let telemetryRecord = new SyncRecord(
|
|
SyncTelemetry.allowedEngines,
|
|
"quick-write"
|
|
);
|
|
telemetryRecord.onEngineStart(name);
|
|
try {
|
|
Async.checkAppReady();
|
|
// We need to prep the bridge before we try to POST since it grabs
|
|
// the most recent local client id.
|
|
await this.prepareTheBridge();
|
|
this._tracker.clearChangedIDs();
|
|
this._tracker.resetScore();
|
|
|
|
Async.checkAppReady();
|
|
// now just the "upload" part of a sync, which for a rust engine is not obvious.
|
|
// We need to do is ask the rust engine for the changes. Although
|
|
// this is kinda abusing the bridged-engine interface, we know the tabs
|
|
// implementation of it works ok
|
|
let outgoing = await this._bridge.apply();
|
|
// We know we always have exactly 1 record.
|
|
let mine = outgoing[0];
|
|
this._log.trace("outgoing bso", mine);
|
|
// `this._recordObj` is a `BridgedRecord`, which isn't exported.
|
|
let record = this._recordObj.fromOutgoingBso(this.name, JSON.parse(mine));
|
|
|
|
Async.checkAppReady();
|
|
// This is a single, device-exclusive record we want to force onto the
|
|
// server, so we POST it directly and unconditionally: no
|
|
// X-If-Unmodified-Since precondition, no local state update.
|
|
await record.encrypt(
|
|
this.service.collectionKeys.keyForCollection(this.name)
|
|
);
|
|
let coll = new Collection(this.engineURL, this._recordObj, this.service);
|
|
let resp = await coll.postSingleRawRecord(record);
|
|
if (!resp.success) {
|
|
this._log.warn(`quick-write POST failed: ${resp.status}`);
|
|
throw resp;
|
|
}
|
|
Observers.notify(
|
|
"weave:engine:sync:uploaded",
|
|
{ sent: 1, failed: 0, failedReasons: null },
|
|
this.name
|
|
);
|
|
telemetryRecord.onEngineStop(name, null);
|
|
return true;
|
|
} catch (ex) {
|
|
this._log.warn("quicksync sync failed", ex);
|
|
telemetryRecord.onEngineStop(name, ex);
|
|
return false;
|
|
} finally {
|
|
// The top-level sync is never considered to fail here, just the engine
|
|
telemetryRecord.finished(null);
|
|
SyncTelemetry.takeTelemetryRecord(telemetryRecord);
|
|
}
|
|
},
|
|
|
|
async _sync() {
|
|
try {
|
|
await this._engineLock("tabs.js: fullSync", async () => {
|
|
await super._sync();
|
|
})();
|
|
} catch (ex) {
|
|
if (!Utils.isLockException(ex)) {
|
|
throw ex;
|
|
}
|
|
this._log.info(
|
|
"Can't do full tabs sync as a quick-write is currently running"
|
|
);
|
|
}
|
|
},
|
|
};
|
|
Object.setPrototypeOf(TabEngine.prototype, BridgedEngine.prototype);
|
|
|
|
export const TabProvider = {
|
|
// Get ordered non-private windows - can be overridden in tests
|
|
// Windows are ordered from most recently used to least recently used
|
|
getOrderedNonPrivateWindows() {
|
|
return lazy.BrowserWindowTracker.getOrderedWindows({ private: false });
|
|
},
|
|
|
|
// This function creates tabs records up to a specified amount of bytes
|
|
// It is an "estimation" since we don't accurately calculate how much the
|
|
// favicon and JSON overhead is and give a rough estimate (for optimization purposes)
|
|
async getAllTabsWithEstimatedMax(filter, bytesMax) {
|
|
let log = Log.repository.getLogger(`Sync.Engine.Tabs.Provider`);
|
|
let tabRecords = [];
|
|
let iconPromises = [];
|
|
let runningByteLength = 0;
|
|
let encoder = new TextEncoder();
|
|
let referencedWindowIds = new Set();
|
|
let referencedGroupIds = new Set();
|
|
|
|
// Get ordered windows once - order determines the index we report (lower index = more recent)
|
|
let orderedWindows = this.getOrderedNonPrivateWindows();
|
|
|
|
// Build a map of window -> windowId and collect all tabs
|
|
let windowIdMap = new Map();
|
|
let allWinTabs = [];
|
|
for (let [winIndex, win] of orderedWindows.entries()) {
|
|
let windowId = `window-${winIndex}`;
|
|
windowIdMap.set(win, windowId);
|
|
|
|
for (let tabIndex = 0; tabIndex < win.gBrowser.tabs.length; tabIndex++) {
|
|
let tab = win.gBrowser.tabs[tabIndex];
|
|
allWinTabs.push({ tab, windowId, windowIndex: winIndex, tabIndex });
|
|
}
|
|
}
|
|
|
|
// Sort by last accessed to prioritize recently used tabs
|
|
allWinTabs.sort((a, b) => b.tab.lastAccessed - a.tab.lastAccessed);
|
|
|
|
for (let { tab, windowId, tabIndex } of allWinTabs) {
|
|
// We don't want to process any more tabs than we can sync
|
|
if (runningByteLength >= bytesMax) {
|
|
log.warn(
|
|
`Can't fit all tabs in sync payload: have ${allWinTabs.length},
|
|
but can only fit ${tabRecords.length}.`
|
|
);
|
|
break;
|
|
}
|
|
|
|
// Note that we used to sync "tab history" (ie, the "back button") state,
|
|
// but in practice this hasn't been used - only the current URI is of
|
|
// interest to clients.
|
|
// We stopped recording this in bug 1783991.
|
|
if (!tab?.linkedBrowser) {
|
|
continue;
|
|
}
|
|
let acceptable = !filter
|
|
? url => url
|
|
: url =>
|
|
url &&
|
|
!lazy.TABS_FILTERED_SCHEMES.has(Services.io.extractScheme(url));
|
|
|
|
let url = tab.linkedBrowser.currentURI?.spec;
|
|
// Special case for reader mode.
|
|
if (url && url.startsWith("about:reader?")) {
|
|
url = lazy.ReaderMode.getOriginalUrl(url);
|
|
}
|
|
// We ignore the tab completely if the current entry url is
|
|
// not acceptable (we need something accurate to open).
|
|
if (!acceptable(url)) {
|
|
continue;
|
|
}
|
|
|
|
if (url.length > URI_LENGTH_MAX) {
|
|
log.trace("Skipping over-long URL.");
|
|
continue;
|
|
}
|
|
|
|
let tabGroupId = tab.group?.id || "";
|
|
|
|
let thisTab = new lazy.RemoteTabRecord({
|
|
title: tab.linkedBrowser.contentTitle || "",
|
|
urlHistory: [url],
|
|
icon: "",
|
|
lastUsed: tab.lastAccessed || 0,
|
|
inactive: false, // desktop doesn't have "inactive" tabs.
|
|
windowId,
|
|
index: tabIndex,
|
|
tabGroupId,
|
|
pinned: tab.pinned,
|
|
});
|
|
tabRecords.push(thisTab);
|
|
|
|
// Track which windows and groups are referenced
|
|
referencedWindowIds.add(windowId);
|
|
if (tabGroupId) {
|
|
referencedGroupIds.add(tabGroupId);
|
|
}
|
|
|
|
// we don't want to wait for each favicon to resolve to get the bytes
|
|
// so we estimate a conservative 100 chars for the favicon and json overhead
|
|
// Rust will further optimize and trim if we happened to be wildly off
|
|
runningByteLength +=
|
|
encoder.encode(thisTab.title + thisTab.lastUsed + url).byteLength + 100;
|
|
|
|
// Use the favicon service for the icon url - we can wait for the promises at the end.
|
|
let iconPromise = lazy.PlacesUtils.favicons
|
|
.getFaviconForPage(lazy.PlacesUtils.toURI(url))
|
|
.then(favicon => {
|
|
thisTab.icon = favicon.uri.spec;
|
|
})
|
|
.catch(() => {
|
|
log.trace(
|
|
`Failed to fetch favicon for ${url}`,
|
|
thisTab.urlHistory[0]
|
|
);
|
|
});
|
|
iconPromises.push(iconPromise);
|
|
}
|
|
|
|
await Promise.allSettled(iconPromises);
|
|
return {
|
|
tabs: tabRecords,
|
|
referencedWindowIds,
|
|
referencedGroupIds,
|
|
orderedWindows, // Return windows so we only collect them once
|
|
};
|
|
},
|
|
|
|
_collectReferencedTabGroups(referencedGroupIds, orderedWindows) {
|
|
let tabGroups = new Map();
|
|
for (let win of orderedWindows) {
|
|
if (!win.gBrowser.tabGroups) {
|
|
continue;
|
|
}
|
|
for (let group of win.gBrowser.tabGroups) {
|
|
if (referencedGroupIds.has(group.id)) {
|
|
tabGroups.set(
|
|
group.id,
|
|
new lazy.TabGroup({
|
|
id: group.id,
|
|
name: group.label,
|
|
color: group.color,
|
|
collapsed: group.collapsed,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return tabGroups;
|
|
},
|
|
|
|
_collectReferencedWindows(referencedWindowIds, orderedWindows) {
|
|
let windows = new Map();
|
|
// because we've generated the IDs based on the index we don't need to look
|
|
// inside the array, just know it's length
|
|
for (let winIndex = 0; winIndex < orderedWindows.length; winIndex++) {
|
|
let windowId = `window-${winIndex}`;
|
|
if (referencedWindowIds.has(windowId)) {
|
|
windows.set(
|
|
windowId,
|
|
new lazy.Window({
|
|
id: windowId,
|
|
// We don't have a "lastUsed" available for windows.
|
|
lastUsed: 0,
|
|
index: winIndex + 1,
|
|
windowType: lazy.WindowType.NORMAL,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
return windows;
|
|
},
|
|
|
|
// This function returns the LocalTabsInfo used by the Rust engine.
|
|
async getLocalTabsInfo(maxPayloadSize) {
|
|
// See bug 535326 comment 8 for an explanation of the estimation
|
|
const maxSerializedSize = (maxPayloadSize / 4) * 3 - 1500;
|
|
let { tabs, referencedWindowIds, referencedGroupIds, orderedWindows } =
|
|
await this.getAllTabsWithEstimatedMax(true, maxSerializedSize);
|
|
|
|
// Collect metadata for windows and groups that have tabs being synced.
|
|
// This will almost always be all windows and groups, but we might as well
|
|
// avoid including groups or windows if we ended up not including tabs in them.
|
|
let tabGroups = this._collectReferencedTabGroups(
|
|
referencedGroupIds,
|
|
orderedWindows
|
|
);
|
|
let windows = this._collectReferencedWindows(
|
|
referencedWindowIds,
|
|
orderedWindows
|
|
);
|
|
|
|
return new lazy.LocalTabsInfo({
|
|
tabs,
|
|
tabGroups,
|
|
windows,
|
|
});
|
|
},
|
|
};
|
|
|
|
function TabTracker(name, engine) {
|
|
Tracker.call(this, name, engine);
|
|
|
|
// Make sure "this" pointer is always set correctly for event listeners.
|
|
this.onTab = Utils.bind2(this, this.onTab);
|
|
this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
|
|
}
|
|
TabTracker.prototype = {
|
|
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
|
|
|
|
clearChangedIDs() {
|
|
this.modified = false;
|
|
},
|
|
|
|
// We do not track TabSelect because that almost always triggers
|
|
// the web progress listeners (onLocationChange), which we already track
|
|
_topics: ["TabOpen", "TabClose"],
|
|
|
|
_registerListenersForWindow(window) {
|
|
this._log.trace("Registering tab listeners in window");
|
|
for (let topic of this._topics) {
|
|
window.addEventListener(topic, this.onTab);
|
|
}
|
|
window.addEventListener("unload", this._unregisterListeners);
|
|
// If it's got a tab browser we can listen for things like navigation.
|
|
if (window.gBrowser) {
|
|
window.gBrowser.addProgressListener(this);
|
|
}
|
|
},
|
|
|
|
_unregisterListeners(event) {
|
|
this._unregisterListenersForWindow(event.target);
|
|
},
|
|
|
|
_unregisterListenersForWindow(window) {
|
|
this._log.trace("Removing tab listeners in window");
|
|
window.removeEventListener("unload", this._unregisterListeners);
|
|
for (let topic of this._topics) {
|
|
window.removeEventListener(topic, this.onTab);
|
|
}
|
|
if (window.gBrowser) {
|
|
window.gBrowser.removeProgressListener(this);
|
|
}
|
|
},
|
|
|
|
onStart() {
|
|
Svc.Obs.add("domwindowopened", this.asyncObserver);
|
|
for (let win of Services.wm.getEnumerator("navigator:browser")) {
|
|
this._registerListenersForWindow(win);
|
|
}
|
|
},
|
|
|
|
onStop() {
|
|
Svc.Obs.remove("domwindowopened", this.asyncObserver);
|
|
for (let win of Services.wm.getEnumerator("navigator:browser")) {
|
|
this._unregisterListenersForWindow(win);
|
|
}
|
|
},
|
|
|
|
async observe(subject, topic) {
|
|
switch (topic) {
|
|
case "domwindowopened": {
|
|
let onLoad = () => {
|
|
subject.removeEventListener("load", onLoad);
|
|
// Only register after the window is done loading to avoid unloads.
|
|
this._registerListenersForWindow(subject);
|
|
};
|
|
|
|
// Add tab listeners now that a window has opened.
|
|
subject.addEventListener("load", onLoad);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
onTab(event) {
|
|
if (event.originalTarget.linkedBrowser) {
|
|
let browser = event.originalTarget.linkedBrowser;
|
|
if (
|
|
lazy.PrivateBrowsingUtils.isBrowserPrivate(browser) &&
|
|
!lazy.PrivateBrowsingUtils.permanentPrivateBrowsing
|
|
) {
|
|
this._log.trace("Ignoring tab event from private browsing.");
|
|
return;
|
|
}
|
|
}
|
|
this._log.trace("onTab event: " + event.type);
|
|
|
|
switch (event.type) {
|
|
case "TabOpen":
|
|
/* We do not have a reliable way of checking the URI on the TabOpen
|
|
* so we will rely on the other methods (onLocationChange, getAllTabsWithEstimatedMax)
|
|
* to filter these when going through sync
|
|
*/
|
|
this.callScheduleSync(SCORE_INCREMENT_SMALL);
|
|
break;
|
|
case "TabClose": {
|
|
// If event target has `linkedBrowser`, the event target can be assumed <tab> element.
|
|
// Else, event target is assumed <browser> element, use the target as it is.
|
|
const tab = event.target.linkedBrowser || event.target;
|
|
|
|
// TabClose means the tab has already loaded and we can check the URI
|
|
// and ignore if it's a scheme we don't care about
|
|
if (lazy.TABS_FILTERED_SCHEMES.has(tab.currentURI.scheme)) {
|
|
return;
|
|
}
|
|
this.callScheduleSync(SCORE_INCREMENT_SMALL);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
// web progress listeners.
|
|
onLocationChange(webProgress, request, locationURI, flags) {
|
|
// We only care about top-level location changes. We do want location changes in the
|
|
// same document because if a page uses the `pushState()` API, they *appear* as though
|
|
// they are in the same document even if the URL changes. It also doesn't hurt to accurately
|
|
// reflect the fragment changing - so we allow LOCATION_CHANGE_SAME_DOCUMENT
|
|
if (
|
|
flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD ||
|
|
!webProgress.isTopLevel ||
|
|
!locationURI
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// We can't filter out tabs that we don't sync here, because we might be
|
|
// navigating from a tab that we *did* sync to one we do not, and that
|
|
// tab we *did* sync should no longer be synced.
|
|
this.callScheduleSync();
|
|
},
|
|
|
|
callScheduleSync(scoreIncrement) {
|
|
this.modified = true;
|
|
let { scheduler } = this.engine.service;
|
|
let delayInMs = lazy.SYNC_AFTER_DELAY_MS;
|
|
|
|
// Schedule a sync once we detect a tab change
|
|
// to ensure the server always has the most up to date tabs
|
|
if (
|
|
delayInMs > 0 &&
|
|
scheduler.numClients > 1 // Only schedule quick syncs for multi client users
|
|
) {
|
|
if (this.tabsQuickWriteTimer) {
|
|
this._log.debug(
|
|
"Detected a tab change, but a quick-write is already scheduled"
|
|
);
|
|
return;
|
|
}
|
|
this._log.debug(
|
|
"Detected a tab change: scheduling a quick-write in " + delayInMs + "ms"
|
|
);
|
|
CommonUtils.namedTimer(
|
|
() => {
|
|
this._log.trace("tab quick-sync timer fired.");
|
|
this.engine
|
|
.quickWrite()
|
|
.then(() => {
|
|
this._log.trace("tab quick-sync done.");
|
|
})
|
|
.catch(ex => {
|
|
this._log.error("tab quick-sync failed.", ex);
|
|
});
|
|
},
|
|
delayInMs,
|
|
this,
|
|
"tabsQuickWriteTimer"
|
|
);
|
|
} else if (scoreIncrement) {
|
|
this._log.debug(
|
|
"Detected a tab change, but conditions aren't met for a quick write - bumping score"
|
|
);
|
|
this.score += scoreIncrement;
|
|
} else {
|
|
this._log.debug(
|
|
"Detected a tab change, but conditions aren't met for a quick write or a score bump"
|
|
);
|
|
}
|
|
},
|
|
};
|
|
Object.setPrototypeOf(TabTracker.prototype, Tracker.prototype);
|