Bug 1181930 - Refactoring: move the message broadcaster out of Webapps.jsm r=ferjm
authorFabrice Desré <fabrice@mozilla.com>
Wed, 05 Aug 2015 20:30:55 -0700
changeset 288050 939e1009ebaf93dfc56fd1ee45d4ecc0a42e4abe
parent 288049 58c131a75dd9a6a0bfaaba50fffa94c5884dd042
child 288051 e200ec9a11fbc3c930c39a613f8186e9028586c6
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersferjm
bugs1181930
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1181930 - Refactoring: move the message broadcaster out of Webapps.jsm r=ferjm
dom/apps/ImportExport.jsm
dom/apps/MessageBroadcaster.jsm
dom/apps/Webapps.jsm
dom/apps/moz.build
toolkit/devtools/server/actors/webapps.js
--- a/dom/apps/ImportExport.jsm
+++ b/dom/apps/ImportExport.jsm
@@ -8,16 +8,17 @@ const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Webapps.jsm");
+Cu.import("resource://gre/modules/MessageBroadcaster.jsm");
 
 Cu.importGlobalProperties(['File']);
 
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
   "resource://gre/modules/FileUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
@@ -470,20 +471,20 @@ this.ImportExport = {
                                                meta);
 
       // Save the app registry, and sends the various notifications.
       // TODO: stop accessing internal methods of other objects.
       yield DOMApplicationRegistry._saveApps();
 
       app = AppsUtils.cloneAppObject(meta);
       app.manifest = manifest;
-      DOMApplicationRegistry.broadcastMessage("Webapps:AddApp",
-                                              { id: meta.id, app: app });
-      DOMApplicationRegistry.broadcastMessage("Webapps:Install:Return:OK",
-                                              { app: app });
+      MessageBroadcaster.broadcastMessage("Webapps:AddApp",
+                                          { id: meta.id, app: app });
+      MessageBroadcaster.broadcastMessage("Webapps:Install:Return:OK",
+                                          { app: app });
       Services.obs.notifyObservers(null, "webapps-installed",
         JSON.stringify({ manifestURL: meta.manifestURL }));
 
     } catch(e) {
       debug("Import failed: " + e);
       if (appDir && appDir.exists()) {
         appDir.remove(true);
       }
new file mode 100644
--- /dev/null
+++ b/dom/apps/MessageBroadcaster.jsm
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Manages registration of message managers from child processes and
+// broadcasting messages to them.
+
+this.EXPORTED_SYMBOLS = ["MessageBroadcaster"];
+
+this.MessageBroadcaster = {
+  appGetter: null,
+  children: [],
+
+  init: function(aAppGetter) {
+    if (!aAppGetter || typeof aAppGetter !== "function") {
+      throw "MessageBroadcaster.init needs a function parameter";
+    }
+    this.appGetter = aAppGetter;
+  },
+
+  // We manage refcounting of listeners per message manager.
+  addMessageListener: function(aMsgNames, aApp, aMm) {
+    aMsgNames.forEach(aMsgName => {
+      let manifestURL = aApp && aApp.manifestURL;
+      if (!(aMsgName in this.children)) {
+        this.children[aMsgName] = [];
+      }
+
+      let mmFound = this.children[aMsgName].some(mmRef => {
+        if (mmRef.mm === aMm) {
+          mmRef.refCount++;
+          return true;
+        }
+        return false;
+      });
+
+      if (!mmFound) {
+        this.children[aMsgName].push({
+          mm: aMm,
+          refCount: 1
+        });
+      }
+
+      // If the state reported by the registration is outdated, update it now.
+      if (manifestURL && ((aMsgName === 'Webapps:FireEvent') ||
+          (aMsgName === 'Webapps:UpdateState'))) {
+        let app = this.appGetter(aApp.manifestURL);
+        if (app && ((aApp.installState !== app.installState) ||
+                    (aApp.downloading !== app.downloading))) {
+          debug("Got a registration from an outdated app: " +
+                manifestURL);
+          let aEvent ={
+            type: app.installState,
+            app: app,
+            manifestURL: app.manifestURL,
+            manifest: app.manifest
+          };
+          aMm.sendAsyncMessage(aMsgName, aEvent);
+        }
+      }
+    });
+  },
+
+  removeMessageListener: function(aMsgNames, aMm) {
+    if (aMsgNames.length === 1 &&
+        aMsgNames[0] === "Webapps:Internal:AllMessages") {
+      for (let msgName in this.children) {
+        let msg = this.children[msgName];
+
+        for (let mmI = msg.length - 1; mmI >= 0; mmI -= 1) {
+          let mmRef = msg[mmI];
+          if (mmRef.mm === aMm) {
+            msg.splice(mmI, 1);
+          }
+        }
+
+        if (msg.length === 0) {
+          delete this.children[msgName];
+        }
+      }
+      return;
+    }
+
+    aMsgNames.forEach(aMsgName => {
+      if (!(aMsgName in this.children)) {
+        return;
+      }
+
+      let removeIndex;
+      this.children[aMsgName].some((mmRef, index) => {
+        if (mmRef.mm === aMm) {
+          mmRef.refCount--;
+          if (mmRef.refCount === 0) {
+            removeIndex = index;
+          }
+          return true;
+        }
+        return false;
+      });
+
+      if (removeIndex) {
+        this.children[aMsgName].splice(removeIndex, 1);
+      }
+    });
+  },
+
+  // Some messages can be listened by several content processes:
+  // Webapps:AddApp
+  // Webapps:RemoveApp
+  // Webapps:Install:Return:OK
+  // Webapps:Uninstall:Return:OK
+  // Webapps:Uninstall:Broadcast:Return:OK
+  // Webapps:FireEvent
+  // Webapps:checkForUpdate:Return:OK
+  // Webapps:UpdateState
+  broadcastMessage: function(aMsgName, aContent) {
+    if (!(aMsgName in this.children)) {
+      return;
+    }
+    this.children[aMsgName].forEach((mmRef) => {
+      mmRef.mm.sendAsyncMessage(aMsgName, this.formatMessage(aContent));
+    });
+  },
+
+  formatMessage: function(aData) {
+    let msg = aData;
+    delete msg["mm"];
+    return msg;
+  },
+}
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -37,16 +37,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/AppDownloadManager.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/MessageBroadcaster.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "UserCustomizations", function() {
   let enabled = false;
   try {
     enabled = Services.prefs.getBoolPref("dom.apps.customization.enabled");
   } catch(e) {}
 
   if (enabled) {
@@ -190,17 +191,16 @@ this.DOMApplicationRegistry = {
   get kPackaged()       "packaged",
   get kHosted()         "hosted",
   get kHostedAppcache() "hosted-appcache",
   get kTrustedHosted()  "hosted-trusted",
 
   // Path to the webapps.json file where we store the registry data.
   appsFile: null,
   webapps: { },
-  children: [ ],
   allAppsLaunchable: false,
   _updateHandlers: [ ],
   _pendingUninstalls: {},
   _contentActions: new Map(),
   dirKey: DIRECTORY_NAME,
 
   init: function() {
     // Keep the messages in sync with the lazy-loading in browser.js (bug 1171013).
@@ -246,19 +246,21 @@ this.DOMApplicationRegistry = {
 
     AppDownloadManager.registerCancelFunction(this.cancelDownload.bind(this));
 
     this.appsFile = FileUtils.getFile(DIRECTORY_NAME,
                                       ["webapps", "webapps.json"], true).path;
 
     this.loadAndUpdateApps();
 
-    Langpacks.registerRegistryFunctions(this.broadcastMessage.bind(this),
+    Langpacks.registerRegistryFunctions(MessageBroadcaster.broadcastMessage.bind(MessageBroadcaster),
                                         this._appIdForManifestURL.bind(this),
                                         this.getFullAppByManifestURL.bind(this));
+
+    MessageBroadcaster.init(this.getAppByManifestURL);
   },
 
   // loads the current registry, that could be empty on first run.
   loadCurrentRegistry: function() {
     return AppsUtils.loadJSONAsync(this.appsFile).then((aData) => {
       if (!aData) {
         return;
       }
@@ -1175,103 +1177,16 @@ this.DOMApplicationRegistry = {
       cpmm = null;
       ppmm = null;
     } else if (aTopic == "memory-pressure") {
       // Clear the manifest cache on memory pressure.
       this._manifestCache = {};
     }
   },
 
-  addMessageListener: function(aMsgNames, aApp, aMm) {
-    aMsgNames.forEach(function (aMsgName) {
-      let man = aApp && aApp.manifestURL;
-      if (!(aMsgName in this.children)) {
-        this.children[aMsgName] = [];
-      }
-
-      let mmFound = this.children[aMsgName].some(function(mmRef) {
-        if (mmRef.mm === aMm) {
-          mmRef.refCount++;
-          return true;
-        }
-        return false;
-      });
-
-      if (!mmFound) {
-        this.children[aMsgName].push({
-          mm: aMm,
-          refCount: 1
-        });
-      }
-
-      // If the state reported by the registration is outdated, update it now.
-      if ((aMsgName === 'Webapps:FireEvent') ||
-          (aMsgName === 'Webapps:UpdateState')) {
-        if (man) {
-          let app = this.getAppByManifestURL(aApp.manifestURL);
-          if (app && ((aApp.installState !== app.installState) ||
-                      (aApp.downloading !== app.downloading))) {
-            debug("Got a registration from an outdated app: " +
-                  aApp.manifestURL);
-            let aEvent ={
-              type: app.installState,
-              app: app,
-              manifestURL: app.manifestURL,
-              manifest: app.manifest
-            };
-            aMm.sendAsyncMessage(aMsgName, aEvent);
-          }
-        }
-      }
-    }, this);
-  },
-
-  removeMessageListener: function(aMsgNames, aMm) {
-    if (aMsgNames.length === 1 &&
-        aMsgNames[0] === "Webapps:Internal:AllMessages") {
-      for (let msgName in this.children) {
-        let msg = this.children[msgName];
-
-        for (let mmI = msg.length - 1; mmI >= 0; mmI -= 1) {
-          let mmRef = msg[mmI];
-          if (mmRef.mm === aMm) {
-            msg.splice(mmI, 1);
-          }
-        }
-
-        if (msg.length === 0) {
-          delete this.children[msgName];
-        }
-      }
-      return;
-    }
-
-    aMsgNames.forEach(function(aMsgName) {
-      if (!(aMsgName in this.children)) {
-        return;
-      }
-
-      let removeIndex;
-      this.children[aMsgName].some(function(mmRef, index) {
-        if (mmRef.mm === aMm) {
-          mmRef.refCount--;
-          if (mmRef.refCount === 0) {
-            removeIndex = index;
-          }
-          return true;
-        }
-        return false;
-      });
-
-      if (removeIndex) {
-        this.children[aMsgName].splice(removeIndex, 1);
-      }
-    }, this);
-  },
-
   formatMessage: function(aData) {
     let msg = aData;
     delete msg["mm"];
     return msg;
   },
 
   receiveMessage: function(aMessage) {
     // nsIPrefBranch throws if pref does not exist, faster to simply write
@@ -1344,23 +1259,23 @@ this.DOMApplicationRegistry = {
         this.notifyAppsRegistryReady();
         break;
       case "Webapps:GetList":
         // GetList is special because it's synchronous. So far so well, it's the
         // only synchronous message, if we get more at some point they should get
         // this treatment also.
         return this.doGetList();
       case "child-process-shutdown":
-        this.removeMessageListener(["Webapps:Internal:AllMessages"], mm);
+        MessageBroadcaster.removeMessageListener(["Webapps:Internal:AllMessages"], mm);
         break;
       case "Webapps:RegisterForMessages":
-        this.addMessageListener(msg.messages, msg.app, mm);
+        MessageBroadcaster.addMessageListener(msg.messages, msg.app, mm);
         break;
       case "Webapps:UnregisterForMessages":
-        this.removeMessageListener(msg, mm);
+        MessageBroadcaster.removeMessageListener(msg, mm);
         break;
       default:
         processedImmediately = false;
     }
 
     if (processedImmediately) {
       return;
     }
@@ -1455,34 +1370,16 @@ this.DOMApplicationRegistry = {
       }
     });
   },
 
   getAppInfo: function getAppInfo(aAppId) {
     return AppsUtils.getAppInfo(this.webapps, aAppId);
   },
 
-  // Some messages can be listened by several content processes:
-  // Webapps:AddApp
-  // Webapps:RemoveApp
-  // Webapps:Install:Return:OK
-  // Webapps:Uninstall:Return:OK
-  // Webapps:Uninstall:Broadcast:Return:OK
-  // Webapps:FireEvent
-  // Webapps:checkForUpdate:Return:OK
-  // Webapps:UpdateState
-  broadcastMessage: function broadcastMessage(aMsgName, aContent) {
-    if (!(aMsgName in this.children)) {
-      return;
-    }
-    this.children[aMsgName].forEach((mmRef) => {
-      mmRef.mm.sendAsyncMessage(aMsgName, this.formatMessage(aContent));
-    });
-  },
-
   registerUpdateHandler: function(aHandler) {
     this._updateHandlers.push(aHandler);
   },
 
   unregisterUpdateHandler: function(aHandler) {
     let index = this._updateHandlers.indexOf(aHandler);
     if (index != -1) {
       this._updateHandlers.splice(index, 1);
@@ -1749,26 +1646,26 @@ this.DOMApplicationRegistry = {
 
     // Ensure we don't send additional errors for this download
     app.isCanceling = true;
 
     // Ensure this app can be downloaded again after canceling
     app.downloading = false;
 
     this._saveApps().then(() => {
-      this.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: {
           progress: 0,
           installState: download.previousState,
           downloading: false
         },
         error: error,
         id: app.id
       })
-      this.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL: app.manifestURL
       });
     });
     AppDownloadManager.remove(aManifestURL);
   },
 
   startDownload: Task.async(function*(aManifestURL) {
@@ -1785,21 +1682,21 @@ this.DOMApplicationRegistry = {
     if (app.downloading) {
       debug("app is already downloading. Ignoring.");
       throw new Error("APP_IS_DOWNLOADING");
     }
 
     // If the caller is trying to start a download but we have nothing to
     // download, send an error.
     if (!app.downloadAvailable) {
-      this.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         error: "NO_DOWNLOAD_AVAILABLE",
         id: app.id
       });
-      this.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL: app.manifestURL
       });
       throw new Error("NO_DOWNLOAD_AVAILABLE");
     }
 
     // First of all, we check if the download is supposed to update an
     // already installed application.
@@ -1833,22 +1730,22 @@ this.DOMApplicationRegistry = {
       } else {
         // Hosted app with no appcache, nothing to do, but we fire a
         // downloaded event.
         debug("No appcache found, sending 'downloaded' for " + aManifestURL);
         app.downloadAvailable = false;
 
         yield this._saveApps();
 
-        this.broadcastMessage("Webapps:UpdateState", {
+        MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
           app: app,
           manifest: jsonManifest,
           id: app.id
         });
-        this.broadcastMessage("Webapps:FireEvent", {
+        MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
           eventType: "downloadsuccess",
           manifestURL: aManifestURL
         });
       }
 
       return;
     }
 
@@ -1886,21 +1783,21 @@ this.DOMApplicationRegistry = {
     app = this.webapps[id];
 
     // Set state and fire events.
     app.downloading = false;
     app.downloadAvailable = false;
     app.readyToApplyDownload = true;
     app.updateTime = Date.now();
 
-    this.broadcastMessage("Webapps:UpdateState", {
+    MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
       app: app,
       id: app.id
     });
-    this.broadcastMessage("Webapps:FireEvent", {
+    MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
       eventType: "downloadsuccess",
       manifestURL: aManifestURL
     });
     if (app.installState == "pending") {
       // We restarted a failed download, apply it automatically.
       this.applyDownload(aManifestURL);
     }
   }),
@@ -2007,22 +1904,22 @@ this.DOMApplicationRegistry = {
         { manifest: newManifest,
           origin: app.origin,
           manifestURL: app.manifestURL,
           kind: app.kind },
         true);
     }
     this.updateDataStore(this.webapps[id].localId, app.origin,
                          app.manifestURL, newManifest);
-    this.broadcastMessage("Webapps:UpdateState", {
+    MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
       app: app,
       manifest: newManifest,
       id: app.id
     });
-    this.broadcastMessage("Webapps:FireEvent", {
+    MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
       eventType: "downloadapplied",
       manifestURL: app.manifestURL
     });
   }),
 
   startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) {
     debug("startOfflineCacheDownload " + aApp.id + " " + aApp.kind);
     if ((aApp.kind !== this.kHostedAppcache &&
@@ -2045,17 +1942,17 @@ this.DOMApplicationRegistry = {
       aApp.installState = "updating";
     }
 
     // We set the 'downloading' flag and update the apps registry right before
     // starting the app download/update.
     aApp.downloading = true;
     aApp.progress = 0;
     DOMApplicationRegistry._saveApps().then(() => {
-      DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         // Clear any previous errors.
         error: null,
         app: {
           downloading: true,
           installState: aApp.installState,
           progress: 0
         },
         id: aApp.id
@@ -2186,21 +2083,21 @@ this.DOMApplicationRegistry = {
         // "downloadapplied".
         let updateObserver = {
           observe: function(aSubject, aTopic, aObsData) {
             debug("onlyCheckAppCache updateSvc.checkForUpdate return for " +
                   app.manifestURL + " - event is " + aTopic);
             if (aTopic == "offline-cache-update-available") {
               app.downloadAvailable = true;
               this._saveApps().then(() => {
-                this.broadcastMessage("Webapps:UpdateState", {
+                MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
                   app: app,
                   id: app.id
                 });
-                this.broadcastMessage("Webapps:FireEvent", {
+                MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
                   eventType: "downloadavailable",
                   manifestURL: app.manifestURL,
                   requestID: aData.requestID
                 });
               });
             } else {
               sendError("NOT_UPDATABLE");
             }
@@ -2386,21 +2283,21 @@ this.DOMApplicationRegistry = {
     // event.
     aApp.downloadAvailable = true;
     aApp.downloadSize = manifest.size;
     aApp.updateManifest = aNewManifest;
     this._saveWidgetsFullPath(manifest, aApp);
 
     yield this._saveApps();
 
-    this.broadcastMessage("Webapps:UpdateState", {
+    MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
       app: aApp,
       id: aApp.id
     });
-    this.broadcastMessage("Webapps:FireEvent", {
+    MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
       eventType: "downloadavailable",
       manifestURL: aApp.manifestURL,
       requestID: aData.requestID
     });
   }),
 
   // A hosted app is updated if the app manifest or the appcache needs
   // updating. Even if the app manifest has not changed, we still check
@@ -2418,22 +2315,22 @@ this.DOMApplicationRegistry = {
 
     aApp.manifest = aNewManifest || aOldManifest;
 
     let manifest =
       new ManifestHelper(aApp.manifest, aApp.origin, aApp.manifestURL);
     aApp.role = manifest.role || "";
 
     if (!AppsUtils.checkAppRole(aApp.role, aApp.appStatus)) {
-      this.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: aApp,
         manifest: aApp.manifest,
         id: aApp.id
       });
-      this.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloadapplied",
         manifestURL: aApp.manifestURL,
         requestID: aData.requestID
       });
       delete aApp.manifest;
       return;
     }
 
@@ -2469,22 +2366,22 @@ this.DOMApplicationRegistry = {
 
     // Update the registry.
     this.webapps[aId] = aApp;
     yield this._saveApps();
 
     if ((aApp.kind !== this.kHostedAppcache &&
          aApp.kind !== this.kTrustedHosted) ||
          !aApp.manifest.appcache_path) {
-      this.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: aApp,
         manifest: aApp.manifest,
         id: aApp.id
       });
-      this.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloadapplied",
         manifestURL: aApp.manifestURL,
         requestID: aData.requestID
       });
     } else {
       // Check if the appcache is updatable, and send "downloadavailable" or
       // "downloadapplied".
       debug("updateHostedApp: updateSvc.checkForUpdate for " +
@@ -2503,22 +2400,22 @@ this.DOMApplicationRegistry = {
 
       let eventType =
         topic == "offline-cache-update-available" ? "downloadavailable"
                                                   : "downloadapplied";
 
       aApp.downloadAvailable = (eventType == "downloadavailable");
       yield this._saveApps();
 
-      this.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: aApp,
         manifest: aApp.manifest,
         id: aApp.id
       });
-      this.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: eventType,
         manifestURL: aApp.manifestURL,
         requestID: aData.requestID
       });
     }
 
     delete aApp.manifest;
   }),
@@ -3045,17 +2942,17 @@ this.DOMApplicationRegistry = {
 
     // Store the manifest and the updateManifest.
     this._writeManifestFile(app.id, false, aManifest);
     if (aUpdateManifest) {
       this._writeManifestFile(app.id, true, aUpdateManifest);
     }
 
     this._saveApps().then(() => {
-      this.broadcastMessage("Webapps:AddApp",
+      MessageBroadcaster.broadcastMessage("Webapps:AddApp",
                             { id: app.id, app: app, manifest: aManifest });
     });
   }),
 
   confirmInstall: Task.async(function*(aData, aProfileDir, aInstallSuccessCallback) {
     debug("confirmInstall");
 
     let origin = Services.io.newURI(aData.app.origin, null, null);
@@ -3161,17 +3058,17 @@ this.DOMApplicationRegistry = {
 
     // We notify about the successful installation via mgmt.oninstall and the
     // corresponding DOMRequest.onsuccess event as soon as the app is properly
     // saved in the registry.
     yield this._saveApps();
 
     aData.isPackage ? appObject.updateManifest = jsonManifest :
                       appObject.manifest = jsonManifest;
-    this.broadcastMessage("Webapps:AddApp", { id: id, app: appObject });
+    MessageBroadcaster.broadcastMessage("Webapps:AddApp", { id: id, app: appObject });
 
     if (!aData.isPackage) {
       this.updateAppHandlers(null, app.manifest, app);
       if (aInstallSuccessCallback) {
         try {
           yield aInstallSuccessCallback(app, app.manifest);
         } catch (e) {
           // Ignore exceptions during the local installation of
@@ -3186,17 +3083,17 @@ this.DOMApplicationRegistry = {
       // Skip directly to onInstallSuccessAck, since there isn't
       // a WebappsRegistry to receive Webapps:Install:Return:OK and respond
       // Webapps:Install:Return:Ack when an app is being auto-installed.
       this.onInstallSuccessAck(app.manifestURL);
     } else {
       // Broadcast Webapps:Install:Return:OK so the WebappsRegistry can notify
       // the installing page about the successful install, after which it'll
       // respond Webapps:Install:Return:Ack, which calls onInstallSuccessAck.
-      this.broadcastMessage("Webapps:Install:Return:OK", aData);
+      MessageBroadcaster.broadcastMessage("Webapps:Install:Return:OK", aData);
     }
 
     Services.obs.notifyObservers(null, "webapps-installed",
       JSON.stringify({ manifestURL: app.manifestURL }));
 
     if (aData.forceSuccessAck) {
       // If it's a local install, there's no content process so just
       // ack the install.
@@ -3243,17 +3140,17 @@ this.DOMApplicationRegistry = {
     yield this._saveApps();
 
     this.updateAppHandlers(null, aManifest, aNewApp);
     // Clear the manifest cache in case it holds the update manifest.
     if (aId in this._manifestCache) {
       delete this._manifestCache[aId];
     }
 
-    this.broadcastMessage("Webapps:AddApp",
+    MessageBroadcaster.broadcastMessage("Webapps:AddApp",
                           { id: aId, app: aNewApp, manifest: aManifest });
     Services.obs.notifyObservers(null, "webapps-installed",
       JSON.stringify({ manifestURL: aNewApp.manifestURL }));
 
     if (supportUseCurrentProfile()) {
       // Update the permissions for this app.
       PermissionsInstaller.installPermissions({
         manifest: aManifest,
@@ -3271,29 +3168,29 @@ this.DOMApplicationRegistry = {
         yield aInstallSuccessCallback(aNewApp, aManifest, zipFile.path);
       } catch (e) {
         // Ignore exceptions during the local installation of
         // an app. If it fails, the app will anyway be considered
         // as not installed because isLaunchable will return false.
       }
     }
 
-    this.broadcastMessage("Webapps:UpdateState", {
+    MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
       app: app,
       manifest: aManifest,
       manifestURL: aNewApp.manifestURL
     });
 
     // Check if we have asm.js code to preload for this application.
     yield ScriptPreloader.preload(aNewApp, aManifest);
 
     // Update langpack information.
     yield Langpacks.register(aNewApp, aManifest);
 
-    this.broadcastMessage("Webapps:FireEvent", {
+    MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
       eventType: ["downloadsuccess", "downloadapplied"],
       manifestURL: aNewApp.manifestURL
     });
   }),
 
   _nextLocalId: function() {
     let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
 
@@ -3404,17 +3301,17 @@ this.DOMApplicationRegistry = {
 
     // initialize the progress to 0 right now
     aOldApp.progress = 0;
 
     // Save the current state of the app to handle cases where we may be
     // retrying a past download.
     yield DOMApplicationRegistry._saveApps();
 
-    DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
+    MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         // Clear any previous download errors.
         error: null,
         app: aOldApp,
         id: aId
     });
 
     let zipFile = yield this._getPackage(requestChannel, aId, aOldApp, aNewApp);
 
@@ -3573,23 +3470,23 @@ this.DOMApplicationRegistry = {
         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
       }
     };
 
     return requestChannel;
   },
 
   _sendDownloadProgressEvent: function(aNewApp, aProgress) {
-    this.broadcastMessage("Webapps:UpdateState", {
+    MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
       app: {
         progress: aProgress
       },
       id: aNewApp.id
     });
-    this.broadcastMessage("Webapps:FireEvent", {
+    MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
       eventType: "progress",
       manifestURL: aNewApp.manifestURL
     });
   },
 
   _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
     let deferred = Promise.defer();
 
@@ -3687,21 +3584,21 @@ this.DOMApplicationRegistry = {
         staged.moveTo(staged.parent, "update.webapp");
       } catch (ex) {
         // We don't really mind much if this fails.
       }
     }
 
     // Save the updated registry, and cleanup the tmp directory.
     this._saveApps().then(() => {
-      this.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: aApp,
         id: aApp.id
       });
-      this.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         manifestURL: aApp.manifestURL,
         eventType: ["downloadsuccess", "downloadapplied"]
       });
     });
     let file = FileUtils.getFile("TmpD", ["webapps", aApp.id], false);
     if (file && file.exists()) {
       file.remove(true);
     }
@@ -4035,17 +3932,17 @@ this.DOMApplicationRegistry = {
         delete this.webapps[oldId];
         // Rename the directories where the files are installed.
         [DIRECTORY_NAME, "TmpD"].forEach(function(aDir) {
           let parent = FileUtils.getDir(aDir, ["webapps"], true, true);
           let dir = FileUtils.getDir(aDir, ["webapps", oldId], true, true);
           dir.moveTo(parent, newId);
         });
         // Signals that we need to swap the old id with the new app.
-        this.broadcastMessage("Webapps:UpdateApp", { oldId: oldId,
+        MessageBroadcaster.broadcastMessage("Webapps:UpdateApp", { oldId: oldId,
                                                      newId: newId,
                                                      app: aOldApp });
 
       }
     }
   },
 
   _getIds: function(aIsSigned, aZipReader, aConverter, aNewApp, aOldApp,
@@ -4147,22 +4044,22 @@ this.DOMApplicationRegistry = {
 
     // Erase the .staged properties only if there's no download available
     // anymore.
     if (!aOldApp.downloadAvailable && aOldApp.staged) {
       delete aOldApp.staged;
     }
 
     this._saveApps().then(() => {
-      this.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: aOldApp,
         error: aError,
         id: aId
       });
-      this.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL:  aNewApp.manifestURL
       });
     });
     AppDownloadManager.remove(aNewApp.manifestURL);
   },
 
   doUninstall: Task.async(function*(aData, aMm) {
@@ -4231,18 +4128,18 @@ this.DOMApplicationRegistry = {
     try {
       dir.remove(true);
     } catch (e) {}
 
     delete this.webapps[id];
 
     yield this._saveApps();
 
-    this.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", aApp);
-    this.broadcastMessage("Webapps:RemoveApp", { id: id });
+    MessageBroadcaster.broadcastMessage("Webapps:Uninstall:Broadcast:Return:OK", aApp);
+    MessageBroadcaster.broadcastMessage("Webapps:RemoveApp", { id: id });
 
     return aApp;
   }),
 
   _promptForUninstall: function(aData) {
     let deferred = Promise.defer();
     this._pendingUninstalls[aData.requestID] = deferred;
     Services.obs.notifyObservers(null, "webapps-ask-uninstall",
@@ -4652,21 +4549,21 @@ this.DOMApplicationRegistry = {
     if (!id || !this.webapps[id]) {
       return;
     }
 
     debug("Enabling " + id);
     let app = this.webapps[id];
     app.enabled = aData.enabled;
     this._saveApps().then(() => {
-      DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: app,
         id: app.id
       });
-      this.broadcastMessage("Webapps:SetEnabled:Return", app);
+      MessageBroadcaster.broadcastMessage("Webapps:SetEnabled:Return", app);
     });
 
     // Update customization.
     this.getManifestFor(app.manifestURL).then((aManifest) => {
       app.enabled ? UserCustomizations.register(aManifest, app)
                   : UserCustomizations.unregister(aManifest, app);
     });
   },
@@ -4876,21 +4773,21 @@ let AppcacheObserver = function(aApp) {
   // Send a first progress event to correctly set the DOM object's properties.
   this._sendProgressEvent();
 };
 
 AppcacheObserver.prototype = {
   // nsIOfflineCacheUpdateObserver implementation
   _sendProgressEvent: function() {
     let app = this.app;
-    DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
+    MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
       app: app,
       id: app.id
     });
-    DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
+    MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
       eventType: "progress",
       manifestURL: app.manifestURL
     });
   },
 
   updateStateChanged: function appObs_Update(aUpdate, aState) {
     let mustSave = false;
     let app = this.app;
@@ -4908,21 +4805,21 @@ AppcacheObserver.prototype = {
       if (aStatus != "installed") {
         self._sendProgressEvent();
         return;
       }
 
       app.updateTime = Date.now();
       app.downloading = false;
       app.downloadAvailable = false;
-      DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: app,
         id: app.id
       });
-      DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: ["downloadsuccess", "downloadapplied"],
         manifestURL: app.manifestURL
       });
     }
 
     let setError = function appObs_setError(aError) {
       debug("Offlinecache setError to " + aError);
       app.downloading = false;
@@ -4930,22 +4827,22 @@ AppcacheObserver.prototype = {
 
       // If we are canceling the download, we already send a DOWNLOAD_CANCELED
       // error.
       if (app.isCanceling) {
         delete app.isCanceling;
         return;
       }
 
-      DOMApplicationRegistry.broadcastMessage("Webapps:UpdateState", {
+      MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
         app: app,
         error: aError,
         id: app.id
       });
-      DOMApplicationRegistry.broadcastMessage("Webapps:FireEvent", {
+      MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
         eventType: "downloaderror",
         manifestURL: app.manifestURL
       });
     }
 
     switch (aState) {
       case Ci.nsIOfflineCacheUpdateObserver.STATE_ERROR:
         aUpdate.removeObserver(this);
--- a/dom/apps/moz.build
+++ b/dom/apps/moz.build
@@ -29,16 +29,17 @@ EXTRA_COMPONENTS += [
     'Webapps.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'AppDownloadManager.jsm',
     'AppsServiceChild.jsm',
     'FreeSpaceWatcher.jsm',
     'Langpacks.jsm',
+    'MessageBroadcaster.jsm',
     'OfflineCacheInstaller.jsm',
     'PermissionsInstaller.jsm',
     'PermissionsTable.jsm',
     'StoreTrustAnchor.jsm',
     'UserCustomizations.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
--- a/toolkit/devtools/server/actors/webapps.js
+++ b/toolkit/devtools/server/actors/webapps.js
@@ -207,16 +207,17 @@ PackageUploadBulkActor.prototype.request
 function WebappsActor(aConnection) {
   debug("init");
   // Load actor dependencies lazily as this actor require extra environnement
   // preparation to work (like have a profile setup in xpcshell tests)
 
   Cu.import("resource://gre/modules/Webapps.jsm");
   Cu.import("resource://gre/modules/AppsUtils.jsm");
   Cu.import("resource://gre/modules/FileUtils.jsm");
+  Cu.import("resource://gre/modules/MessageBroadcaster.jsm");
 
   // Keep reference of already connected app processes.
   // values: app frame message manager
   this._connectedApps = new Set();
 
   this.conn = aConnection;
   this._uploads = [];
   this._actorPool = new ActorPool(this.conn);
@@ -284,27 +285,27 @@ WebappsActor.prototype = {
         if (aApp.kind == undefined) {
           aApp.kind = manifest.appcache_path ? reg.kHostedAppcache
                                              : reg.kHosted;
         }
 
         // Needed to evict manifest cache on content side
         // (has to be dispatched first, otherwise other messages like
         // Install:Return:OK are going to use old manifest version)
-        reg.broadcastMessage("Webapps:UpdateState", {
+        MessageBroadcaster.broadcastMessage("Webapps:UpdateState", {
           app: aApp,
           manifest: manifest,
           id: aApp.id
         });
-        reg.broadcastMessage("Webapps:FireEvent", {
+        MessageBroadcaster.broadcastMessage("Webapps:FireEvent", {
           eventType: ["downloadsuccess", "downloadapplied"],
           manifestURL: aApp.manifestURL
         });
-        reg.broadcastMessage("Webapps:AddApp", { id: aId, app: aApp });
-        reg.broadcastMessage("Webapps:Install:Return:OK", {
+        MessageBroadcaster.broadcastMessage("Webapps:AddApp", { id: aId, app: aApp });
+        MessageBroadcaster.broadcastMessage("Webapps:Install:Return:OK", {
           app: aApp,
           oid: "foo",
           requestID: "bar"
         });
 
         Services.obs.notifyObservers(null, "webapps-installed",
           JSON.stringify({ manifestURL: aApp.manifestURL }));