Lej77 / tab-unloader-for-tree-style-tab

19 stars 4 forks source link

All non-pinned tabs are unloaded (and hidden) on restart #30

Closed itsdani closed 3 years ago

itsdani commented 3 years ago

I mainly use the extension to manually unload and hide tabs, but when I restart the browser (or my computer) all my previously opened tabs are automatically unloaded and hidden. Sometimes I don't realize this, I forget about Tree Style Tab, and after a while I have a ton of open but hidden tabs - many of which are duplicates.

I was wondering if this is the intended behavior, or I just keep missing something on the preferences page to change the behavior?

Lej77 commented 3 years ago

When Firefox starts and restores a previous session it always restores the tabs as unloaded (except for pinned tabs). So that is not something caused by this extension. Regarding tab hiding of unloaded tabs there is indeed a setting for this extension that does this but it isn't enabled by default and requires an optional permission that must be granted to the extension, so it can't really happen by mistake because of any bugs in my code. You could have enabled it and forgotten about it I guess. This feature is under the Tab Hiding header in the extension's option page so it should be quite easy to find.

itsdani commented 3 years ago

Yes, I know about the hiding feature, and it is intended, I just didn't want to unload all tabs on startup. Thanks for pointing me in the right direction with this, I didn't suspect Firefox unloads tabs except if low on memory.

Do you know if I can prevent it from doing this somehow?

For the record, if anyone comes across this, I have tried to set browser.sessionstore.restore_on_demand and browser.sessionstore.restore_tabs_lazily to false, but setting either of these to false results in restoring all the previously unloaded tabs on startup.

What boggles my mind is that in the first few seconds Firefox appears to do just the thing I'd like it to do, but after that either loads all of my hidden tabs or hides all my open tabs and I can't make it stop doing that.

Lej77 commented 3 years ago

Unfortunately, I don't know of a way to change Firefox behavior around this. I also don't think the tabs actually are loaded correctly at startup like it "appears". I think what is happening there is that Tree Style Tab caches some of its state in the session and uses that to quickly load its sidebar before updating the sidebar with more up to date information. So its probably Tree Style Tab misleading you rather than Firefox actually tracking which tabs were loaded in the last session.

Simple addon to persist loaded tabs at restart

One way to solve you problem would be to write a small extension that tracks which tabs are loaded and then restores them when the browser is restarted. Uploading or signing a Firefox extension is also free so it shouldn't be too hard.

You would need a manifest.json file (API docs):

{
  "manifest_version": 2,
  "name": "Persist loaded tabs",
  "version": "1.0",
  "default_locale": "en",
  "applications": {
    "gecko": {
      "strict_min_version": "61.0"
    }
  },
  "permissions": [
    "sessions"
  ],
  "background": {
    "scripts": ["background.js"]
  }
}

and a background.js file (API docs):

const sessionKey = 'shouldBeLoaded';

// Reloading unloaded tabs makes them lose their titles (Firefox bug?)
// So we just make them the active tab instead if this workaround is enabled:
const useActiveTabInsteadOfReload = true;
// Can try to reload tab's twice (this might fix the issue?)
const useDoubleReload = true;

const persistLoadStateForReOpenedTabs = true;

// Handle startup
// Permission: no permission needed
// Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onStartup
// Min Firefox version: 52
browser.runtime.onStartup.addListener(async () => {
    try {
        // Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/query
        // Permission: none needed (tabs permission is only needed for url/title access).
        // Min Firefox version: 45
        const unloadedTabs = await browser.tabs.query({ discarded: true });
        await restoreTabs(unloadedTabs);
    } catch (error) {
        console.error('Failed to load tabs that were loaded in last session');
    }
});

/**
 * Restore the specified tabs. (Make them loaded.) This will only do something
 * to tabs that correctly have their session data set.
 *
 * @param {{id: number, windowId: number,}[]} tabs Tabs to restore.
 * @param {Object} Options Extra options.
 * @param {boolean} [Options.checkSessionData] Only affect tabs that have their session data set.
 */
async function restoreTabs(tabs, { checkSessionData = true } = {}) {
    try {

        if (tabs.length === 0) return;
        /** @type {{id: number}[]} */
        let activeTabs = [];
        if (useActiveTabInsteadOfReload) {
            activeTabs = await browser.tabs.query({ active: true });
        }
        /** @type {Set<number>} */
        const windowIds = new Set();
        await Promise.all(tabs.map(async (tab) => {
            try {
                if (!tab.discarded) return;
                if (checkSessionData) {
                    const wasLoaded = await wasLoadedTab(tab.id);
                    if (!wasLoaded) return;
                }
                if (useActiveTabInsteadOfReload) {
                    windowIds.add(tab.windowId);

                    // Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/update
                    // Permission: none needed (tabs permission is only needed for url/title access).
                    // Min Firefox version: 45
                    await browser.tabs.update(tab.id, { active: true });
                } else {
                    // Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/reload
                    // Permission: none needed (tabs permission is only needed for url/title access).
                    // Min Firefox version: 45
                    await browser.tabs.reload(tab.id);
                    if (useDoubleReload) {
                        // Try to reload tab again to restore title correctly:
                        await browser.tabs.reload(tab.id);
                    }
                }
            } catch (error) {
                // Tab was probably closed.
                console.error('Failed to restore tab', error);
            }
        }));
        if (useActiveTabInsteadOfReload) {
            // We changed active tab to load tabs so now change it back.
            await Promise.all(Array.from(windowIds.keys()).map(async (windowId) => {
                const previousActiveTab = activeTabs.find(tab => tab.windowId === windowId);
                if (!previousActiveTab) return;

                // Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/update
                // Permission: none needed (tabs permission is only needed for url/title access).
                // Min Firefox version: 45
                try {
                    await browser.tabs.update(previousActiveTab.id, { active: true });
                } catch (error) {
                    // Probably closed the previous active tab.
                    console.error('Failed to select the previous active tab', error);
                }
            }));
        }
    } catch (error) {
        console.error('Failed to restore tabs', error);
    }
}

// Store loaded tab info in session data:

async function forgetLoadedTab(tabId) {
    // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sessions/removeTabValue
    // Permission: "sessions"
    // Min Firefox version: 57
    try {
        await browser.sessions.removeTabValue(tabId, sessionKey);
    } catch (error) {
        console.error('Failed to forget a loaded tab', error);
    }
}
async function rememberLoadedTab(tabId) {
    // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sessions/setTabValue
    // Permission: "sessions"
    // Min Firefox version: 57
    try {
        await browser.sessions.setTabValue(tabId, sessionKey, true);
    } catch (error) {
        console.error('Failed to remember loaded tab', error);
    }
}
async function wasLoadedTab(tabId) {
    // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sessions/removeTabValue
    // Permission: "sessions"
    // Min Firefox version: 57
    try {
        // will be undefined if the value didn't exist or was removed:
        const value = await browser.sessions.getTabValue(tabId, sessionKey);
        return value === true;
    } catch (error) {
        console.error('Failed to determine if a tab should be loaded', error);
        return false;
    }
}

// Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onCreated
// Permission: none needed (tabs permission is only needed for url/title access).
// Min Firefox version: 45
browser.tabs.onCreated.addListener(async (tab) => {
    // New tab or undid a tab close or a window close.
    if (!tab.discarded) {
        await rememberLoadedTab(tab.id);
    } else {
        const wasLoaded = await wasLoadedTab(tab.id);
        if (wasLoaded) {
            if (persistLoadStateForReOpenedTabs) {
                await restoreTabs([tab], { checkSessionData: false });
            } else {
                await forgetLoadedTab(tab.id);
            }
        }
    }
});
// Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onAttached
// Permission: none needed (tabs permission is only needed for url/title access).
// Min Firefox version: 45
browser.tabs.onAttached.addListener(async (tabId) => {
    // Sessions data might be lost for tabs moved between windows?
    let tab;
    try {
        // Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/get
        // Permission: none needed (tabs permission is only needed for url/title access).
        // Min Firefox version: 45
        tab = await browser.tabs.get(tabId);
    } catch (error) {
        // Tab was probably closed.
        console.error('Failed to get info about attached tab', error);
    }
    if (!tab.discarded) {
        await rememberLoadedTab(tab.id);
    }
});
// Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onUpdated
// Permission: none needed (tabs permission is only needed for url/title access).
// Min Firefox version: 61
browser.tabs.onUpdated.addListener(async (tabId, change, tab) => {
    // Tab was loaded or unloaded
    if (!tab.discarded) {
        await rememberLoadedTab(tab.id);
    } else {
        await forgetLoadedTab(tab.id);
    }
}, {
    properties: ['discarded']
});

async function rememberAllLoadedTabs() {
    try {
        // docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/query
        // Permission: none needed (tabs permission is only needed for url/title access).
        // Min Firefox version: 45
        const loadedTabs = await browser.tabs.query({ discarded: false });
        await Promise.all(loadedTabs.map(tab => rememberLoadedTab(tab.id)));
    } catch (error) {
        console.error('Failed to remember all loaded tabs');
    }
}
// Track tabs that existed before extension started:
rememberAllLoadedTabs();

and possibly an empty messages.json file in ./_locales/en/:

{}

Then you should be able to just zip all the files and upload it to Firefox addon store. Note that I have only lightly tested the above code and while it appears to be working there might still be some bugs with it (testing code that uses session storage is a bit annoying since they need a stable id, which is automatically generated after uploading it to the Firefox store or you could manually set something). You can see how to install an unsigned extension temporarily in Tree Style Tab's readme.

deroverda commented 1 year ago

I assume this is still a problem since all tabs opens up unloaded every time I restart Firefox? A damn pity, since this extension is amazing otherwise.