Waiting for add-on page to be ready for receiving messages

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 :slight_smile: that hide it )

So again, any easy way to wait for the script to load?
I can think of these:

  1. nasty inline solution:
while (false === await browser.tabs.sendMessage(tab.id, 'ping').catch(() => false)) {}  // WARNING: possible infinite loop!
  1. 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…

  2. 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 :smiley:.

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 :slight_smile:

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.