Options script fails to handle response from background script

I am trying to setup a one-off communication between the options script and the background script. The options script starts by sending a message to the background script which then calls a reset() function. Once the reset() function has completed its task, it sends a response back to the options script. The options script is then ready to perform a final task.

Unfortunately, I keep getting the same error message:

Uncaught (in promise) TypeError: can't access property "response", message is undefined
    handleResponse moz-extension://1af36a4c-0f27-4c69-bad9-bbcff8ea5181/scripts/options.js:787
    promise callback*reset moz-extension://1af36a4c-0f27-4c69-bad9-bbcff8ea5181/scripts/options.js:783
    EventListener.handleEvent* moz-extension://1af36a4c-0f27-4c69-bad9-bbcff8ea5181/scripts/options.js:142
options.js:787:71

Line 787 is the first console.log message in the handleResponse function.

Here is part of my code:

background.js script:

// Listen for messages from the content or options script
browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
    switch (message.action) {
        case 'reset':
            try {
                // Call the reset function
                const response = await reset();
                console.log(response);
                // Send the response back to options.js
                sendResponse({ response });
            } catch (error) {
                // Handle any errors
                sendResponse({ error: error.message });
            }
            break;
…
        default:
            break;
    }
    // Make sure to return true to indicate that sendResponse will be used asynchronously
    return true;
});

async function reset() {
    if (logToConsole) {
        console.log(
            "Resetting extension's preferences and search engines as per user reset preferences."
        );
    }
    const data = await browser.storage.sync.get(null).catch((err) => {
        if (logToConsole) {
            console.error(err);
            console.log('Failed to retrieve options from storage sync.');
        }
        return;
    });
    const options = data.options;
    const forceReload = options.forceSearchEnginesReload;
    if (logToConsole) console.log(`Options:`);
    if (logToConsole) console.log(options);
    await initialiseOptionsAndSearchEngines(forceReload);
    rebuildContextMenu();
    return "resetCompleted";
}

options.js script:

function reset() {
    const sending = sendMessage('reset', null);
    sending.then(handleResponse).catch(handleError);
}

function handleResponse(message) {
    if (logToConsole) console.log(`Response from background script: ${message.response}`);
    if (message.response === 'resetCompleted') {
        restoreOptionsPage();
    }
}

function handleError(error) {
    if (logToConsole) console.error(error);
}

// Send a message to the background script
async function sendMessage(action, data) {
    await browser.runtime.sendMessage({ action: action, data: JSON.parse(JSON.stringify(data)) })
        .catch(e => {
            if (logToConsole) console.error(e);
        });
}

Don’t use async function as a onMessage handler, it will consume ALL messages from ALL contexts and your other message handlers will not fire.
Makes no sense, right? :smiley:
More info here:

It’s a common source of very hard to investigate issues.

Also, that means, you shouldn’t return “true” from the handler in ALL cases, only when “sendResponse” is actually being used.

So actually, it’s best to NOT use “sendResponse” but instead return a Promise instead for the cases when you want to send response. For the cases when result is a value like “test”, you need to return Promise.resolve('test').

1 Like

Dear Juraj,

Thank you for your quick answer. I’m not a very proficient developer and I don’t quite understand what you mean by:

“…it will consume ALL messages from ALL contexts and your other message handlers will not fire.”

My onMessage listener function includes some await code, thats why I inserted async. Some switch cases include await browser.tabs.query(...) for example.

I tried modifying the reset case to the following, but the linter complains that the break statement is unreachable and it still results in the same error.

case 'reset':
    try {
        // Call the reset function
        const response = await reset();
        console.log(response);
        // Send the response back to options.js
        return Promise.resolve({ response });
    } catch (error) {
        // Handle any errors
        return Promise.reject({ error: error.message });
    }
    break;

Make sure to read the documentation, it explains why it is a problem.

If the handler returns a Promise, the Promise result is send as a response to the process that send the message.

Async function (while amazing thing that everyone should be using) returns always a Promise.

If you combine these two facts, you will get a message handler that will always reply to the message it receives.

So if you register 2 message handlers and you send them a message, only one will process the message and reply with something or “undefined”. And the other one will NOT see the message - because it was already processed. You see the issue now? :slight_smile:

Returning a Promise in the message handler is the same as returning a TRUE and calling “sendResponse”.

1 Like

I had read the documentation several times and have been struggling to fix this issue for many many hours. I guess that I don’t always fully understand what I read. I have a tendency to use async and await as much as I can because the code is easier to read and I can’t bend my mine around Promises!

Does this mean that I have to get rid of all the await code in the listener?

I wish things could be simpler!

Async and await is great, don’t stop using that!

But in this handler, you need to compromise a bit.

First, make the callback NOT async.

Then in each of your “case” blocks return an async function call - and the function should return a Promise of the response.
Here some example from one of my addons:

type RuntimeMessageResult = Promise<unknown> | undefined | void;

browser.runtime.onMessage.addListener((data: RuntimeMessage, sender): RuntimeMessageResult => {
  switch (data.type) {
    case 'health_check': return BACKGROUND_PROMISE;    // frontend waits for health-check => and thanks to BACKGROUND_PROMISE it will wait for background script to finish init!
    // these two returns a promise:
    case 'getImages': return MainDB.withDB(db => db.getThumbnails(data.groupId));
    case 'getDbDial': return MainDB.withDB(db => db.getDbDial(data.groupId, data.url));
    // this one only processes the message, but doesn't send back reply:
    case 'broadcast': broadcastToAll(data.message); break;
  }
});

You see, the message callback is not async but the functions it calls are.
So you can still use async/await, just not in the callback, but in the functions it calls.

I understand that I need to remove async and that I can have case statements that don’t necessarily end with break. I changed the code (removed await everywhere and used promises instead) as follows, but I’m still getting the same error message:

TypeError: can't access property "response", message is undefined

I am getting the error message from options.js before “resetCompleted” is logged to the console by background.js. I may be mistaken, but my understanding is that handleResponse in the options.js script is called before a response is sent by the background.js script!

browser.runtime.onMessage.addListener((message, sender) => {
    let id = '';
    let domain = '';
    let activeTab, lastTab, activeTabIndex, tabPosition, tabs;
    switch (message.action) {
        case 'reset':
            // Call the reset function
            reset()
                .then(response => {
                    console.log(response);
                    // Send the response back to options.js
                    return Promise.resolve({ response });
                })
                .catch(error => {
                    // Handle any errors
                    return Promise.reject({ error: error.message });
                });
            break;

Did you updated all of your message handlers in all contexts?
Also, does your handler returns “undefined” for the cases which are not processed?
Make sure you add “console.log” into each of your onMessage handlers so that you see which have received the message.

Note that any value returned in the “.then()” and “.catch()” handlers are always gonna be a Promise, so no need to wrap it into new promise:

return Promise.resolve({ response });  // this is redundant
return {response} // this is enough

Also the whole “.then” block is redundant apart from the console log :slight_smile:, but I would put the log into the “reset” function instead. I hope the reset function is async :slight_smile:.

And the “.catch” is redundant too, since returning a rejected promise will reject the promise returned by the “browser.runtime.sendMessage()” so you can catch it there.

So you should just use case 'reset': return reset();.

Could you post more of your code?

Yes, reset() is async.

I ended up solving this issue by removing handleResponse in the options.js script and adding a listener as follows:

browser.runtime.onMessage.addListener(handleMessage);

// Handle incoming messages
function handleMessage(message) {
    if (logToConsole) console.log(message);
    if (message.action === 'resetCompleted') {
        restoreOptionsPage();
    }
}

Handling responses doesn’t seem to work well. I also only have one message handler in my background.js script:

/// Handle Incoming Messages
// Listen for messages from the content or options script
browser.runtime.onMessage.addListener((message, sender) => {
    let id = '';
    let domain = '';
    let activeTab, lastTab, activeTabIndex, tabPosition, tabs;
    switch (message.action) {
        case 'doSearch':
            id = message.data.id;
            if (logToConsole) console.log('Search engine id: ' + id);
            if (logToConsole) console.log(contextsearch_openSearchResultsInSidebar);
            browser.tabs.query({ active: true, currentWindow: true })
                .then((result) => {
                    activeTab = result[0];
                    if (logToConsole) console.log('Active tab url: ' + activeTab.url);
                    return browser.tabs.query({ currentWindow: true });
                })
                .then((result) => {
                    tabs = result;
                    if (logToConsole) console.log(tabs);
                    lastTab = tabs[tabs.length - 1];
                    activeTabIndex = activeTab.index;
                    if (logToConsole) console.log('Active tab index: ' + activeTabIndex);
                    if (contextsearch_multiMode === 'multiAfterLastTab') {
                        tabPosition = lastTab.index + 1;
                    } else {
                        tabPosition = activeTabIndex + 1;
                    }
                    if (id === 'multisearch') {
                        processMultiTabSearch([], tabPosition);
                        return;
                    }
                    if (contextsearch_openSearchResultsInSidebar) {
                        searchUsing(id, null);
                        return;
                    }
                    if (contextsearch_openSearchResultsInLastTab)
                        activeTabIndex = lastTab.index;
                    searchUsing(id, activeTabIndex + 1);
                });
            break;
        case 'notify':
            if (notificationsEnabled) notify(message.data);
            break;
        case 'setSelection':
            if (logToConsole) console.log(`Selected text: ${message.data}`);
            selection = message.data;
            break;
        case 'reset':
            // Call the reset function
            reset()
                .then(response => {
                    console.log(response);
                    // Send the response back to options.js
                    sendMessageToOptionsPage(response, null);
                });
            break;
        case 'setTargetUrl':
            if (message.data) {
                targetUrl = message.data;
            }
            break;
        case 'testSearchEngine':
            testSearchEngine(message.data);
            break;
        case 'saveSearchEngines':
            searchEngines = sortByIndex(message.data);
            if (logToConsole) console.log(searchEngines);
            browser.storage.local.clear()
                .then(() => {
                    if (logToConsole) console.log('Local storage cleared.');
                    return saveSearchEnginesToLocalStorage(false);
                })
                .then(() => {
                    rebuildContextMenu();
                    // continue with the rest of your code here
                })
                .catch(err => {
                    if (logToConsole) {
                        console.error(err);
                        console.log('Failed to clear local storage.');
                    }
                });
            break;
        case 'addNewSearchEngine':
            id = message.data.id;
            if (!id.startsWith("separator-")) {
                domain = getDomain(message.data.searchEngine.url);
                if (logToConsole) console.log(id, domain);
                searchEngines[id] = message.data.searchEngine;
            }
            searchEngines = sortByIndex(searchEngines);
            addNewSearchEngine(id, domain);
            break;
        case 'updateSearchOptions':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.exactMatch = message.data.exactMatch;
                    setExactMatch(options);
                    return saveOptions(options, true);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'updateDisplayFavicons':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.displayFavicons = message.data.displayFavicons;
                    setDisplayFavicons(options);
                    return saveOptions(options, true);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'updateDisableAltClick':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.disableAltClick = message.data.disableAltClick;
                    setDisableAltClick(options);
                    return saveOptions(options, false);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'updateTabMode':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.tabMode = message.data.tabMode;
                    options.tabActive = message.data.tabActive;
                    options.lastTab = message.data.lastTab;
                    options.privateMode = message.data.privateMode;
                    setTabMode(options);
                    return saveOptions(options, false);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });

            break;
        case 'updateMultiMode':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.multiMode = message.data.multiMode;
                    setMultiMode(options);
                    return saveOptions(options, false);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'updateOptionsMenuLocation':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.optionsMenuLocation = message.data.optionsMenuLocation;
                    setOptionsMenuLocation(options);
                    return saveOptions(options, true);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'updateSiteSearchSetting':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.siteSearch = message.data.siteSearch;
                    options.siteSearchUrl = message.data.siteSearchUrl;
                    setSiteSearchSetting(options);
                    return saveOptions(options, true);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'updateResetOptions':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log('Preferences retrieved from sync storage:');
                        console.log(options);
                    }
                    options.forceSearchEnginesReload = message.data.resetOptions.forceSearchEnginesReload;
                    options.resetPreferences = message.data.resetOptions.resetPreferences;
                    options.forceFaviconsReload = message.data.resetOptions.forceFaviconsReload;
                    setResetOptions(options);
                    return saveOptions(options, false);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'updateUseRegex':
            getOptions()
                .then(options => {
                    if (logToConsole) {
                        console.log(`Preferences retrieved from sync storage: ${JSON.stringify(options)}`);
                    }
                    options.useRegex = message.data.useRegex;
                    setUseRegex(options);
                    return saveOptions(options, true);
                })
                .catch(err => {
                    console.error(err);
                    // handle any errors that occur
                });
            break;
        case 'saveSearchEnginesToDisk':
            browser.downloads.download({
                url: message.data,
                saveAs: true,
                filename: 'searchEngines.json',
            });
            break;
        case 'hidePageAction':
            browser.pageAction.hide(sender.tab.id);
            break;
        case 'showPageAction':
            browser.pageAction.show(sender.tab.id);
            break;
        default:
            break;
    }
});

With the above changes, my code doesn’t produce any more errors.

That’s a huge switch! :open_mouth:
As I mentioned before, it’s best to encapsulate the “more complex” case causes into own async functions so that you don’t have to sacrifice readability.

And few more pointers:

.catch(err => {
  console.error(err);
});

// can be simplified to:
.catch(console.error);

I’ve noticed you are using “let” to define properties on top and then use them in the switch - this is not a best practice, but I think I know why you did it:

case 'addNewSearchEngine':
  id = message.data.id;
  break;

You can’t re-declare same variable called “id” in each case clause - BUT, since the variables are “block scoped”, you can use “block statement” to wrap them:

case 'addNewSearchEngine': {
  const id = message.data.id;
  break;
}

Made some code changes to the background.js script:

/// Handle Incoming Messages
// Listen for messages from the content or options script
browser.runtime.onMessage.addListener((message, sender) => {
    const id = message.data.id;

    const queryActiveTab = () => {
        return browser.tabs.query({ active: true, currentWindow: true });
    };

    const queryAllTabs = () => {
        return browser.tabs.query({ currentWindow: true });
    };

    const logSearchEngineId = () => {
        logToConsole('Search engine id: ' + id);
        logToConsole(contextsearch_openSearchResultsInSidebar);
    };

    const handleDoSearch = () => {
        let activeTab;
        queryActiveTab()
            .then((result) => {
                activeTab = result[0];
                logToConsole('Active tab url: ' + activeTab.url);
                return queryAllTabs();
            })
            .then((result) => {
                const tabs = result;
                logToConsole(tabs);
                const lastTab = tabs[tabs.length - 1];
                let activeTabIndex = activeTab.index;
                let tabPosition;
                logToConsole('Active tab index: ' + activeTabIndex);
                if (contextsearch_multiMode === 'multiAfterLastTab') {
                    tabPosition = lastTab.index + 1;
                } else {
                    tabPosition = activeTabIndex + 1;
                }
                if (id === 'multisearch') {
                    processMultiTabSearch([], tabPosition);
                    return;
                }
                if (contextsearch_openSearchResultsInSidebar) {
                    searchUsing(id, null);
                    return;
                }
                if (contextsearch_openSearchResultsInLastTab) {
                    activeTabIndex = lastTab.index;
                }
                searchUsing(id, activeTabIndex + 1);
            });
    };

    const handleReset = () => {
        reset()
            .then((response) => {
                console.log(response);
                sendMessageToOptionsPage(response, null);
            });
    };

    const handleSaveSearchEngines = (data) => {
        searchEngines = sortByIndex(data);
        logToConsole(searchEngines);
        browser.storage.local.clear()
            .then(() => {
                logToConsole('Local storage cleared.');
                return saveSearchEnginesToLocalStorage(false);
            })
            .then(() => {
                rebuildContextMenu();
            })
            .catch(console.error);
    };

    const handleAddNewSearchEngine = (data) => {
        const id = data.id;
        let domain;
        if (!id.startsWith("separator-")) {
            domain = getDomain(data.searchEngine.url);
            logToConsole(id, domain);
            searchEngines[id] = data.searchEngine;
        }
        searchEngines = sortByIndex(searchEngines);
        addNewSearchEngine(id, domain);
    };

    const handleUpdateSearchOptions = (data) => {
        getOptions()
            .then(options => {
                options.exactMatch = data.exactMatch;
                return setOptions(options, true, false);
            })
            .catch(console.error);
    };

    const handleUpdateDisplayFavicons = (data) => {
        getOptions()
            .then(options => {
                options.displayFavicons = data.displayFavicons;
                return setOptions(options, true, true);
            })
            .catch(console.error);
    };

    const handleUpdateDisableAltClick = (data) => {
        getOptions()
            .then(options => {
                options.disableAltClick = data.disableAltClick;
                return setOptions(options, true, false);
            })
            .catch(console.error);
    };

    const handleUpdateTabMode = (data) => {
        getOptions()
            .then(options => {
                options.tabMode = data.tabMode;
                options.tabActive = data.tabActive;
                options.lastTab = data.lastTab;
                options.privateMode = data.privateMode;
                return setOptions(options, true, false);
            })
            .catch(console.error);
    };

    const handleUpdateMultiMode = (data) => {
        getOptions()
            .then(options => {
                options.multiMode = data.multiMode;
                return setOptions(options, true, false);
            })
            .catch(console.error);
    };

    const handleUpdateOptionsMenuLocation = (data) => {
        getOptions()
            .then(options => {
                options.optionsMenuLocation = data.optionsMenuLocation;
                return setOptions(options, true, true);
            })
            .catch(console.error);
    };

    const handleUpdateSiteSearchSetting = (data) => {
        getOptions()
            .then(options => {
                options.siteSearch = data.siteSearch;
                options.siteSearchUrl = data.siteSearchUrl;
                return setOptions(options, true, true);
            })
            .catch(console.error);
    };

    const handleUpdateResetOptions = (data) => {
        getOptions()
            .then(options => {
                options.forceSearchEnginesReload = data.resetOptions.forceSearchEnginesReload;
                options.resetPreferences = data.resetOptions.resetPreferences;
                options.forceFaviconsReload = data.resetOptions.forceFaviconsReload;
                return setOptions(options, true, false);
            })
            .catch(console.error);
    };

    const handleUpdateUseRegex = (data) => {
        getOptions()
            .then(options => {
                options.useRegex = data.useRegex;
                return setOptions(options, true, true);
            })
            .catch(console.error);
    };

    const handleSaveSearchEnginesToDisk = (data) => {
        browser.downloads.download({
            url: data,
            saveAs: true,
            filename: 'searchEngines.json',
        });
    };

    switch (message.action) {
        case 'doSearch':
            logSearchEngineId(id);
            handleDoSearch(id);
            break;
        case 'notify':
            if (notificationsEnabled) notify(message.data);
            break;
        case 'setSelection':
            logToConsole(`Selected text: ${message.data}`);
            selection = message.data;
            break;
        case 'reset':
            handleReset();
            break;
        case 'setTargetUrl':
            if (message.data) {
                targetUrl = message.data;
            }
            break;
        case 'testSearchEngine':
            testSearchEngine(message.data);
            break;
        case 'saveSearchEngines':
            handleSaveSearchEngines(message.data);
            break;
        case 'addNewSearchEngine':
            handleAddNewSearchEngine(message.data);
            break;
        case 'updateSearchOptions':
            handleUpdateSearchOptions(message.data);
            break;
        case 'updateDisplayFavicons':
            handleUpdateDisplayFavicons(message.data);
            break;
        case 'updateDisableAltClick':
            handleUpdateDisableAltClick(message.data);
            break;
        case 'updateTabMode':
            handleUpdateTabMode(message.data);
            break;
        case 'updateMultiMode':
            handleUpdateMultiMode(message.data);
            break;
        case 'updateOptionsMenuLocation':
            handleUpdateOptionsMenuLocation(message.data);
            break;
        case 'updateSiteSearchSetting':
            handleUpdateSiteSearchSetting(message.data);
            break;
        case 'updateResetOptions':
            handleUpdateResetOptions(message.data);
            break;
        case 'updateUseRegex':
            handleUpdateUseRegex(message.data);
            break;
        case 'saveSearchEnginesToDisk':
            handleSaveSearchEnginesToDisk(message.data);
            break;
        case 'hidePageAction':
            browser.pageAction.hide(sender.tab.id);
            break;
        case 'showPageAction':
            browser.pageAction.show(sender.tab.id);
            break;
        default:
            break;
    }
});

This looks bad - the function doesn’t return any value. Also, why not use async function?
Actually all of them are bad :smiley:.

Also, why use anonymous functions assigned to constants, why not use named functions?
For example:

    const handleReset = () => {
        reset()
            .then((response) => {
                console.log(response);
                sendMessageToOptionsPage(response, null);
            });
    };

Could be:

async function handleReset() {
  const response = await reset()
  console.log(response);
  sendMessageToOptionsPage(response, null);
}
// plus these functions could be defined outside the handler, maybe even in own file (well, those more complex)

The restriction about async/await really applies ONLY to the onMessage handler.
Also I’ve noticed you no longer return any value from the handler, so you no longer send any response?
I know this may sound complicated but this is really simple and fundamental thing - if the onMessage handler returns a Promise, the promise result is send to the caller. I do this in all of my addons and it works great! So if you send a message from Options page and the background script handler will receive it and return a Promise, that promise will be returned to the Options page caller.

Try to create a simple proof of concept - add a new onMessage handler that replies to a specific message and see if it works.

Hi Juraj,

First of all, I just want to let you know that I am very grateful for your help and advice.

I abandoned the idea of sending a response because I couldn’t get it to work. Instead, I just send a new message from the background script to the options page.

I’ve also modified the background script taking your kind advice into account. Here’s the result: (it does look cleaner!)

/// Handle Incoming Messages
// Functions used to handle incoming messages
function isActive(tab) {
    return tab.active;
}

function queryAllTabs() {
    return browser.tabs.query({ currentWindow: true });
}

async function handleDoSearch(data) {
    const id = data.id;
    if (debug) console.log('Search engine id: ' + id);
    if (debug) console.log(contextsearch_openSearchResultsInSidebar);
    if (contextsearch_openSearchResultsInSidebar) {
        searchUsing(id, null);
        return;
    }
    const tabs = await queryAllTabs();
    const activeTab = tabs.filter(isActive)[0];
    const lastTab = tabs[tabs.length - 1];
    let tabPosition = activeTab.index + 1;
    if (contextsearch_multiMode === 'multiAfterLastTab') {
        tabPosition = lastTab.index + 1;
    }
    if (id === 'multisearch') {
        processMultiTabSearch([], tabPosition);
        return;
    }
    if (contextsearch_openSearchResultsInLastTab) {
        tabPosition = lastTab.index + 1;
    }
    searchUsing(id, tabPosition);
}

async function handleReset() {
    const response = await reset();
    if (debug) console.log(response);
    sendMessageToOptionsPage(response, null);
}

async function handleSaveSearchEngines(data) {
    searchEngines = sortByIndex(data);
    if (debug) console.log(searchEngines);
    await browser.storage.local.clear();
    if (debug) console.log('Local storage cleared.');
    await saveSearchEnginesToLocalStorage(false);
    rebuildContextMenu();
}

async function handleAddNewSearchEngine(data) {
    const id = data.id;
    let domain;
    if (!id.startsWith("separator-")) {
        domain = getDomain(data.searchEngine.url);
        if (debug) console.log(id, domain);
        searchEngines[id] = data.searchEngine;
    }
    searchEngines = sortByIndex(searchEngines);
    await addNewSearchEngine(id, domain);
}

async function handleUpdateSearchOptions(data) {
    const options = await getOptions();
    options.exactMatch = data.exactMatch;
    await setOptions(options, true, false);
}

async function handleUpdateDisplayFavicons(data) {
    const options = await getOptions();
    options.displayFavicons = data.displayFavicons;
    await setOptions(options, true, true);
}

async function handleUpdateDisableAltClick(data) {
    const options = await getOptions();
    options.disableAltClick = data.disableAltClick;
    await setOptions(options, true, false);
}

async function handleUpdateTabMode(data) {
    const options = await getOptions();
    options.tabMode = data.tabMode;
    options.tabActive = data.tabActive;
    options.lastTab = data.lastTab;
    options.privateMode = data.privateMode;
    await setOptions(options, true, false);
}

async function handleUpdateMultiMode(data) {
    const options = await getOptions();
    options.multiMode = data.multiMode;
    await setOptions(options, true, false);
}

async function handleUpdateOptionsMenuLocation(data) {
    const options = await getOptions();
    options.optionsMenuLocation = data.optionsMenuLocation;
    await setOptions(options, true, true);
}

async function handleUpdateSiteSearchSetting(data) {
    const options = await getOptions();
    options.siteSearch = data.siteSearch;
    options.siteSearchUrl = data.siteSearchUrl;
    await setOptions(options, true, true);
}

async function handleUpdateResetOptions(data) {
    const options = await getOptions();
    options.forceSearchEnginesReload = data.resetOptions.forceSearchEnginesReload;
    options.resetPreferences = data.resetOptions.resetPreferences;
    options.forceFaviconsReload = data.resetOptions.forceFaviconsReload;
    await setOptions(options, true, false);
}

async function handleUpdateUseRegex(data) {
    const options = await getOptions();
    options.useRegex = data.useRegex;
    await setOptions(options, true, true);
}

async function handleSaveSearchEnginesToDisk(data) {
    await browser.downloads.download({
        url: data,
        saveAs: true,
        filename: 'searchEngines.json',
    });
}

// Listen for messages from the content or options script
browser.runtime.onMessage.addListener((message, sender) => {
    const action = message.action;
    const data = message.data;
    switch (action) {
        case 'doSearch':
            handleDoSearch(data);
            break;
        case 'notify':
            if (notificationsEnabled) notify(data);
            break;
        case 'setSelection':
            if (debug) console.log(`Selected text: ${data}`);
            if (data) selection = data;
            break;
        case 'reset':
            handleReset();
            break;
        case 'setTargetUrl':
            if (data) targetUrl = data;
            break;
        case 'testSearchEngine':
            testSearchEngine(data);
            break;
        case 'saveSearchEngines':
            handleSaveSearchEngines(data);
            break;
        case 'addNewSearchEngine':
            handleAddNewSearchEngine(data);
            break;
        case 'updateSearchOptions':
            handleUpdateSearchOptions(data);
            break;
        case 'updateDisplayFavicons':
            handleUpdateDisplayFavicons(data);
            break;
        case 'updateDisableAltClick':
            handleUpdateDisableAltClick(data);
            break;
        case 'updateTabMode':
            handleUpdateTabMode(data);
            break;
        case 'updateMultiMode':
            handleUpdateMultiMode(data);
            break;
        case 'updateOptionsMenuLocation':
            handleUpdateOptionsMenuLocation(data);
            break;
        case 'updateSiteSearchSetting':
            handleUpdateSiteSearchSetting(data);
            break;
        case 'updateResetOptions':
            handleUpdateResetOptions(data);
            break;
        case 'updateUseRegex':
            handleUpdateUseRegex(data);
            break;
        case 'saveSearchEnginesToDisk':
            handleSaveSearchEnginesToDisk(data);
            break;
        case 'hidePageAction':
            browser.pageAction.hide(sender.tab.id);
            break;
        case 'showPageAction':
            browser.pageAction.show(sender.tab.id);
            break;
        default:
            break;
    }
});/// Handle Incoming Messages
// Functions used to handle incoming messages
function isActive(tab) {
    return tab.active;
}

function queryAllTabs() {
    return browser.tabs.query({ currentWindow: true });
}

async function handleDoSearch(data) {
    const id = data.id;
    if (debug) console.log('Search engine id: ' + id);
    if (debug) console.log(contextsearch_openSearchResultsInSidebar);
    if (contextsearch_openSearchResultsInSidebar) {
        searchUsing(id, null);
        return;
    }
    const tabs = await queryAllTabs();
    const activeTab = tabs.filter(isActive)[0];
    const lastTab = tabs[tabs.length - 1];
    let tabPosition = activeTab.index + 1;
    if (contextsearch_multiMode === 'multiAfterLastTab') {
        tabPosition = lastTab.index + 1;
    }
    if (id === 'multisearch') {
        processMultiTabSearch([], tabPosition);
        return;
    }
    if (contextsearch_openSearchResultsInLastTab) {
        tabPosition = lastTab.index + 1;
    }
    searchUsing(id, tabPosition);
}

async function handleReset() {
    const response = await reset();
    if (debug) console.log(response);
    sendMessageToOptionsPage(response, null);
}

async function handleSaveSearchEngines(data) {
    searchEngines = sortByIndex(data);
    if (debug) console.log(searchEngines);
    await browser.storage.local.clear();
    if (debug) console.log('Local storage cleared.');
    await saveSearchEnginesToLocalStorage(false);
    rebuildContextMenu();
}

async function handleAddNewSearchEngine(data) {
    const id = data.id;
    let domain;
    if (!id.startsWith("separator-")) {
        domain = getDomain(data.searchEngine.url);
        if (debug) console.log(id, domain);
        searchEngines[id] = data.searchEngine;
    }
    searchEngines = sortByIndex(searchEngines);
    await addNewSearchEngine(id, domain);
}

async function handleUpdateSearchOptions(data) {
    const options = await getOptions();
    options.exactMatch = data.exactMatch;
    await setOptions(options, true, false);
}

async function handleUpdateDisplayFavicons(data) {
    const options = await getOptions();
    options.displayFavicons = data.displayFavicons;
    await setOptions(options, true, true);
}

async function handleUpdateDisableAltClick(data) {
    const options = await getOptions();
    options.disableAltClick = data.disableAltClick;
    await setOptions(options, true, false);
}

async function handleUpdateTabMode(data) {
    const options = await getOptions();
    options.tabMode = data.tabMode;
    options.tabActive = data.tabActive;
    options.lastTab = data.lastTab;
    options.privateMode = data.privateMode;
    await setOptions(options, true, false);
}

async function handleUpdateMultiMode(data) {
    const options = await getOptions();
    options.multiMode = data.multiMode;
    await setOptions(options, true, false);
}

async function handleUpdateOptionsMenuLocation(data) {
    const options = await getOptions();
    options.optionsMenuLocation = data.optionsMenuLocation;
    await setOptions(options, true, true);
}

async function handleUpdateSiteSearchSetting(data) {
    const options = await getOptions();
    options.siteSearch = data.siteSearch;
    options.siteSearchUrl = data.siteSearchUrl;
    await setOptions(options, true, true);
}

async function handleUpdateResetOptions(data) {
    const options = await getOptions();
    options.forceSearchEnginesReload = data.resetOptions.forceSearchEnginesReload;
    options.resetPreferences = data.resetOptions.resetPreferences;
    options.forceFaviconsReload = data.resetOptions.forceFaviconsReload;
    await setOptions(options, true, false);
}

async function handleUpdateUseRegex(data) {
    const options = await getOptions();
    options.useRegex = data.useRegex;
    await setOptions(options, true, true);
}

async function handleSaveSearchEnginesToDisk(data) {
    await browser.downloads.download({
        url: data,
        saveAs: true,
        filename: 'searchEngines.json',
    });
}

// Listen for messages from the content or options script
browser.runtime.onMessage.addListener((message, sender) => {
    const action = message.action;
    const data = message.data;
    switch (action) {
        case 'doSearch':
            handleDoSearch(data);
            break;
        case 'notify':
            if (notificationsEnabled) notify(data);
            break;
        case 'setSelection':
            if (debug) console.log(`Selected text: ${data}`);
            if (data) selection = data;
            break;
        case 'reset':
            handleReset();
            break;
        case 'setTargetUrl':
            if (data) targetUrl = data;
            break;
        case 'testSearchEngine':
            testSearchEngine(data);
            break;
        case 'saveSearchEngines':
            handleSaveSearchEngines(data);
            break;
        case 'addNewSearchEngine':
            handleAddNewSearchEngine(data);
            break;
        case 'updateSearchOptions':
            handleUpdateSearchOptions(data);
            break;
        case 'updateDisplayFavicons':
            handleUpdateDisplayFavicons(data);
            break;
        case 'updateDisableAltClick':
            handleUpdateDisableAltClick(data);
            break;
        case 'updateTabMode':
            handleUpdateTabMode(data);
            break;
        case 'updateMultiMode':
            handleUpdateMultiMode(data);
            break;
        case 'updateOptionsMenuLocation':
            handleUpdateOptionsMenuLocation(data);
            break;
        case 'updateSiteSearchSetting':
            handleUpdateSiteSearchSetting(data);
            break;
        case 'updateResetOptions':
            handleUpdateResetOptions(data);
            break;
        case 'updateUseRegex':
            handleUpdateUseRegex(data);
            break;
        case 'saveSearchEnginesToDisk':
            handleSaveSearchEnginesToDisk(data);
            break;
        case 'hidePageAction':
            browser.pageAction.hide(sender.tab.id);
            break;
        case 'showPageAction':
            browser.pageAction.show(sender.tab.id);
            break;
        default:
            break;
    }
});

I removed the .catch(console.error); because I wasn’t sure where to place it!

1 Like

Looks better :slight_smile:.
But I’m still unhappy that sending reply doesn’t work for you… something is fishy :slight_smile:.

Try this simple proof of concept, put this into background script:

// background_script.js
browser.runtime.onMessage.addListener(message => message === 'ping' ? Promise.resolve('pong') : undefined);

And this to the options page:

// options page:
const result = await browser.runtime.sendMessage('ping');
console.log(result);  // expecting to be "pong"
1 Like