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

(Juraj Masiar) #1

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

(Juraj Masiar) #2

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, {
  timeout = 0,
  checkCurrentState = true,
} = {}) {
  return new Promise(async (resolve, reject) => {
    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);
    if (checkCurrentState && tab.status === browser.tabs.TabStatus.COMPLETE) {
      cleanup();
      resolve(tab);
    }
    function onTabUpdated(tabId, {status}, tab) {
      if (tabId !== targetTabId || status !== browser.tabs.TabStatus.COMPLETE) return;
      cleanup();
      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:

(Martin Giger) #3

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.

(Juraj Masiar) #4

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.