Search code examples
javascriptgoogle-chrome-extensionchrome-extension-manifest-v3

How to refactor global variables when using asynchronous chrome.storage?


To remove the global variables used in a persistent MV2 background script when migrating to an MV2 event page or MV3 service worker, all the guides I've found have just given an example of replacing a single global variable with a few lines of setting and then getting using chrome.storage, but it's still not clear to me how it can be used in a bit more complicated scenario.

For instance:

const activatedTabs = [];
let lastActiveTabInfo;
chrome.tabs.onActivated.addListener((activeInfo) => {
  if (activatedTabs.length === 0) {
    activatedTabs.push(activeInfo.tabId);
    lastActiveTabInfo = activeInfo;
  }
}

How could the snippet above be refactored to use chrome.storage and remove the global variables?


Solution

  • The number of variables in the state doesn't change the approach:

    1. read the state on the start of the script
    2. save the state on change

    You can use global variables as long as you await for their initialization.

    For small data (1MB total) use chrome.storage.session, which is in-memory i.e. it doesn't write to disk, otherwise use chrome.storage.local. Both can only store JSON-compatible types i.e. string, number, boolean, null, arrays/objects of such types. There's also IndexedDB for Blob or Uint8Array.

    Separate variables

    let activatedTabs;
    let lastActiveTabInfo;
    let busy = chrome.storage.session.get().then(data => {
      activatedTabs = data.activatedTabs || [];
      lastActiveTabInfo = data.lastActiveTabInfo;
      busy = null;
    });
    const saveState = () => chrome.storage.session.set({
      activatedTabs,
      lastActiveTabInfo,
    });
    
    chrome.tabs.onActivated.addListener(async info => {
      if (!activatedTabs.length) {
        if (busy) await busy;
        activatedTabs.push(info.tabId);
        lastActiveTabInfo = info;
        await saveState();
      }
    });
    

    Single object with properties

    const state = {
      activatedTabs: [],
      lastActiveTabInfo: null,
    };
    const saveState = () => chrome.storage.session.set({ state });
    let busy = chrome.storage.session.get('state').then(data => {
      Object.assign(state, data.state);
      busy = null;
    });
    
    chrome.tabs.onActivated.addListener(async info => {
      if (!state.activatedTabs.length) {
        if (busy) await busy;
        state.activatedTabs.push(info.tabId);
        state.lastActiveTabInfo = info;
        await saveState();
      }
    });
    

    If you change storage outside the background script

    ...then you'll need to subscribe to chrome.storage.onChanged event.

    Notes

    Note that if you subscribe to frequent events like tabs.onActivated, your service worker may restart hundreds of times a day, which wastes much more resources than keeping an idle persistent background page. The Chromium team ignores this problem, but you shouldn't, and luckily there's a way to reduce the number of restarts by prolonging the SW lifetime. You still need to read/save the state as shown.