'How to detect that Chrome Extension with Manifest v3 was unloaded

Our Chrome extension has both content and background scripts communicating with each other. When the plugin is updated, the background script is stopped and the content scripts start getting Error: Extension context invalidated.. In V2, we used port.onDisconnect event as described here to clean things up. But in V3, this event is also sent after 5 minutes (when the background service worker is automatically terminated). So this event now means either extension unloading (and the cleanup should be done), or just SW lifecycle event (no need to cleanup, reconnecting is fine).

So the question is, how to unambiguously determine whether the cleanup is necessary.

I've tried:

  1. chrome.management. events: onDisabled etc. But unfortunately chrome.management is undefined in my content script.
  2. Checking for chrome.runtime.id inside port.onDisconnected callback to determine the plugin is unloaded. But the id is still present at that moment.
  3. Again inside port.onDisconnected, trying to do chrome.runtime.connect() again and catching the exception. But there's no exception! The port is created successfully, but it receives neither messages nor its own onDisconnected events.
  4. Trying point 3 inside setTimeout(..., 0) and setTimeout(..., 100). The former doesn't produce exceptions either. The latter does, but it introduces a delay of questionable duration (why 100? would it work the CPU is overloaded?) and potential race conditions when other plugin functionality could try to send messages with unpredictable results. So I'd appreciate a more bullet-proof solution.


Solution 1:[1]

Thanks to wOxxOm's suggestions, I've found a solution that seems to work for now: every once in a while (<5 seconds) to disconnect the port in the content script and then reconnect again. The code looks like this:

let portToBackground: chrome.runtime.Port | undefined = openPortToBackground();

function openPortToBackground(): chrome.runtime.Port {
    const port = chrome.runtime.connect();

    const timeout = setTimeout(() => {
        console.log('reconnecting');
        portToBackground = openPortToBackground();
        port.disconnect();
    }, 2 * 60 * 1000); // 2 minutes here, just to be sure

    port.onDisconnect.addListener(() => {
        clearTimeout(timeout);
        if (port !== portToBackground) return;

        // perform the cleanup
    });

    return port;
}

export function isExtensionContextInvalidated(): boolean {
    return !portToBackground;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1