After refactoring my code to ES6 modules, I’ve noticed a different behavior in execution of the main script.
The use case is simple - create new tab and send it a message:
// background.js
const tab = await browser.tabs.create({url: `my_page.html`});
await browser.tabs.sendMessage(tab.id, {type: 'openSearch'});
// my_page.js
browser.runtime.onMessage.addListener(data => {/*...*/}); // top level
Before - this was working fine, because I register onMessage
handler on the top level - when script is executed.
After migration to Modules - sending message fails with:
Error: “Could not establish connection. Receiving end does not exist.”
This is because the message handler is obviously not yet registered. And if I insert some delay after the tab creation, it will work again.
Is there an easy way to fix this? To wait for tab to be ready for messages.
EDIT:
After testing this further, I’ve found out that it’s actually not guaranteed to work at all even without modules involved. This kind of behavior works only with executeScript
when you can await for the script to run. But await browser.tabs.create
obviously won’t wait for the script to run.
(the fact that it “worked” before, is due to good fallback error handling in my app and bad error logging
that hide it )
So again, any easy way to wait for the script to load?
I can think of these:
- nasty inline solution:
while (false === await browser.tabs.sendMessage(tab.id, 'ping').catch(() => false)) {} // WARNING: possible infinite loop!
-
sending some “ready” message when the script got executed - but it feels too spaghetti code since I need to watch for the message in background script…
-
use webNavigation
or tabs.onUpdated
API - a bit too complex to do it right
So after implementing new “waiting” module using WebNavigation API I found out that it won’t fire for add-on pages in Chrome
.
But tabs.onUpdated
works fine:
export async function waitForTabLoadComplete(targetTabId: number, {
timeout = 0,
checkCurrentState = true,
} = {}): Promise<browser.tabs.Tab> {
return new Promise(async (resolve, reject) => {
console.log('wait for tab load complete');
const cleanup = () => browser.tabs.onUpdated.removeListener(onTabUpdated);
if (timeout) setTimeout(() => { cleanup(); reject(Error('timeout')); }, timeout);
// browser.tabs.onUpdated.addListener(onTabUpdated, {tabId: targetTabId, properties: ['status']}); // WARNING: only FF 61, not Chrome: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onUpdated
browser.tabs.onUpdated.addListener(onTabUpdated);
const tab = await browser.tabs.get(targetTabId);
console.warn('current tab status', tab);
if (checkCurrentState && tab.status === 'complete' && tab.url !== ABOUT_BLANK) {
cleanup();
console.log('tab already loaded', tab);
resolve(tab);
}
function onTabUpdated(tabId: number, {status}: {status?: string}, tab: browser.tabs.Tab | undefined) {
console.warn('changed tab status', tab);
// WARNING: Firefox often loads first "about:blank" blank page BEFORE loading the target page, so if we wait for "complete" status, we also have to make sure the URL is not "about:blank"
if (tabId === targetTabId && status === 'complete' && tab && tab.url !== ABOUT_BLANK) {
cleanup();
console.log('tab finished loading', arguments);
resolve(tab);
}
}
})
}
// example usage:
const tab = await browser.tabs.create({url: `my_page.html`});
await waitForTabLoadComplete(tab.id, {timeout: 1000});
await browser.tabs.sendMessage(tab.id, {type: 'openSearch'});
If somebody knows a better way, please post it here 
EDIT:
Actually it’s even more complicated! See also this bug.
The problem is that Firefox loads “about:blank” in the tab before it loads the actual page. So waiting for status “complete” needs to check also URL.
As always, the easy way to solve this is to reverse the direction of the first message, so have the content script message your bg script and trigger the things from there.
That would work as well, but it’s breaking encapsulation of the feature and mostly the code flow that starts in background script where the feature is and where the tab is being created.
This would add complexity to the code because of additional handler in background script message handler plus message sending from the target page.