Search code examples
javascriptfirefoxfirefox-addonfirefox-addon-sdk

Content script in Firefox extension (SDK) adds up event listeners


My Firefox extension is providing a JavaScript function that a website can use to access addon functionality. The website calls this function and provides two callbacks.

Website code:

function onButtonClick() {
  var callbackSuccess = function() { alert("Yeah!"); };
  var callbackError = function() { alert("Oh no!"); };
  if (window.magicAddon) { // Check if my addon is installed
    magicAddon.doStuff(callbackSuccess, callbackError);
  }
}

Content Script:

unsafeWindow.magicAddon = {
  doStuff: function(callbackSuccess, callbackError) {
    // Bind the two callbacks to events. The addon will fire one of them
    self.port.on("doStuffSuccess", callbackSuccess);
    self.port.on("doStuffError", callbackError);

    // Fire the event that lets the addon do stuff
    self.port.emit("doStuff");
  }
};

That works great on the first call, but the next time the website calls doStuff(), new listeners add up and alert() is executed twice. Next time three alerts, and so on.

Any idea how to elegantly avoid that listeners add up? Can I clear an event type completely?

What not worked so far:

  • Using self.port.once(..) instead, because I have two callback events: Only the one that fires back is cleared, the other one stays and adds up with the next.
  • Before registering new listeners remove the old ones with self.port.removeListener, because I don't have the old callback reference.

Problem seems similar to How to remove an event listener?, only that he uses one callback listener and therefore can use self.port.once(..).


Solution

  • You can use self.port.once and then manually remove the other callback:

    doStuff: function(callbackSuccess, callbackError) {
      // Bind the two callbacks to events. The addon will fire one of them
      self.port.once("doStuffSuccess", function() {
        callbackSuccess();
        self.port.removeListener(callbackError);
      });
      self.port.once("doStuffError", function() {
        callbackError();
        self.port.removeListener(callbackSuccess);
      });
    
      // Fire the event that lets the addon do stuff
      self.port.emit("doStuff");
    }
    

    You're in a content script, so you can't clear an event type completely, you're able to do so only in main add-on code, and using low level API.

    However, I would suggest to avoid unsafeWindow to provide this kind of functionality, because, well, it's unsafe. If you maintain your API async, you could use the postMessage pipeline between content script and pages, to do the same; and provides a separate javascript file that people can include in their website where you expose an abstraction of postMessages calls (e.g. magicAddon.doStuff()). If you want, you could also automatically inject that script from your add-on, in the websites.

    Handling this mechanism is definitely a bit more complex, but you can avoid the usage of unsafeWindow.

    You can find more about content script communication here.

    Hope it helps!

    Update: To answer at your comment, you need a variable to trace the doStuff call activity:

    doStuff: function() {
      var executing = false;
    
      return function(callbackSuccess, callbackError) {
          if (executing)
            return;
    
          executing = true;
    
          // Bind the two callbacks to events. The addon will fire one of them
          self.port.once("doStuffSuccess", function() {
            executing = false;
            callbackSuccess();
            self.port.removeListener(callbackError);
          });
          self.port.once("doStuffError", function() {
            executing = false;
    
            callbackError();
            self.port.removeListener(callbackSuccess);
          });
    
          // Fire the event that lets the addon do stuff
          self.port.emit("doStuff");
      }
    }()
    

    Note the () at the end. Basically in this way the function set at the end to doStuff, is the result of the function we initially assigned. In that way we create a closure for the doStuff method, where a executing variable is living, and keep trace if there is already a doStuff execution or not, in order to discard any other doStuff call, until is done.

    Note: even if in this case is not necessary for javascript, could be a good convention wrap that function in parenthesis, to identify that this function is 'self executing': `doStuff: (function() {...}())

    You could also use property of object of magicAddon for this job, but in that case it will be exposed.