How to load framescript only once per browser?

I am very much new to this FF XUL based add-on development. While exploring, I found that FF will have multiprocess windows in coming releases and web content will no longer be accessed by Chrome process directly. So, I have written a frame script and chrome process script will asynchronously access the web content via message manager. Now, for my add-on I have used the Browser message manager, which works for only one for browser tab.

Now, this is the problem, I am facing: “How to load framescript only once per browser?”.
So, to detect that whether a framscript has already been loaded or not, I keep a “weakMap()” and put the browser object in it and check if the framescript is not loaded already for that browser. The code snippet looks like this:

var browserWeakMap = new WeakMap();		// A week browser map, which has already loaded the framescript..
function loadFrameScript() {
  var BrowserObj = gBrowser.selectedBrowser;
  var BrowserMessageManager = BrowserObj.messageManager;
  if (!browserWeakMap.has(BrowserObj) || !browserWeakMap.get(BrowserObj)) {
	BrowserMessageManager.loadFrameScript("chrome://extension/content/extn_framescript.js", true);
	browserWeakMap.set(BrowserObj, true);
  }
  BrowserMessageManager.addMessageListener("extension:MessageFromContent", handleContentScriptMessage);
  BrowserMessageManager.sendAsyncMessage("extension:MessageFromChrome", {
	FunctionName: "InitWithDocTitle"	/* Unique functionality command */
  });
}

This code works fine when I am working with one single Firefox window. But problem occurs for this scenario : “A framescript is already been loaded in a browser and browserWeakMap has already this browser object in it. But if I drag this browser tab to create a new window, a new browserWeakMap has been created. So, for new window, same browser tab loads the framescript again.

So, what should I do to keep track of browsers where the framescript has already been loaded or not keeping an eye on browser windows. We can create a new instance of the browser window by drag drop and we can merge the tabs from two windows to a single one.

Before we dive in further I just wanted to clarify. Its a process per tab. All windows are in the same process. Every tab has its own frameworker. So you dont load it once per browser. You load it once per tab (with an option of loading into new tabs opened in the future). Did you know that?

Next: Why do you want to keep track? When I first got into addons and e10s I also wanted to track them, and found out that it’s a headache with no advantage, just extra code for tracking. From you bootstrap just broadcast messages. If you keep track because you want to unload or do something on unload - dont do it - instead broad cast a message to all framescripts and wait 1sec to allow them to unload (1sec is needed for backwards compat). And if you want to respond to a specific framescript, you should have the framescript send a message to bootstrap, from there you can send a message back in response, with aMessageEvent.target.

Here is a pretty simple addon, you can ignore the message inside the framescript, thats just some details to make it work with twitter: https://github.com/Noitidart/Tweeeeeeeeeeeeeeeeeeter - in this you also see me set up a sendAsyncMessageWithCallback which makes things so ridiculously easy, check it out.

Yes. By browser I mean to say a browser Obj i.e. tab not browser window.
Yes, I am using a browser message manager i.e. my messages are restricted to one tab only on which I load the framescript.

Ok cool, so to load it once, you have to set the second argument to false in loadFrameScript:

1 Like

But in the Mozila’s blog on Frame script loading and lifetime https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Message_Manager/Frame_script_loading_and_lifetime quotes it as “If the message manager is a browser message manager, you should just always pass true here”.

That’s why I pass true here. I have seen that if we call loadFrameScript multiple times on a browser message manager it does not behave correctly. That’s why I keep a weakMap to track all the tabs on which frameScript has been loaded as the framescript is active till I close the tab.

You seem to be making this a lot more complex than it should be. What is the weakMap for? The browser message manager automatically loads your frame script, once and only once, into each browser including each new browser. If you specify false for the second parameter, it will load the frame script once and only once into the specified browser. What more do you need? Is that what you’re trying to achieve manually using the weakMap?

Perhaps for your situation you actually need a single frame script for all browsers? There is a process message manager that will do that. Or you can use a javascript code module that is imported into each browser frame script to achieve the same thing.

1 Like

Basically what happens is that, I have a XUL based add-on where for each different user action I call loadFrameScript() function described above.

I read the Mozilla’s blog on Frame script loading and lifetime https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Message_Manager/Frame_script_loading_and_lifetime . Here I found that “If the message manager is a browser message manager, you should just always pass true here”. Initially my old code looks like this:

function loadFrameScript() {
  var BrowserObj = gBrowser.selectedBrowser;
  var BrowserMessageManager = BrowserObj.messageManager;
  BrowserMessageManager.loadFrameScript("chrome://extension/content/extn_framescript.js", true);
  BrowserMessageManager.addMessageListener("extension:MessageFromContent", handleContentScriptMessage);
  BrowserMessageManager.sendAsyncMessage("extension:MessageFromChrome", {
    FunctionName: "InitWithDocTitle"	/* Unique functionality command */
  });
}

But main problem with this code is that I am receiving several asynchronous messages from the framescript which are unwanted. Then I restrict the framescript loading using one weakMap such that frameScript will be loaded only once per browser tab and wrote this code.

var browserWeakMap = new WeakMap();		// A week browser map, which has already loaded the framescript..
function loadFrameScript() {
  var BrowserObj = gBrowser.selectedBrowser;
  var BrowserMessageManager = BrowserObj.messageManager;`
  if (!browserWeakMap.has(BrowserObj) || !browserWeakMap.get(BrowserObj)) {
    BrowserMessageManager.loadFrameScript("chrome://extension/content/extn_framescript.js", true);
    browserWeakMap.set(BrowserObj, true);
  }
  BrowserMessageManager.addMessageListener("extension:MessageFromContent", handleContentScriptMessage);
  BrowserMessageManager.sendAsyncMessage("extension:MessageFromChrome", {
    FunctionName: "InitWithDocTitle"	/* Unique functionality command */
  });
}

So, If loaded once then we won’t call loadFrameScript of messageManager again. But here also, previous problem persists if I create one window by dragging one tab on which framescript is already loaded - I am receiving several unwanted asynchronous calls.

But, thanks to @noitidart @Lithopsian, setting the second parameter to false resolves this issue. Mainly Mozilla’s official documentation creates the confusion that I have quoted earlier.

A far better idea is to load one frame script into every browser, then send it messages to do whatever action you are interested in. You can define as many messages as you want, they are just strings, but it is a good idea to always prefix them with something unique so that they don’t accidentally get mixed up between addons.

Trying to load frame script on demand is, apart from being excessively complex and difficult to control, likely to fail due to timing issues. Specifically, if you set the second parameter to false on a browser message manager, there are conditions where it will not be loaded at all, with almost no way to detect or correct it. Hence the instruction to always set the second parameter to true for a browser message manager.

Thanks for the information. But if I do this, I am facing following issue:

If I call loadFrameScript on an already loaded browser tab, I receive multiple wanted calls asynchronously from the framescript. Is there a way other than setting second parameter false?

There may be confusion here about which message manager to use to load the frame script and which to use to send the messages.

There are three message managers (in the chrome process): the global message manager, the window message manager, and the browser message manager. Each one can load frame scripts and each one can send messages, but there are important differences.

The global message manager loads frame scripts into every browser in every window. This is normally the simplest thing to do, although for an XUL addon you have to find a way to do it only once (not once per window!). It is known as a broadcaster because when it sends a message (broadcastAsyncMessage), the message is received by every frame script. Sometimes you want to do this, sometimes not.

The window message manager loads frame scripts into every browser in one window. This may be a convenient way to get a frame script into every browser when you don’t have a singleton, because you just call window.messageManager.loadFrameScript() from each window instance. In an XUL addon, your code is automatically run once in each window. The window message manager is also a broadcaster and messages get sent to every frame script in that window, and received from every frame script in the window.

The browser message manager only loads frame scripts into that browser. You should only need to use this rarely, because it is a pain to detect each browser that needs a frame script and get it loaded at the right time to do anything useful. The browser message manager only sends messages (sendAsyncMessage) to frame scripts in one browser, and that is very often something you want to do.

So, in your case, you probably want to use the global message manager (or perhaps the window message manager, but I usually have a javascript code module or two and use the global message manager from there, automatically called only once) to load the frame script into every browser. Call it once, up front, and forget about it. Then send messages using the browser message manager for the tab you are playing with at that moment.

In content, everything is quite simple, with one message manager in each tab. It has its own namespace, its own set of global variables not visible to anyone else (unless you want them to be), and it can send messages that can be received by any of the chrome message managers. They can be distinguished by the target which will generally be the browser that sent the message, as well as the message name string.

In addition, there are process message managers, which run one per process instead of one per tab. They are useful for some activities, but probably not for you.

1 Like

Thanks @Lithopsian for your detailed input.
Finally I have switched to window message manager from browser message manager. So, the multiple browser tab management is not required anymore. Also, if I drag a tab, on which we have already loaded the framescript, to create another window then it receives the async call properly. Earlier, when we user browser MM, we receive multiple unwanted call in the above scenario.

window.addEventListener("load",registerMyContextMenuListener,false);
function registerMyContextMenuListener() {
  var WindowMessageManager = window.messageManager;
  WindowMessageManager.loadFrameScript("chrome://extension/content/extn_framescript.js", true);
}

And my loadFrameScript() function has been modified accordingly, which is really simple.

function loadFrameScript() {
  var BrowserObj = gBrowser.selectedBrowser;
  var BrowserMessageManager = BrowserObj.messageManager;
  BrowserMessageManager.addMessageListener("extension:MessageFromContent", handleContentScriptMessage);
  BrowserMessageManager.sendAsyncMessage("extension:MessageFromChrome", {
    FunctionName: "InitWithDocTitle"	/* Unique functionality command */
  });
}
1 Like