615 lines
21 KiB
JavaScript
615 lines
21 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/.
|
|
*/
|
|
'use strict';
|
|
|
|
import { SequenceMatcher } from '/extlib/diff.js';
|
|
|
|
import {
|
|
log as internalLogger,
|
|
dumpTab,
|
|
wait,
|
|
mapAndFilter,
|
|
configs
|
|
} from '/common/common.js';
|
|
import * as ApiTabs from '/common/api-tabs.js';
|
|
import * as CacheStorage from '/common/cache-storage.js';
|
|
import * as Constants from '/common/constants.js';
|
|
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
|
import * as TabsStore from '/common/tabs-store.js';
|
|
import * as TabsUpdate from '/common/tabs-update.js';
|
|
import * as UniqueId from '/common/unique-id.js';
|
|
|
|
import MetricsData from '/common/MetricsData.js';
|
|
import { Tab } from '/common/TreeItem.js';
|
|
|
|
import * as Tree from './tree.js';
|
|
|
|
function log(...args) {
|
|
internalLogger('background/background-cache', ...args);
|
|
}
|
|
|
|
const kCONTENTS_VERSION = 5;
|
|
|
|
let mActivated = false;
|
|
const mCaches = {};
|
|
|
|
export function activate() {
|
|
mActivated = true;
|
|
configs.$addObserver(onConfigChange);
|
|
|
|
if (!configs.persistCachedTree) {
|
|
// clear obsolete cache
|
|
browser.windows.getAll().then(windows => {
|
|
for (const win of windows) {
|
|
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_TABS).catch(ApiTabs.createErrorSuppressor());
|
|
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY).catch(ApiTabs.createErrorSuppressor());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
// ===================================================================
|
|
// restoring tabs from cache
|
|
// ===================================================================
|
|
|
|
export async function restoreWindowFromEffectiveWindowCache(windowId, options = {}) {
|
|
MetricsData.add('restoreWindowFromEffectiveWindowCache: start');
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId} start`);
|
|
const owner = options.owner || getWindowCacheOwner(windowId);
|
|
if (!owner) {
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId} fail: no owner`);
|
|
return false;
|
|
}
|
|
cancelReservedCacheTree(windowId); // prevent to break cache before loading
|
|
const tabs = options.tabs || await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler());
|
|
if (configs.debug)
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId} tabs: `, () => tabs.map(dumpTab));
|
|
const actualSignature = getWindowSignature(tabs);
|
|
let cache = options.caches?.get(`window-${owner.windowId}`) || await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: window cache', getWindowCache(owner, Constants.kWINDOW_STATE_CACHED_TABS));
|
|
if (!cache) {
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId} fail: no cache`);
|
|
return false;
|
|
}
|
|
const promisedPermanentStates = Promise.all(tabs.map(tab => Tab.get(tab.id).$TST.getPermanentStates())); // don't await at here for better performance
|
|
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: start');
|
|
let cachedSignature = cache?.signature;
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: got from the owner `, {
|
|
owner, cachedSignature, cache
|
|
});
|
|
const signatureGeneratedFromCache = getWindowSignature(cache.tabs).join('\n');
|
|
if (cache &&
|
|
cache.tabs &&
|
|
cachedSignature &&
|
|
cachedSignature.join('\n') != signatureGeneratedFromCache) {
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: cache is broken.`, {
|
|
cachedSignature: cachedSignature.join('\n'),
|
|
signatureGeneratedFromCache
|
|
});
|
|
cache = cachedSignature = null;
|
|
TabsInternalOperation.clearCache(owner);
|
|
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: signature failed.');
|
|
}
|
|
else {
|
|
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: signature passed.');
|
|
}
|
|
if (options.ignorePinnedTabs &&
|
|
cache &&
|
|
cache.tabs &&
|
|
cachedSignature) {
|
|
cache.tabs = trimTabsCache(cache.tabs, cache.pinnedTabsCount);
|
|
cachedSignature = trimSignature(cachedSignature, cache.pinnedTabsCount);
|
|
}
|
|
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: matching actual signature of got cache');
|
|
const signatureMatchResult = matcheSignatures({
|
|
actual: actualSignature,
|
|
cached: cachedSignature
|
|
});
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: verify cache`, {
|
|
cache, actualSignature, cachedSignature,
|
|
...signatureMatchResult,
|
|
});
|
|
if (!cache ||
|
|
cache.version != kCONTENTS_VERSION ||
|
|
!signatureMatchResult.matched) {
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: no effective cache`);
|
|
TabsInternalOperation.clearCache(owner);
|
|
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: actual signature failed.');
|
|
return false;
|
|
}
|
|
MetricsData.add('restoreWindowFromEffectiveWindowCache: validity check: actual signature passed.');
|
|
cache.offset = signatureMatchResult.offset;
|
|
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: restore from cache`);
|
|
|
|
const permanentStates = await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: permanentStatus', promisedPermanentStates); // await at here for better performance
|
|
const restored = await MetricsData.addAsync('restoreWindowFromEffectiveWindowCache: restoreTabsFromCache', restoreTabsFromCache(windowId, { cache, tabs, permanentStates }));
|
|
if (restored) {
|
|
MetricsData.add(`restoreWindowFromEffectiveWindowCache: window ${windowId} succeeded`);
|
|
// Now we reload the sidebar if it is opened, because it is the easiest way
|
|
// to synchronize state of tabs completely.
|
|
log('reload sidebar for a tree restored from cache');
|
|
browser.runtime.sendMessage({
|
|
type: Constants.kCOMMAND_RELOAD,
|
|
windowId,
|
|
}).catch(ApiTabs.createErrorSuppressor());
|
|
}
|
|
else {
|
|
MetricsData.add(`restoreWindowFromEffectiveWindowCache: window ${windowId} failed`);
|
|
}
|
|
|
|
log(`restoreWindowFromEffectiveWindowCache for ${windowId}: restored = ${restored}`);
|
|
return restored;
|
|
}
|
|
|
|
function getWindowSignature(tabs) {
|
|
return tabs.map(tab => `${tab.cookieStoreId},${tab.incognito},${tab.pinned},${tab.url}`);
|
|
}
|
|
|
|
function trimSignature(signature, ignoreCount) {
|
|
if (!ignoreCount || ignoreCount < 0)
|
|
return signature;
|
|
return signature.slice(ignoreCount);
|
|
}
|
|
|
|
function trimTabsCache(cache, ignoreCount) {
|
|
if (!ignoreCount || ignoreCount < 0)
|
|
return cache;
|
|
return cache.slice(ignoreCount);
|
|
}
|
|
|
|
function matcheSignatures(signatures) {
|
|
const operations = (new SequenceMatcher(signatures.cached, signatures.actual)).operations();
|
|
log('matcheSignatures: operations ', operations);
|
|
let matched = false;
|
|
let offset = 0;
|
|
for (const operation of operations) {
|
|
const [tag, fromStart, fromEnd, toStart, toEnd] = operation;
|
|
if (tag == 'equal' &&
|
|
fromEnd - fromStart == signatures.cached.length) {
|
|
matched = true;
|
|
break;
|
|
}
|
|
offset += toEnd - toStart;
|
|
}
|
|
log('matcheSignatures: ', { matched, offset });
|
|
return { matched, offset };
|
|
}
|
|
|
|
|
|
async function restoreTabsFromCache(windowId, params = {}) {
|
|
if (!params.cache ||
|
|
params.cache.version != kCONTENTS_VERSION)
|
|
return false;
|
|
|
|
return (await restoreTabsFromCacheInternal({
|
|
windowId,
|
|
tabs: params.tabs,
|
|
permanentStates: params.permanentStates,
|
|
offset: params.cache.offset || 0,
|
|
cache: params.cache.tabs
|
|
})).length > 0;
|
|
}
|
|
|
|
async function restoreTabsFromCacheInternal(params) {
|
|
MetricsData.add('restoreTabsFromCacheInternal: start');
|
|
log(`restoreTabsFromCacheInternal: restore tabs for ${params.windowId} from cache`);
|
|
const offset = params.offset || 0;
|
|
const win = TabsStore.windows.get(params.windowId);
|
|
const tabs = params.tabs.slice(offset).map(tab => Tab.get(tab.id));
|
|
if (offset > 0 &&
|
|
tabs.length <= offset) {
|
|
log('restoreTabsFromCacheInternal: missing window');
|
|
return [];
|
|
}
|
|
log(`restoreTabsFromCacheInternal: there is ${win.tabs.size} tabs`);
|
|
if (params.cache.length != tabs.length) {
|
|
log('restoreTabsFromCacheInternal: Mismatched number of restored tabs?');
|
|
return [];
|
|
}
|
|
try {
|
|
await MetricsData.addAsync('rebuildAll: fixupTabsRestoredFromCache', fixupTabsRestoredFromCache(tabs, params.permanentStates, params.cache));
|
|
}
|
|
catch(e) {
|
|
log(String(e), e.stack);
|
|
throw e;
|
|
}
|
|
log('restoreTabsFromCacheInternal: done');
|
|
if (configs.debug)
|
|
Tab.dumpAll();
|
|
return tabs;
|
|
}
|
|
|
|
async function fixupTabsRestoredFromCache(tabs, permanentStates, cachedTabs) {
|
|
MetricsData.add('fixupTabsRestoredFromCache: start');
|
|
if (tabs.length != cachedTabs.length)
|
|
throw new Error(`fixupTabsRestoredFromCache: Mismatched number of tabs restored from cache, tabs=${tabs.length}, cachedTabs=${cachedTabs.length}`);
|
|
log('fixupTabsRestoredFromCache start ', () => ({ tabs: tabs.map(dumpTab), cachedTabs }));
|
|
const idMap = new Map();
|
|
let remappedCount = 0;
|
|
// step 1: build a map from old id to new id
|
|
tabs = tabs.map((tab, index) => {
|
|
const cachedTab = cachedTabs[index];
|
|
const oldId = cachedTab.id;
|
|
tab = Tab.get(tab.id);
|
|
log(`fixupTabsRestoredFromCache: remap ${oldId} => ${tab.id}`);
|
|
idMap.set(oldId, tab);
|
|
if (oldId != tab.id)
|
|
remappedCount++;
|
|
return tab;
|
|
});
|
|
if (remappedCount && remappedCount < tabs.length)
|
|
throw new Error(`fixupTabsRestoredFromCache: not a window restoration, only ${remappedCount} tab(s) are restored (maybe restoration of closed tabs)`);
|
|
MetricsData.add('fixupTabsRestoredFromCache: step 1 done.');
|
|
// step 2: restore information of tabs
|
|
// Do this from bottom to top, to reduce post operations for modified trees.
|
|
// (Attaching a tab to an existing tree will trigger "update" task for
|
|
// existing ancestors, but attaching existing subtree to a solo tab won't
|
|
// trigger such tasks.)
|
|
// See also: https://github.com/piroor/treestyletab/issues/2278#issuecomment-519387792
|
|
for (let i = tabs.length - 1; i > -1; i--) {
|
|
fixupTabRestoredFromCache(tabs[i], permanentStates[i], cachedTabs[i], idMap);
|
|
}
|
|
// step 3: restore collapsed/expanded state of tabs and finalize the
|
|
// restoration process
|
|
// Do this from top to bottom, because a tab can be placed under an
|
|
// expanded parent but the parent can be placed under a collapsed parent.
|
|
for (const tab of tabs) {
|
|
fixupTabRestoredFromCachePostProcess(tab);
|
|
}
|
|
MetricsData.add('fixupTabsRestoredFromCache: step 2 done.');
|
|
}
|
|
|
|
function fixupTabRestoredFromCache(tab, permanentStates, cachedTab, idMap) {
|
|
tab.$TST.clear();
|
|
const tabStates = new Set([...cachedTab.$TST.states, ...permanentStates]);
|
|
for (const state of Constants.kTAB_TEMPORARY_STATES) {
|
|
tabStates.delete(state);
|
|
}
|
|
tab.$TST.states = tabStates;
|
|
tab.$TST.attributes = cachedTab.$TST.attributes;
|
|
|
|
log('fixupTabRestoredFromCache children: ', cachedTab.$TST.childIds);
|
|
const childIds = mapAndFilter(cachedTab.$TST.childIds, oldId => {
|
|
const tab = idMap.get(oldId);
|
|
return tab?.id || undefined;
|
|
});
|
|
tab.$TST.children = childIds;
|
|
if (childIds.length > 0)
|
|
tab.$TST.setAttribute(Constants.kCHILDREN, `|${childIds.join('|')}|`);
|
|
else
|
|
tab.$TST.removeAttribute(Constants.kCHILDREN);
|
|
log('fixupTabRestoredFromCache children: => ', tab.$TST.childIds);
|
|
|
|
log('fixupTabRestoredFromCache parent: ', cachedTab.$TST.parentId);
|
|
const parentTab = idMap.get(cachedTab.$TST.parentId) || null;
|
|
tab.$TST.parent = parentTab;
|
|
if (parentTab)
|
|
tab.$TST.setAttribute(Constants.kPARENT, parentTab.id);
|
|
else
|
|
tab.$TST.removeAttribute(Constants.kPARENT);
|
|
log('fixupTabRestoredFromCache parent: => ', tab.$TST.parentId);
|
|
|
|
if (tab.discarded) {
|
|
tab.$TST.addState(Constants.kTAB_STATE_PENDING);
|
|
}
|
|
|
|
tab.$TST.temporaryMetadata.set('treeStructureAlreadyRestoredFromSessionData', true);
|
|
}
|
|
|
|
function fixupTabRestoredFromCachePostProcess(tab) {
|
|
const parentTab = tab.$TST.parent;
|
|
if (parentTab &&
|
|
(parentTab.$TST.collapsed ||
|
|
parentTab.$TST.subtreeCollapsed)) {
|
|
tab.$TST.addState(Constants.kTAB_STATE_COLLAPSED);
|
|
tab.$TST.addState(Constants.kTAB_STATE_COLLAPSED_DONE);
|
|
}
|
|
else {
|
|
tab.$TST.removeState(Constants.kTAB_STATE_COLLAPSED);
|
|
tab.$TST.removeState(Constants.kTAB_STATE_COLLAPSED_DONE);
|
|
}
|
|
|
|
TabsStore.updateIndexesForTab(tab);
|
|
TabsUpdate.updateTab(tab, tab, { forceApply: true, onlyApply: true });
|
|
}
|
|
|
|
|
|
// ===================================================================
|
|
// updating cache
|
|
// ===================================================================
|
|
|
|
async function updateWindowCache(owner, key, value) {
|
|
if (!owner)
|
|
return;
|
|
|
|
if (configs.persistCachedTree) {
|
|
try {
|
|
if (value)
|
|
await CacheStorage.setValue({
|
|
windowId: owner.windowId,
|
|
key,
|
|
value,
|
|
store: CacheStorage.BACKGROUND,
|
|
});
|
|
else
|
|
await CacheStorage.deleteValue({
|
|
windowId: owner.windowId,
|
|
key,
|
|
store: CacheStorage.BACKGROUND,
|
|
});
|
|
return;
|
|
}
|
|
catch(error) {
|
|
console.log(`BackgroundCache.updateWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error);
|
|
}
|
|
}
|
|
|
|
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
|
|
if (value)
|
|
mCaches[storageKey] = value;
|
|
else
|
|
delete mCaches[storageKey];
|
|
}
|
|
|
|
export function markWindowCacheDirtyFromTab(tab, akey) {
|
|
const win = TabsStore.windows.get(tab.windowId);
|
|
if (!win) // the window may be closed
|
|
return;
|
|
if (win.markWindowCacheDirtyFromTabTimeout)
|
|
clearTimeout(win.markWindowCacheDirtyFromTabTimeout);
|
|
win.markWindowCacheDirtyFromTabTimeout = setTimeout(() => {
|
|
win.markWindowCacheDirtyFromTabTimeout = null;
|
|
updateWindowCache(win.lastWindowCacheOwner, akey, true);
|
|
}, 100);
|
|
}
|
|
|
|
async function getWindowCache(owner, key) {
|
|
if (configs.persistCachedTree) {
|
|
try {
|
|
const value = await CacheStorage.getValue({
|
|
windowId: owner.windowId,
|
|
key,
|
|
store: CacheStorage.BACKGROUND,
|
|
});
|
|
return value;
|
|
}
|
|
catch(error) {
|
|
console.log(`BackgroundCache.getWindowCache for ${owner.windowId}/${key} failed: `, error.message, error.stack, error);
|
|
}
|
|
}
|
|
|
|
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
|
|
return mCaches[storageKey];
|
|
}
|
|
|
|
function getWindowCacheOwner(windowId) {
|
|
const tab = Tab.getFirstTab(windowId);
|
|
if (!tab)
|
|
return null;
|
|
return {
|
|
id: tab.id,
|
|
windowId: tab.windowId
|
|
};
|
|
}
|
|
|
|
export async function reserveToCacheTree(windowId, trigger) {
|
|
if (!mActivated ||
|
|
!configs.useCachedTree)
|
|
return;
|
|
|
|
const win = TabsStore.windows.get(windowId);
|
|
if (!win)
|
|
return;
|
|
|
|
// If there is any opening (but not resolved its unique id yet) tab,
|
|
// we are possibly restoring tabs. To avoid cache breakage before
|
|
// restoration, we must wait until we know whether there is any other
|
|
// restoring tab or not.
|
|
if (Tab.needToWaitTracked(windowId))
|
|
await Tab.waitUntilTrackedAll(windowId);
|
|
|
|
if (win.promisedAllTabsRestored) // not restored yet
|
|
return;
|
|
|
|
if (!trigger && configs.debug)
|
|
trigger = new Error().stack;
|
|
|
|
log('reserveToCacheTree for window ', windowId, trigger);
|
|
TabsInternalOperation.clearCache(win.lastWindowCacheOwner);
|
|
|
|
if (trigger)
|
|
reserveToCacheTree.triggers.add(trigger);
|
|
|
|
if (win.waitingToCacheTree)
|
|
clearTimeout(win.waitingToCacheTree);
|
|
win.waitingToCacheTree = setTimeout(() => {
|
|
const triggers = [...reserveToCacheTree.triggers];
|
|
reserveToCacheTree.triggers.clear();
|
|
cacheTree(windowId, triggers);
|
|
}, 500);
|
|
}
|
|
reserveToCacheTree.triggers = new Set();
|
|
|
|
function cancelReservedCacheTree(windowId) {
|
|
const win = TabsStore.windows.get(windowId);
|
|
if (win?.waitingToCacheTree) {
|
|
clearTimeout(win.waitingToCacheTree);
|
|
delete win.waitingToCacheTree;
|
|
}
|
|
}
|
|
|
|
async function cacheTree(windowId, triggers) {
|
|
if (Tab.needToWaitTracked(windowId))
|
|
await Tab.waitUntilTrackedAll(windowId);
|
|
const win = TabsStore.windows.get(windowId);
|
|
if (!win ||
|
|
!configs.useCachedTree)
|
|
return;
|
|
const signature = getWindowSignature(Tab.getAllTabs(windowId));
|
|
if (win.promisedAllTabsRestored) // not restored yet
|
|
return;
|
|
//log('save cache for ', windowId);
|
|
win.lastWindowCacheOwner = getWindowCacheOwner(windowId);
|
|
if (!win.lastWindowCacheOwner)
|
|
return;
|
|
const firstTab = Tab.getFirstTab(windowId);
|
|
if (firstTab.incognito) { // never save cache for incognito windows
|
|
updateWindowCache(win.lastWindowCacheOwner, Constants.kWINDOW_STATE_CACHED_TABS, null);
|
|
return;
|
|
}
|
|
log('cacheTree for window ', windowId, triggers/*{ stack: configs.debug && new Error().stack }*/);
|
|
updateWindowCache(win.lastWindowCacheOwner, Constants.kWINDOW_STATE_CACHED_TABS, {
|
|
version: kCONTENTS_VERSION,
|
|
tabs: TabsStore.windows.get(windowId).export(true).tabs,
|
|
pinnedTabsCount: Tab.getPinnedTabs(windowId).length,
|
|
signature
|
|
});
|
|
}
|
|
|
|
|
|
// update cache on events
|
|
|
|
Tab.onCreated.addListener((tab, _info = {}) => {
|
|
if (!tab.$TST.previousTab) { // it is a new cache owner
|
|
const win = TabsStore.windows.get(tab.windowId);
|
|
if (win.lastWindowCacheOwner)
|
|
TabsInternalOperation.clearCache(win.lastWindowCacheOwner);
|
|
}
|
|
reserveToCacheTree(tab.windowId, 'tab created');
|
|
});
|
|
|
|
// Tree restoration for "Restore Previous Session"
|
|
Tab.onWindowRestoring.addListener(async ({ windowId, restoredCount }) => {
|
|
if (!configs.useCachedTree)
|
|
return;
|
|
|
|
log('Tabs.onWindowRestoring ', { windowId, restoredCount });
|
|
if (restoredCount == 1) {
|
|
log('Tabs.onWindowRestoring: single tab restored');
|
|
return;
|
|
}
|
|
|
|
log('Tabs.onWindowRestoring: continue ', windowId);
|
|
MetricsData.add('Tabs.onWindowRestoring restore start');
|
|
|
|
const tabs = await browser.tabs.query({ windowId }).catch(ApiTabs.createErrorHandler());
|
|
try {
|
|
await restoreWindowFromEffectiveWindowCache(windowId, {
|
|
ignorePinnedTabs: true,
|
|
owner: tabs[tabs.length - 1],
|
|
tabs
|
|
});
|
|
MetricsData.add('Tabs.onWindowRestoring restore end');
|
|
}
|
|
catch(e) {
|
|
log('Tabs.onWindowRestoring: FATAL ERROR while restoring tree from cache', String(e), e.stack);
|
|
}
|
|
});
|
|
|
|
Tab.onRemoved.addListener((tab, info) => {
|
|
if (!tab.$TST.previousTab) // the tab was the cache owner
|
|
TabsInternalOperation.clearCache(tab);
|
|
wait(0).then(() => {
|
|
// "Restore Previous Session" closes some tabs at first, so we should not clear the old cache yet.
|
|
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
|
|
reserveToCacheTree(info.windowId, 'tab removed');
|
|
});
|
|
});
|
|
|
|
Tab.onMoved.addListener((tab, info) => {
|
|
if (info.fromIndex == 0) // the tab is not the cache owner anymore
|
|
TabsInternalOperation.clearCache(tab);
|
|
reserveToCacheTree(info.windowId, 'tab moved');
|
|
});
|
|
|
|
Tab.onUpdated.addListener((tab, info) => {
|
|
markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
|
|
if ('url' in info)
|
|
reserveToCacheTree(tab.windowId, 'tab updated');
|
|
});
|
|
|
|
Tab.onStateChanged.addListener((tab, state, _has) => {
|
|
if (state == Constants.kTAB_STATE_STICKY)
|
|
markWindowCacheDirtyFromTab(tab, Constants.kWINDOW_STATE_CACHED_SIDEBAR_TABS_DIRTY);
|
|
});
|
|
|
|
Tree.onSubtreeCollapsedStateChanging.addListener(tab => {
|
|
reserveToCacheTree(tab.windowId, 'subtree collapsed/expanded');
|
|
});
|
|
|
|
Tree.onAttached.addListener((tab, _info) => {
|
|
wait(0).then(() => {
|
|
// "Restore Previous Session" closes some tabs at first and it causes tree changes, so we should not clear the old cache yet.
|
|
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
|
|
reserveToCacheTree(tab.windowId, 'tab attached to tree');
|
|
});
|
|
});
|
|
|
|
Tree.onDetached.addListener((tab, _info) => {
|
|
TabsInternalOperation.clearCache(tab);
|
|
wait(0).then(() => {
|
|
// "Restore Previous Session" closes some tabs at first and it causes tree changes, so we should not clear the old cache yet.
|
|
// See also: https://dxr.mozilla.org/mozilla-central/rev/5be384bcf00191f97d32b4ac3ecd1b85ec7b18e1/browser/components/sessionstore/SessionStore.jsm#3053
|
|
reserveToCacheTree(tab.windowId, 'tab detached from tree');
|
|
});
|
|
});
|
|
|
|
Tab.onPinned.addListener(tab => {
|
|
reserveToCacheTree(tab.windowId, 'tab pinned');
|
|
});
|
|
|
|
Tab.onUnpinned.addListener(tab => {
|
|
if (tab.$TST.previousTab) // the tab was the cache owner
|
|
TabsInternalOperation.clearCache(tab);
|
|
reserveToCacheTree(tab.windowId, 'tab unpinned');
|
|
});
|
|
|
|
Tab.onShown.addListener(tab => {
|
|
reserveToCacheTree(tab.windowId, 'tab shown');
|
|
});
|
|
|
|
Tab.onHidden.addListener(tab => {
|
|
reserveToCacheTree(tab.windowId, 'tab hidden');
|
|
});
|
|
|
|
browser.windows.onRemoved.addListener(async windowId => {
|
|
try {
|
|
CacheStorage.clearForWindow(windowId);
|
|
}
|
|
catch(_error) {
|
|
}
|
|
|
|
const storageKeyPart = `Cache-${await UniqueId.ensureWindowId(windowId)}-`;
|
|
for (const key in mCaches) {
|
|
if (key.includes(storageKeyPart))
|
|
delete mCaches[key];
|
|
}
|
|
});
|
|
|
|
function onConfigChange(key) {
|
|
switch (key) {
|
|
case 'useCachedTree':
|
|
case 'persistCachedTree':
|
|
browser.windows.getAll({
|
|
populate: true,
|
|
windowTypes: ['normal']
|
|
}).then(windows => {
|
|
for (const win of windows) {
|
|
const owner = win.tabs[win.tabs.length - 1];
|
|
if (configs[key]) {
|
|
reserveToCacheTree(win.id, 'config change');
|
|
}
|
|
else {
|
|
TabsInternalOperation.clearCache(owner);
|
|
location.reload();
|
|
}
|
|
}
|
|
}).catch(ApiTabs.createErrorSuppressor());
|
|
break;
|
|
}
|
|
}
|