So I'm in the process of migrating from an MV2(persistent background page) extension to an MV3(non-persistent service worker). Now we cant use global variables to store any kind of state(Eg let's say a click counter). So according to official migration docs of google in order to tackle the problem we should use chrome Storage API as our single source of truth. But we also need to take care of "clearing" storage as we don't want global variables to persist across different browser sessions. (Eg:-Think of a case where an extension counts how many tabs are opened in a single browser "session").
I developed a promise based solution to handle state . Here is what I did .
I store a global object in chrome storage which stores all app state
I have a promise globalVariablesFetched that resolves with the global variables value and initializes globals variable after fetching data from storage
There is a function waitForGlobalsFetch that takes a fn & returns a wrapper fn .The returned fn is nothing but the callback passed in argument awaited for the globalVariablesFetched promise. All event listeners that read/write global state need to be wrapped in this.
You would notice i have done
if (isResponseCallback) return true;
This allows us to return true based on whether/not we are calling responseCallback inside listener. here responsecallback is nothing but the callback for onmessage (if any). Since we are going to call callback asynchronously(if any) we return true to inform it to keep connection alive.
Whenever Im setting/updating global state im updating both local & storage copy.
OnInstalled and onStartup i have cleared globals as its a new "session", so get rid of old state. We have a initializeGlobals function that initializes all globals with default state
So this is my system so far . It works fine now . But im not sure if it is totally correct or if I am missing something. Is this an accurate way of doing it ? Are there any better ways?
If someone who has migrated/is in the process to MV3 can share your methods.
background.js
let globals=null; //stateful global variables object
//fetch all the global variables first .
const globalVariablesFetched = new Promise((resolve,reject)=>{
chrome.storage.local.get(["globals"], (obj) => {
globals=obj.globals;
resolve(obj);
});
})
/**
* wrapper function to make callback wait till globals are fetched
* use this if a function has dependency on stateful global variables
* @param {Function} callback function which needs to await global fetch
* @param {boolean} isResponseCallback if wrapped function is an event listener which calls sendResponse callback(of sendMessage)
* @return {Function} Wrapped function that awaits globalsFetch
*/
function waitForGlobalsFetch(callback,isResponseCallback=false)
{
const wrappedFn = (...args)=>{
globalVariablesFetched.then(()=>callback(...args));
if (isResponseCallback) return true;
//return true lets us call response callback (for onMessage) asynchronously
}
return wrappedFn;
}
/**
* sets global variable in chrome storage and also updates local copy
*/
function setGlobalProperty(keyname,value)
{
if (globals) globals[keyname]=value;
globalVariablesFetched.then(()=>{
globals[keyname]=value; // update (temporary)local copy
chrome.storage.local.set({ globals }); //update storage copy
})
}
//need to (re)initialize global variables at the start of a session eg when chrome is reopened
function initializeGlobals()
{
//wait till globalsVariables are fetched to overwrite ,in order to avoid race condition
globalVariablesFetched.then(()=>{
globals = {
openedTabs: [],
clickCount:0
};
chrome.storage.local.set({ globals });
});
}
//extension refresh
chrome.runtime.onInstalled.addListener(initializeGlobals);
//chrome closed and then started again
chrome.runtime.onStartup.addListener(initializeGlobals);
Note - Sorry for long & opinion based question . I dont see much discussion/articles around state management in MV3.