Tabs.OnCreated event before Create.then on Android

Hi,
I’m developing an addon for real-time synchronization of open tabs between browsers.
I’ve discovered an inconsistency in the ordering of method executions between Firefox on desktop and Firefox on Android which is a bit problematic for me.

In my addon I need to be notified when the user opens a new tab, so I’m subscribing to browser.tabs.oncreated event to tell the server to create a tab on another browser. This results in creating the tab in the other browser with the usage of browser.tabs.create() call. To prevent a loop I need to detect whether the tab was created by user or programmatically by the addon (and not call the server in this case).

To achieve this, I do something like this:

var tabsCreatedByAddon = {}
browser.tabs.onCreated.addListener(function(createdTab) {
    if (!tabsCreatedBySynchronizer.hasOwnProperty(createdTab.id)) {
        // Call the server if the tab has not been created by us.
    }
});

function someCallbackCalledByServer(newTabProperties) {
    browser.tabs.create(newTabProperties).then(function(tab) {
        tabsCreatedBySynchronizer[tab.id] = true;
    });
};

On desktop browser it works fine but on Android it doesn’t because onCreated is executed before the Promise then method is executed.

For example

browser.tabs.onCreated.addListener(function(createdTab) {
    console.log("OnCreated:");
    console.log(createdTab);
});

var newTabProperties = {
  index: 1,
  url: "http://www.google.com"
};

browser.tabs.create(newTabProperties).then(function(tab) {
	  console.log("result from create: ");
	  console.log(tab);
});

On desktop it logs:

result from create:
Object { id: 1, index: 1, windowId: 0, selected: true, highlighted: true, active: true, pinned: false, status: “loading”, incognito: false, width: 1280, 5 more… }
OnCreated:
Object { id: 1, index: 1, windowId: 0, selected: true, highlighted: true, active: true, pinned: false, status: “loading”, incognito: false, width: 1280, 5 more… }

While on Android it writes:

OnCreated:
Object { id: 16, index: 1, windowId: 1, selected: true, highlighted: true, active: true, pinned: false, status: “loading”, incognito: false, width: 1024, 4 more… }
result from create:
Object { id: 16, index: 1, windowId: 1, selected: true, highlighted: true, active: true, pinned: false, status: “loading”, incognito: false, width: 1024, 4 more… }

Mozilla desktop: 52.0
Mozilla Android: 54.0b2

Is it expected behavior? If so, what’s the preferred solution to my problem? Is it possible to retrieve a tab id before onCreated event()? For now I will do a workaround in which I save a url to the dictionary before a call to create() and remove it after retrieving result.

Edit: Seems like the url is not passed to onCreated and then()… Great…

Thanks.

After thinking a little more about it… the behaviour looks normal. Though it doesn’t change the fact that there is nothing to correlate oncreated event and then() on.

Cannot correlate on index as user can create a new tab in meantime at the same position.

So far, I have made an workaround in which when I enter the callback from server I start to intercept all handlers to a dictionary instead of directly calling the server. After function passed to then() starts executing I execute all the handlers except the one for current tab.

Something like this:

var capturedEventHandlers = {};
var captureEventHandlersCount = 0;

browser.tabs.onCreated.addListener(function(createdTab) {
    if (captureEventHandlersCount == 0) {
        CallServer();
    } else {
        capturedEventHandlers[createdTab.id] = CallServer;
    }
});

function someCallbackCalledByServer(newTabProperties) {
    captureEventHandlersCount++;

    browser.tabs.create(newTabProperties).then(function(tab) {
        captureEventHandlersCount--;

        for (var tabId in capturedEventHandlers) {
            if (capturedEventHandlers.hasOwnProperty(tabId) && tabId != tab.id) {
                capturedEventHandlers[tabId]();
            }
        }

        capturedEventHandlers = {};
    }, onFailInvokeAllHandlers);
};

Is it expected behavior?

Probably not, but the browser.tabs.* API hasn’t been released in a stable version of Firefox yet, so it can’t really be expected to be stable in all regards.

If so, what’s the preferred solution to my problem?

I would suggest something like this:

const tabsCreatedByAddon = new Set;
browser.tabs.onCreated.addListener(async createdTab => {
    await sleep(500); // wait some time (*)
    if (!tabsCreatedByAddon.has(createdTab.id)) {
        // Call the server if the tab has not been created by us.
    }
});

function someCallbackCalledByServer(newTabProperties) {
    browser.tabs.create(newTabProperties).then(tab => tabsCreatedByAddon.add(tab.id);
};

const sleep = ms => new Promise(done => setTimeout(done));

(*): A timeout generally sucks, but I think it is the best solution here. You will have to experiment with the best time to choose. To long and you are wasting time, to short and you get wrong results.

One of the biggest problems with timeouts for computationally intensive tasks in single-threaded browsers is that the performance can vary a lot depending on other tasks performed by the browser. Using something like this instead of a simple setTimeout should help with that:

function waitIdleTime(time) { return new Promise(done => {
	const start = performance.now();
	if (typeof requestIdleCallback !== 'function') {
		return void setTimeout(function loop() {
			if ((time -= 5) <= 0) { done(performance.now() - start); }
			else { setTimeout(loop, 5); }
		}, 5);
	}
	requestIdleCallback(function loop(idle) { // available in FF55 and Chrome
		const left = Math.max(5, idle.timeRemaining()); time -= left;
		if (time <= 0) { done(performance.now() - start); }
		else { setTimeout(() => requestIdleCallback(loop), left + 1); }
	});
}); }

As the name suggests, it only decrements its timer while there are no active task on the thread.
I haven’t really used that function much yet, but in my experience, e.g. 150ms idle time usually pass in 250ms if there is not much else happening, but can take up to 750ms or longer when the browser is quite busy (which may also delay the task you are waiting for).