Mar 25 2009

The loadOverlay Dilemma

Category: Firefox, JavaScript, Pirate Questing, XULJonathan Fingland @ 4:51 am

When making Firefox extensions with modular components, it’s nice to be able to include overlays based on preferences or some other criteria at loadtime. One of the problems that quickly comes up is that sequential loadOverlay calls will fail. This bug is documented here. The solution described there, and elsewhere, is to use chained observers and a custom queue. Strangely, I never came across an implementation example of such an observer/queue combination. So… with that stunning introduction, I give you the system used in PirateQuesting 2.

The first part here is an overlay registry. Essentially, the queue portion of the solution.

piratequesting.overlayRegistry = function() {
	var overlays = [];
	var index = 0;

	function conflicts(tabid, tabpanelid, overlayFile) {
		for (var i = 0, len = overlays.length; i<len;++i)
			if ((tabid == overlays[i].getTabId() && tabid != null )  || (tabpanelid == overlays[i].getTabPanelId() && tabpanelid != null) || overlayFile == overlays[i].getOverlayFile())
				return true;
		return false;
	}

	function overlay(tabid, tabpanelid, overlayFile) {
		var tabid = tabid;
		var tabpanelid = tabpanelid;
		var overlayFile = overlayFile;
		var added = false;

		return {
			toString : function() {
				return "tabid: " + tabid + "\ntabpanelid: " + tabpanelid
						+ "\noverlayFile: " + overlayFile;
			},
			getTabId : function() {
				return tabid;
			},
			getTabPanelId : function() {
				return tabpanelid;
			},
			getOverlayFile : function() {
				return overlayFile;
			},
			getAdded : function() {
				return added;
			},
			setAdded : function(val) {
				added = !!val; // ensure boolean
			}
		}

	}

	return {
		addOverlay : function(tabid, tabpanelid, stringFile) {
			if (!conflicts(tabid, tabpanelid, stringFile))
				overlays.push(new overlay(tabid, tabpanelid, stringFile));
			else
				dump("\nOverlay conflict occurred on: " + tabid + ", " + tabpanelid + ", " + stringFile);
		},
		getOverlayByIndex : function(index) {
			return overlays[index];
		},
		getOverlayByTabId : function(tabid) {
			var ol = overlays.length;
			for (var i = 0; i < ol; i++) {
				if (overlays[i].getTabId() == tabid)
					return overlays[i];
			}
			return false;
		},
		count : function() {
			return overlays.length;
		},
		reset : function() {
			index = 0;
		},
		next : function() {
			if (index < overlays.length) {
				return overlays[index++];
			} else
				return null;
		},
		progress : function() {
			return Math.ceil(index * 100 / overlays.length);
		},
		resetAll : function() {
			this.reset();
			var nex;
			while (nex = this.next()) {
				nex.setAdded(false);
			}
			this.reset();
		}

	}
}();

Now, that is a somewhat specialized case. The tabid and tabpanelid are arguably unnecessary but have been included to prevent two modules having the same tab ids, and, more importantly, to be able to refer to the overlay by a known value, in this case the tabid.

A somewhat stripped down version might look like:

var overlayRegistry = function() {
	var overlays = [];
	var index = 0;

	function conflicts(overlayFile) {
		for (var i = 0, len = overlays.length; i<len;++i)
			if (overlayFile == overlays[i].getOverlayFile())
				return true;
		return false;
	}

	function overlay(overlayFile) {
		var overlayFile = overlayFile;
		var added = false;

		return {
			toString : function() {
				return "\noverlayFile: " + overlayFile;
			},
			getOverlayFile : function() {
				return overlayFile;
			},
			getAdded : function() {
				return added;
			},
			setAdded : function(val) {
				added = !!val; // ensure boolean
			}
		}

	}

	return {
		addOverlay : function(stringFile) {
			if (!conflicts(stringFile))
				overlays.push(new overlay(stringFile));
			else
				dump("\nOverlay conflict occurred on: " + stringFile);
		},
		getOverlayByIndex : function(index) {
			return overlays[index];
		},
		count : function() {
			return overlays.length;
		},
		reset : function() {
			index = 0;
		},
		next : function() {
			if (index < overlays.length) {
				return overlays[index++];
			} else
				return null;
		},
		progress : function() {
			return Math.ceil(index * 100 / overlays.length);
		},
		resetAll : function() {
			this.reset();
			var nex;
			while (nex = this.next()) {
				nex.setAdded(false);
			}
			this.reset();
		}

	}
}();

The overlay registry is simply an iterator-style queue. This makes walking through the items very easy for the observer (shown next) which doesn’t (and shouldn’t) really have any idea of the state of the queue. The queue, overlayRegistry, makes a number of methods available for getting basic info about the queue (size, progress, etc) for use in progress bars or the like. It also provides ways of resetting the queue. Obviously, since it’s designed to be an iterator, there are ways of getting the next item and advancing the queue.

The second part of the solution is using a chained observer. Again, the piratequesting implementation is:

function overlayObserver()
{
  this.register();
}
overlayObserver.prototype = {
  observe: function(subject, topic, data) {

  	function cleanUp() {
		sidebar.contentDocument.getElementById("pqmain_deck").selectedIndex="1";

		var mod_boxes = sidebar.contentDocument.getElementsByTagName("tabbox");
		for (var i=0,len=mod_boxes.length;i<len;++i) {
			if (hasClassName(mod_boxes[i],"moduleBox")) {
				mod_boxes[i].selectedIndex = 0;
			}
		}
  	}

  	if (topic == "xul-overlay-merged") {
		try {
			var nex = piratequesting.overlayRegistry.next();
			if (nex) {
  				sidebar.contentDocument.getElementById("pqloadprogress").value = piratequesting.overlayRegistry.progress();
  				try {
  					sidebar.contentDocument.loadOverlay(nex.getOverlayFile(),this);
	  			} catch (error) {
  					cleanUp();
  					dump("Failed to load: " + nex.getOverlayFile() + "\nReported Error " + getErrorString(error));
	  			}
  			} else {
  				cleanUp();
	  		}
  		} catch (error) { alert(getErrorString(error)); }
  	}
  },
  register: function() {
    var observerService = Components.classes["@mozilla.org/observer-service;1"]
                          .getService(Components.interfaces.nsIObserverService);
    observerService.addObserver(this, "xul-overlay-merged", false);
  },
  unregister: function() {
    var observerService = Components.classes["@mozilla.org/observer-service;1"]
                            .getService(Components.interfaces.nsIObserverService);
    observerService.removeObserver(this, "xul-overlay-merged");
  }
}

And a stripped down version would look something like this:

function overlayObserver()
{
  this.register();
}
overlayObserver.prototype = {
  observe: function(subject, topic, data) {

  	if (topic == "xul-overlay-merged") {
		try {
			var nex = overlayRegistry.next();
			if (nex) {
  				try {
  					document.loadOverlay(nex.getOverlayFile(),this);
	  			} catch (error) {
  					dump("Failed to load: " + nex.getOverlayFile() + "\nReported Error " + getErrorString(error));
	  			}
  			} else {
  				cleanUp();
	  		}
  		} catch (error) { alert(getErrorString(error)); }
  	}
  },
  register: function() {
    var observerService = Components.classes["@mozilla.org/observer-service;1"]
                          .getService(Components.interfaces.nsIObserverService);
    observerService.addObserver(this, "xul-overlay-merged", false);
  },
  unregister: function() {
    var observerService = Components.classes["@mozilla.org/observer-service;1"]
                            .getService(Components.interfaces.nsIObserverService);
    observerService.removeObserver(this, "xul-overlay-merged");
  }
}

This observer chain is started with:

overlayObserver.observe(null,"xul-overlay-merged", null);

The observer code is also fairly simple but relies on some things that were not terribly well documented. The key thing to know about loadOverlay is that it raises an xul-overlay-merged observer notification. here, but you’ll notice what’s missing. This entry, however, has that piece of info. If you’ve never used observers before, this gives a good rundown.

Sooo…. What happens? how does it work? Basically, at some point during the initialization of the sidebar (not important in this discussion), the observer chain is started and will loop as follows:

  1. receive notification of the last loadOverlay finishing
  2. get the next item in the iterator
  3. if the “next item” is null, stop
  4. load the overlay file specified by said item

And you’re done. One Note I would make is that if there are multiple extensions making use of this at the same time, you’re very likely to have problems when both extensions receive the notifications and both start loading their next items. As firefox still lacks a built-in queue for overlay loading, we’re stuck hoping that nobody else will use this at the same time.

Note: You may notice that a lot of the piratequesting code makes use of a function getErrorString(). This is a very siple function that puts all of the error info I want into a string. dump() is a function available in firefox for dumping text to the console. I am also currently working on an error logging system and will cover all of these issues in more detail when that is finished. For the time being, ignore how I handle the errors but for obvious reasons, you will want to have some kind of error handling in place.

Edit 2009/04/04:
It has since occurred to me that by using additional information, that is, subject, from the observer notification, I can reduce the chances of conflict by ensuring it only fires on the completion of it’s own loadOverlay calls.